Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76f24a755b |
+66
-2
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import (
|
||||
Depends,
|
||||
@@ -20,8 +21,17 @@ from paskia.fastapi import authz, session, user
|
||||
from paskia.fastapi.response import MsgspecResponse
|
||||
from paskia.fastapi.session import AUTH_COOKIE, AUTH_COOKIE_NAME, get_client_ip
|
||||
from paskia.globals import passkey as global_passkey
|
||||
from paskia.util import hostutil, htmlutil, passphrase, userinfo
|
||||
from paskia.util.apistructs import ApiSettings, ApiTokenInfo, ApiValidateResponse
|
||||
from paskia.util import hostutil, htmlutil, passphrase, permutil, userinfo
|
||||
from paskia.util.apistructs import (
|
||||
ApiCheckUserResponse,
|
||||
ApiOrgContext,
|
||||
ApiRoleContext,
|
||||
ApiSessionContext,
|
||||
ApiSettings,
|
||||
ApiTokenInfo,
|
||||
ApiUserContext,
|
||||
ApiValidateResponse,
|
||||
)
|
||||
|
||||
bearer_auth = HTTPBearer(auto_error=False)
|
||||
|
||||
@@ -109,6 +119,60 @@ async def validate_token(
|
||||
)
|
||||
|
||||
|
||||
@app.get("/check")
|
||||
async def check_user(
|
||||
request: Request,
|
||||
user_uuid: UUID = Query(..., alias="user"),
|
||||
perm: list[str] = Query([]),
|
||||
):
|
||||
"""Check permissions for a user by UUID without requiring a session.
|
||||
|
||||
Query Params:
|
||||
- user: UUID of the user to check.
|
||||
- perm: repeated permission scope the user must possess (ALL required).
|
||||
|
||||
Returns 200 with valid=True/False and the user's effective permissions,
|
||||
scoped to the requesting host (domain-restricted permissions are filtered).
|
||||
Returns 404 if the user UUID does not exist.
|
||||
|
||||
No session cookie is read or written. Caller authentication is not required.
|
||||
"""
|
||||
data = db.data()
|
||||
try:
|
||||
u = data.users[user_uuid]
|
||||
role = u.role
|
||||
org = role.org
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
host = hostutil.normalize_host(request.headers.get("host"))
|
||||
org_perm_uuids = {p.uuid for p in org.permissions}
|
||||
|
||||
effective_perms = []
|
||||
for perm_uuid in role.permission_set:
|
||||
if perm_uuid not in org_perm_uuids:
|
||||
continue
|
||||
try:
|
||||
p = data.permissions[perm_uuid]
|
||||
except KeyError:
|
||||
continue
|
||||
if p.domain is not None and p.domain != host:
|
||||
continue
|
||||
effective_perms.append(p)
|
||||
|
||||
required = " ".join(perm).split()
|
||||
effective_scopes = {p.scope for p in effective_perms}
|
||||
valid = permutil.has_all_scopes(effective_scopes, required)
|
||||
|
||||
ctx = ApiSessionContext(
|
||||
user=ApiUserContext(uuid=u.uuid, display_name=u.display_name, theme=u.theme),
|
||||
org=ApiOrgContext(uuid=org.uuid, display_name=org.display_name),
|
||||
role=ApiRoleContext(uuid=role.uuid, display_name=role.display_name),
|
||||
permissions=sorted(effective_scopes),
|
||||
)
|
||||
return MsgspecResponse(ApiCheckUserResponse(valid=valid, ctx=ctx))
|
||||
|
||||
|
||||
@app.get("/forward")
|
||||
async def forward_authentication(
|
||||
request: Request,
|
||||
|
||||
@@ -233,6 +233,13 @@ class ApiValidateResponse(msgspec.Struct):
|
||||
ctx: ApiSessionContext
|
||||
|
||||
|
||||
class ApiCheckUserResponse(msgspec.Struct):
|
||||
"""Response struct for check-user endpoint."""
|
||||
|
||||
valid: bool
|
||||
ctx: ApiSessionContext
|
||||
|
||||
|
||||
class ApiAdminInfo(msgspec.Struct, kw_only=True):
|
||||
"""Combined admin info response."""
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from fnmatch import fnmatchcase
|
||||
from paskia.authsession import session_ctx
|
||||
from paskia.util.hostutil import normalize_host
|
||||
|
||||
__all__ = ["has_any", "has_all", "session_context"]
|
||||
__all__ = ["has_any", "has_all", "has_all_scopes", "session_context"]
|
||||
|
||||
|
||||
def _match(perms: set[str], patterns: Sequence[str]):
|
||||
@@ -36,6 +36,11 @@ def has_all(ctx, patterns: Sequence[str]) -> bool:
|
||||
return all(_match(_get_effective_scopes(ctx), patterns)) if ctx else False
|
||||
|
||||
|
||||
def has_all_scopes(scopes: set[str], patterns: Sequence[str]) -> bool:
|
||||
"""Check that a pre-computed scope set satisfies all required patterns."""
|
||||
return all(_match(scopes, patterns)) if patterns else True
|
||||
|
||||
|
||||
async def session_context(auth: str | None, host: str | None = None):
|
||||
if not auth:
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user