diff --git a/paskia/db/structs.py b/paskia/db/structs.py index 2e49f38..0f1daf6 100644 --- a/paskia/db/structs.py +++ b/paskia/db/structs.py @@ -202,7 +202,6 @@ class User(msgspec.Struct, dict=True, omit_defaults=True, kw_only=True): Immutable fields: created_at (set at creation, never modified) uuid is derived from created_at using uuid7. """ - display_name: str role_uuid: UUID = msgspec.field(name="role") created_at: datetime diff --git a/paskia/fastapi/admin.py b/paskia/fastapi/admin.py index bb7e13d..188a07c 100644 --- a/paskia/fastapi/admin.py +++ b/paskia/fastapi/admin.py @@ -27,8 +27,10 @@ from paskia.util import ( from paskia.util.apistructs import ( ApiAaguidInfo, ApiCreateLinkResponse, + ApiOrg, ApiOrgResponse, ApiPermission, + ApiRole, ApiUser, ApiUserDetail, ApiUserSession, @@ -97,13 +99,14 @@ async def admin_list_orgs(request: Request, auth=AUTH_COOKIE): def org_to_dict(o): roles = o.roles return ApiOrgResponse( - org=o, + uuid=o.uuid, + display_name=o.display_name, permissions={p.uuid: p for p in o.permissions}, roles={r.uuid: r for r in roles}, users={u.uuid: u for r in roles for u in r.users}, ) - return MsgspecResponse({o.uuid: org_to_dict(o) for o in orgs}) + return MsgspecResponse([org_to_dict(o) for o in orgs]) @app.post("/orgs") @@ -583,6 +586,8 @@ async def admin_get_user_detail( ).items() }, sessions=sessions, + org=ApiOrg.from_db(user.org), + role=ApiRole.from_db(user.role), ) ) @@ -832,7 +837,7 @@ async def admin_list_permissions(request: Request, auth=AUTH_COOKIE): host=request.headers.get("host"), ) perms = db.data().permissions.values() if master_admin(ctx) else ctx.org.permissions - return MsgspecResponse({p.uuid: ApiPermission.from_db(p) for p in perms}) + return MsgspecResponse([ApiPermission.from_db(p) for p in perms]) @app.post("/permissions") diff --git a/paskia/util/apistructs.py b/paskia/util/apistructs.py index 0259815..d71b3c0 100644 --- a/paskia/util/apistructs.py +++ b/paskia/util/apistructs.py @@ -123,6 +123,8 @@ class ApiUserDetail(msgspec.Struct, kw_only=True): credentials: dict[UUID, Credential] aaguid_info: dict[str, ApiAaguidInfo] sessions: list[ApiUserSession] + org: ApiOrg + role: ApiRole permissions: dict[UUID, ApiPermission] = {} @@ -134,7 +136,8 @@ class ApiUserDetail(msgspec.Struct, kw_only=True): class ApiOrgResponse(msgspec.Struct, kw_only=True): """Org response containing Org with roles and users as UUID-keyed dicts.""" - org: Org + uuid: UUID + display_name: str permissions: dict[UUID, Permission] roles: dict[UUID, Role] users: dict[UUID, User] diff --git a/paskia/util/userinfo.py b/paskia/util/userinfo.py index 06311da..20c1329 100644 --- a/paskia/util/userinfo.py +++ b/paskia/util/userinfo.py @@ -6,8 +6,10 @@ from paskia.db import SessionContext from paskia.util import hostutil from paskia.util.apistructs import ( ApiAaguidInfo, + ApiOrg, ApiOrgContext, ApiPermission, + ApiRole, ApiRoleContext, ApiSessionContext, ApiUser, @@ -59,15 +61,19 @@ async def build_user_info( for s in user.sessions ] - return ApiUserDetail( - user=ApiUser.from_db(user), - credentials={c.uuid: c for c in user.credentials}, - aaguid_info={ + return { + "ctx": { + "user": ApiUser.from_db(user), + "permissions": {p.uuid: ApiPermission.from_db(p) for p in ctx.permissions} + if ctx + else {}, + }, + "credentials": {c.uuid: c for c in user.credentials}, + "aaguid_info": { k: ApiAaguidInfo(**v) for k, v in aaguid.filter(c.aaguid for c in user.credentials).items() }, - sessions=sessions, - permissions={p.uuid: ApiPermission.from_db(p) for p in ctx.permissions} - if ctx - else {}, - ) + "sessions": sessions, + "org": ApiOrg.from_db(user.org), + "role": ApiRole.from_db(user.role), + } diff --git a/tests/test_admin.py b/tests/test_admin.py index 4b673dd..37a44ee 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -791,7 +791,8 @@ class TestAdminUsersInOrg: ) assert response.status_code == 200 data = response.json() - assert "display_name" in data + assert "user" in data + assert "display_name" in data["user"] assert "credentials" in data assert "sessions" in data assert "aaguid_info" in data @@ -842,7 +843,8 @@ class TestAdminUsersInOrg: ) assert response.status_code == 200 data = response.json() - assert "display_name" in data + assert "user" in data + assert "display_name" in data["user"] @pytest.mark.asyncio async def test_update_user_display_name_in_org( @@ -850,7 +852,7 @@ class TestAdminUsersInOrg: ): """Admin should be able to update user display name.""" response = await client.patch( - f"/auth/api/admin/users/{test_user.uuid}/display-name", + f"/auth/api/admin/users/{test_user.uuid}/info", json={"display_name": "Updated Admin Name"}, headers={**auth_headers(session_token), "Host": "localhost:4401"}, ) @@ -863,7 +865,7 @@ class TestAdminUsersInOrg: """Updating non-existent user should return 404.""" fake_uuid = uuid7.create() response = await client.patch( - f"/auth/api/admin/users/{fake_uuid}/display-name", + f"/auth/api/admin/users/{fake_uuid}/info", json={"display_name": "New Name"}, headers={**auth_headers(session_token), "Host": "localhost:4401"}, ) @@ -881,7 +883,7 @@ class TestAdminUsersInOrg: ): """Org admin cannot update user from another org.""" response = await client.patch( - f"/auth/api/admin/users/{second_org_user.uuid}/display-name", + f"/auth/api/admin/users/{second_org_user.uuid}/info", json={"display_name": "New Name"}, headers={**auth_headers(org_admin_session_token), "Host": "localhost:4401"}, ) @@ -893,13 +895,13 @@ class TestAdminUsersInOrg: ): """Updating user with empty display name should fail.""" response = await client.patch( - f"/auth/api/admin/users/{test_user.uuid}/display-name", + f"/auth/api/admin/users/{test_user.uuid}/info", json={"display_name": " "}, headers={**auth_headers(session_token), "Host": "localhost:4401"}, ) assert response.status_code == 400 data = response.json() - assert "display_name required" in data["detail"] + assert "display_name cannot be empty" in data["detail"] @pytest.mark.asyncio async def test_update_user_display_name_too_long( @@ -907,13 +909,13 @@ class TestAdminUsersInOrg: ): """Updating user with too long display name should fail.""" response = await client.patch( - f"/auth/api/admin/users/{test_user.uuid}/display-name", - json={"display_name": "x" * 100}, + f"/auth/api/admin/users/{test_user.uuid}/info", + json={"display_name": "x" * 65}, headers={**auth_headers(session_token), "Host": "localhost:4401"}, ) assert response.status_code == 400 data = response.json() - assert "too long" in data["detail"] + assert "display_name too long" in data["detail"] @pytest.mark.asyncio async def test_update_user_role_in_org( @@ -1702,7 +1704,7 @@ class TestOrgAdminAuthExceptions: ): """Regular user trying to update display name should get 403.""" response = await client.patch( - f"/auth/api/admin/users/{test_user.uuid}/display-name", + f"/auth/api/admin/users/{test_user.uuid}/info", json={"display_name": "New Name"}, headers={**auth_headers(regular_session_token), "Host": "localhost:4401"}, )