OAuth2 OpenID Connect provider support etc. #3

Merged
LeoVasanko merged 65 commits from oidconnect into main 2026-02-18 02:40:27 +00:00
5 changed files with 40 additions and 25 deletions
Showing only changes of commit 9b23e7afbb - Show all commits
-1
View File
@@ -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
+8 -3
View File
@@ -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")
+4 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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"},
)