diff --git a/ariadne/app.py b/ariadne/app.py index 3e4f266..5176e40 100644 --- a/ariadne/app.py +++ b/ariadne/app.py @@ -384,6 +384,8 @@ def list_access_requests(ctx: AuthContext = Depends(_require_auth)) -> JSONRespo "id": row.get("request_code"), "username": row.get("username"), "email": row.get("contact_email") or "", + "first_name": row.get("first_name") or "", + "last_name": row.get("last_name") or "", "request_code": row.get("request_code"), "created_at": created_at.isoformat() if isinstance(created_at, datetime) else "", "note": row.get("note") or "", diff --git a/ariadne/db/schema.py b/ariadne/db/schema.py index 43246c2..14ba6f5 100644 --- a/ariadne/db/schema.py +++ b/ariadne/db/schema.py @@ -49,4 +49,6 @@ ARIADNE_ACCESS_REQUEST_ALTER = [ "ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS approval_flags TEXT[]", "ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS approval_note TEXT", "ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS denial_note TEXT", + "ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS first_name TEXT", + "ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS last_name TEXT", ] diff --git a/ariadne/db/storage.py b/ariadne/db/storage.py index 902eb02..e7b24b4 100644 --- a/ariadne/db/storage.py +++ b/ariadne/db/storage.py @@ -25,6 +25,8 @@ REQUIRED_TASKS = ( class AccessRequest: request_code: str username: str + first_name: str + last_name: str contact_email: str status: str email_verified_at: datetime | None @@ -113,8 +115,8 @@ class Storage: row = self._portal_db.fetchone( """ SELECT request_code, username, contact_email, status, email_verified_at, - initial_password, initial_password_revealed_at, provision_attempted_at, - approval_flags, approval_note, denial_note + first_name, last_name, initial_password, initial_password_revealed_at, + provision_attempted_at, approval_flags, approval_note, denial_note FROM access_requests WHERE request_code = %s """, @@ -128,8 +130,8 @@ class Storage: row = self._portal_db.fetchone( """ SELECT request_code, username, contact_email, status, email_verified_at, - initial_password, initial_password_revealed_at, provision_attempted_at, - approval_flags, approval_note, denial_note + first_name, last_name, initial_password, initial_password_revealed_at, + provision_attempted_at, approval_flags, approval_note, denial_note FROM access_requests WHERE username = %s ORDER BY created_at DESC @@ -144,7 +146,7 @@ class Storage: def list_pending_requests(self) -> list[dict[str, Any]]: return self._portal_db.fetchall( """ - SELECT request_code, username, contact_email, note, status, created_at + SELECT request_code, username, contact_email, first_name, last_name, note, status, created_at FROM access_requests WHERE status = 'pending' ORDER BY created_at ASC @@ -156,8 +158,8 @@ class Storage: rows = self._portal_db.fetchall( """ SELECT request_code, username, contact_email, status, email_verified_at, - initial_password, initial_password_revealed_at, provision_attempted_at, - approval_flags, approval_note, denial_note + first_name, last_name, initial_password, initial_password_revealed_at, + provision_attempted_at, approval_flags, approval_note, denial_note FROM access_requests WHERE status IN ('approved', 'accounts_building') ORDER BY created_at ASC @@ -351,6 +353,8 @@ class Storage: return AccessRequest( request_code=str(row.get("request_code") or ""), username=str(row.get("username") or ""), + first_name=str(row.get("first_name") or ""), + last_name=str(row.get("last_name") or ""), contact_email=str(row.get("contact_email") or ""), status=str(row.get("status") or ""), email_verified_at=row.get("email_verified_at"), diff --git a/ariadne/manager/provisioning.py b/ariadne/manager/provisioning.py index 5e43bf9..272e8d4 100644 --- a/ariadne/manager/provisioning.py +++ b/ariadne/manager/provisioning.py @@ -44,6 +44,8 @@ class ProvisionOutcome: class RequestContext: request_code: str username: str + first_name: str + last_name: str contact_email: str email_verified_at: datetime | None status: str @@ -123,6 +125,8 @@ class ProvisioningManager: row = conn.execute( """ SELECT username, + first_name, + last_name, contact_email, email_verified_at, status, @@ -142,6 +146,8 @@ class ProvisioningManager: return RequestContext( request_code=request_code, username=username, + first_name=str(row.get("first_name") or ""), + last_name=str(row.get("last_name") or ""), contact_email=str(row.get("contact_email") or ""), email_verified_at=row.get("email_verified_at"), status=str(row.get("status") or ""), @@ -450,8 +456,15 @@ class ProvisioningManager: if existing_email_user and (existing_email_user.get("username") or "") != username: raise RuntimeError("email is already associated with an existing Atlas account") - def _new_user_payload(self, username: str, email: str, mailu_email: str) -> dict[str, Any]: - return { + def _new_user_payload( + self, + username: str, + email: str, + mailu_email: str, + first_name: str, + last_name: str, + ) -> dict[str, Any]: + payload = { "username": username, "enabled": True, "email": email, @@ -462,6 +475,13 @@ class ProvisioningManager: MAILU_ENABLED_ATTR: ["true"], }, } + if first_name: + payload["firstName"] = first_name + if last_name: + payload["lastName"] = last_name + else: + payload["lastName"] = username + return payload def _create_or_fetch_user(self, ctx: RequestContext) -> dict[str, Any]: user = keycloak_admin.find_user(ctx.username) @@ -469,7 +489,7 @@ class ProvisioningManager: return user email = self._require_verified_email(ctx) self._ensure_email_unused(email, ctx.username) - payload = self._new_user_payload(ctx.username, email, ctx.mailu_email) + payload = self._new_user_payload(ctx.username, email, ctx.mailu_email, ctx.first_name, ctx.last_name) try: created_id = keycloak_admin.create_user(payload) return keycloak_admin.get_user(created_id) diff --git a/ariadne/services/keycloak_profile.py b/ariadne/services/keycloak_profile.py index 759ece8..3e1adb3 100644 --- a/ariadne/services/keycloak_profile.py +++ b/ariadne/services/keycloak_profile.py @@ -23,10 +23,9 @@ class ProfileSyncSummary: def _profile_complete(user: dict[str, Any]) -> bool: email = user.get("email") if isinstance(user.get("email"), str) else "" - first_name = user.get("firstName") if isinstance(user.get("firstName"), str) else "" last_name = user.get("lastName") if isinstance(user.get("lastName"), str) else "" email_verified = bool(user.get("emailVerified")) - return bool(email.strip() and first_name.strip() and last_name.strip() and email_verified) + return bool(email.strip() and last_name.strip() and email_verified) def run_profile_sync() -> ProfileSyncSummary: diff --git a/tests/test_keycloak_profile.py b/tests/test_keycloak_profile.py index c34bc8c..39d20c3 100644 --- a/tests/test_keycloak_profile.py +++ b/tests/test_keycloak_profile.py @@ -15,7 +15,7 @@ def test_profile_sync_removes_required_actions(monkeypatch) -> None: "enabled": True, "email": "alice@example.com", "emailVerified": True, - "firstName": "Alice", + "firstName": "", "lastName": "Atlas", "requiredActions": ["UPDATE_PROFILE", "VERIFY_EMAIL"], } @@ -44,7 +44,7 @@ def test_profile_sync_skips_incomplete(monkeypatch) -> None: "enabled": True, "email": "bob@example.com", "emailVerified": True, - "firstName": "", + "firstName": "Bob", "lastName": "", "requiredActions": ["UPDATE_PROFILE"], } diff --git a/tests/test_provisioning.py b/tests/test_provisioning.py index aff738f..0a30714 100644 --- a/tests/test_provisioning.py +++ b/tests/test_provisioning.py @@ -209,6 +209,8 @@ def test_provisioning_creates_user_and_password(monkeypatch) -> None: row = { "username": "alice", + "first_name": "Alice", + "last_name": "Atlas", "contact_email": "alice@example.com", "email_verified_at": datetime.now(timezone.utc), "status": "approved", @@ -225,6 +227,8 @@ def test_provisioning_creates_user_and_password(monkeypatch) -> None: assert outcome.status == "awaiting_onboarding" assert admin.created_payload is not None + assert admin.created_payload.get("firstName") == "Alice" + assert admin.created_payload.get("lastName") == "Atlas" assert admin.reset_calls diff --git a/tests/test_storage.py b/tests/test_storage.py index 54e1a1c..5c2ff44 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -37,6 +37,8 @@ def test_row_to_request_flags() -> None: row = { "request_code": "abc", "username": "alice", + "first_name": "Alice", + "last_name": "Atlas", "contact_email": "a@example.com", "status": "pending", "email_verified_at": None, @@ -57,6 +59,8 @@ def test_access_requests_use_portal_db() -> None: portal_row = { "request_code": "req", "username": "alice", + "first_name": "Alice", + "last_name": "Atlas", "contact_email": "a@example.com", "status": "pending", "email_verified_at": None,