OAuth2 OpenID Connect provider support etc. #3
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
+15
-9
@@ -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),
|
||||
}
|
||||
|
||||
+13
-11
@@ -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"},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user