Database cleanup: make it synchronous because we work with in-memory data. Defer writes to disk and cleanup to background task. Tests passing.

This commit is contained in:
2026-01-23 01:22:47 +00:00
parent 7f3763b46d
commit 02e04da2c4
14 changed files with 548 additions and 556 deletions
+7 -7
View File
@@ -55,7 +55,7 @@ async def create_session(
raise ValueError(f"Host must be the same as or a subdomain of {rp_id}") raise ValueError(f"Host must be the same as or a subdomain of {rp_id}")
token = create_token() token = create_token()
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
await db.create_session( db.create_session(
user_uuid=user_uuid, user_uuid=user_uuid,
credential_uuid=credential_uuid, credential_uuid=credential_uuid,
key=session_key(token), key=session_key(token),
@@ -69,7 +69,7 @@ async def create_session(
async def get_reset(token: str) -> ResetToken: async def get_reset(token: str) -> ResetToken:
"""Validate a credential reset token. Returns None if the token is not well formed (i.e. it is another type of token).""" """Validate a credential reset token. Returns None if the token is not well formed (i.e. it is another type of token)."""
record = await db.get_reset_token(reset_key(token)) record = db.get_reset_token(reset_key(token))
if record and record.expiry >= datetime.now(timezone.utc): if record and record.expiry >= datetime.now(timezone.utc):
return record return record
raise ValueError("This authentication link is no longer valid.") raise ValueError("This authentication link is no longer valid.")
@@ -80,11 +80,11 @@ async def get_session(token: str, host: str | None = None) -> Session:
host = hostutil.normalize_host(host) host = hostutil.normalize_host(host)
if not host: if not host:
raise ValueError("Invalid host") raise ValueError("Invalid host")
session = await db.get_session(session_key(token)) session = db.get_session(session_key(token))
if session and session_expiry(session) >= datetime.now(timezone.utc): if session and session_expiry(session) >= datetime.now(timezone.utc):
if session.host is None: if session.host is None:
# First time binding: store exact host:port (or IPv6 form) now. # First time binding: store exact host:port (or IPv6 form) now.
await db.set_session_host(session.key, host) db.set_session_host(session.key, host)
session.host = host session.host = host
elif session.host != host: elif session.host != host:
raise ValueError("Session host mismatch") raise ValueError("Session host mismatch")
@@ -94,10 +94,10 @@ async def get_session(token: str, host: str | None = None) -> Session:
async def refresh_session_token(token: str, *, ip: str, user_agent: str): async def refresh_session_token(token: str, *, ip: str, user_agent: str):
"""Refresh a session extending its expiry.""" """Refresh a session extending its expiry."""
session_record = await db.get_session(session_key(token)) session_record = db.get_session(session_key(token))
if not session_record: if not session_record:
raise ValueError("Session not found or expired") raise ValueError("Session not found or expired")
updated = await db.update_session( updated = db.update_session(
session_key(token), session_key(token),
ip=ip, ip=ip,
user_agent=user_agent, user_agent=user_agent,
@@ -110,4 +110,4 @@ async def refresh_session_token(token: str, *, ip: str, user_agent: str):
async def delete_credential(credential_uuid: UUID, auth: str, host: str | None = None): async def delete_credential(credential_uuid: UUID, auth: str, host: str | None = None):
"""Delete a specific credential for the current user.""" """Delete a specific credential for the current user."""
s = await get_session(auth, host=host) s = await get_session(auth, host=host)
await db.delete_credential(credential_uuid, s.user_uuid) db.delete_credential(credential_uuid, s.user_uuid)
+10 -10
View File
@@ -42,7 +42,7 @@ async def _create_and_log_admin_reset_link(user_uuid, message, session_type) ->
"""Create an admin reset link and log it with the provided message.""" """Create an admin reset link and log it with the provided message."""
token = passphrase.generate() token = passphrase.generate()
expiry = authsession.reset_expires() expiry = authsession.reset_expires()
await db.create_reset_token( db.create_reset_token(
user_uuid=user_uuid, user_uuid=user_uuid,
key=tokens.reset_key(token), key=tokens.reset_key(token),
expiry=expiry, expiry=expiry,
@@ -62,14 +62,14 @@ async def bootstrap_system() -> dict:
""" """
# Create permission first - will fail if already exists # Create permission first - will fail if already exists
perm0 = Permission(id="auth:admin", display_name="Master Admin") perm0 = Permission(id="auth:admin", display_name="Master Admin")
await db.create_permission(perm0) db.create_permission(perm0)
org = Org(uuid7.create(), "Organization") org = Org(uuid7.create(), "Organization")
await db.create_organization(org) db.create_organization(org)
# After creation, org.permissions now includes the auto-created org admin permission # After creation, org.permissions now includes the auto-created org admin permission
# Allow this org to grant global admin explicitly # Allow this org to grant global admin explicitly
await db.add_permission_to_organization(str(org.uuid), perm0.id) db.add_permission_to_organization(str(org.uuid), perm0.id)
# Create an Administration role granting both org and global admin # Create an Administration role granting both org and global admin
# Compose permissions for Administration role: global admin + org admin auto-perm # Compose permissions for Administration role: global admin + org admin auto-perm
@@ -79,7 +79,7 @@ async def bootstrap_system() -> dict:
"Administration", "Administration",
permissions=[perm0.id, *org.permissions], permissions=[perm0.id, *org.permissions],
) )
await db.create_role(role) db.create_role(role)
user = User( user = User(
uuid=uuid7.create(), uuid=uuid7.create(),
@@ -88,7 +88,7 @@ async def bootstrap_system() -> dict:
created_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc),
visits=0, visits=0,
) )
await db.create_user(user) db.create_user(user)
# Generate reset link and log it # Generate reset link and log it
reset_link = await _create_and_log_admin_reset_link( reset_link = await _create_and_log_admin_reset_link(
@@ -116,7 +116,7 @@ async def check_admin_credentials() -> bool:
""" """
try: try:
# Get permission organizations to find admin users # Get permission organizations to find admin users
permission_orgs = await db.get_permission_organizations( permission_orgs = db.get_permission_organizations(
"auth:admin" "auth:admin"
) )
@@ -124,7 +124,7 @@ async def check_admin_credentials() -> bool:
return False return False
# Get users from the first organization with admin permission # Get users from the first organization with admin permission
org_users = await db.get_organization_users( org_users = db.get_organization_users(
str(permission_orgs[0].uuid) str(permission_orgs[0].uuid)
) )
admin_users = [user for user, role in org_users if role == "Administration"] admin_users = [user for user, role in org_users if role == "Administration"]
@@ -134,7 +134,7 @@ async def check_admin_credentials() -> bool:
# Check first admin user for credentials # Check first admin user for credentials
admin_user = admin_users[0] admin_user = admin_users[0]
credentials = await db.get_credentials_by_user_uuid( credentials = db.get_credentials_by_user_uuid(
admin_user.uuid admin_user.uuid
) )
@@ -162,7 +162,7 @@ async def bootstrap_if_needed() -> bool:
""" """
try: try:
# Check if the admin permission exists - if it does, system is already bootstrapped # Check if the admin permission exists - if it does, system is already bootstrapped
await db.get_permission("auth:admin") db.get_permission("auth:admin")
# Permission exists, system is already bootstrapped # Permission exists, system is already bootstrapped
# Check if admin needs credentials (only for already-bootstrapped systems) # Check if admin needs credentials (only for already-bootstrapped systems)
await check_admin_credentials() await check_admin_credentials()
+11 -3
View File
@@ -3,13 +3,14 @@ Database module for WebAuthn passkey authentication.
This module re-exports the JSONL database types and implementation. This module re-exports the JSONL database types and implementation.
All data types are msgspec Structs for efficient serialization. All data types are msgspec Structs for efficient serialization.
Database methods are synchronous (no await needed).
Usage: Usage:
from paskia import db from paskia import db
# Access the database instance (after init) # Access the database instance (after init)
await db.create_session(...) db.create_session(...)
user = await db.get_user_by_uuid(uuid) user = db.get_user_by_uuid(uuid)
""" """
from paskia.db.json import ( from paskia.db.json import (
@@ -23,8 +24,11 @@ from paskia.db.json import (
SessionContext, SessionContext,
User, User,
init, init,
start_background,
stop_background,
start_cleanup,
stop_cleanup,
) )
from paskia.db.json import _db as _json_db
import paskia.db.json as _json_module import paskia.db.json as _json_module
@@ -63,4 +67,8 @@ __all__ = [
"SessionContext", "SessionContext",
"User", "User",
"init", "init",
"start_background",
"stop_background",
"start_cleanup",
"stop_cleanup",
] ]
+407 -426
View File
File diff suppressed because it is too large Load Diff
+46 -46
View File
@@ -59,7 +59,7 @@ async def admin_list_orgs(request: Request, auth=AUTH_COOKIE):
match=permutil.has_any, match=permutil.has_any,
host=request.headers.get("host"), host=request.headers.get("host"),
) )
orgs = await db.list_organizations() orgs = db.list_organizations()
if "auth:admin" not in ctx.role.permissions: if "auth:admin" not in ctx.role.permissions:
orgs = [o for o in orgs if f"auth:org:{o.uuid}" in ctx.role.permissions] orgs = [o for o in orgs if f"auth:org:{o.uuid}" in ctx.role.permissions]
@@ -72,7 +72,7 @@ async def admin_list_orgs(request: Request, auth=AUTH_COOKIE):
} }
async def org_to_dict(o): async def org_to_dict(o):
users = await db.get_organization_users(str(o.uuid)) users = db.get_organization_users(str(o.uuid))
return { return {
"uuid": str(o.uuid), "uuid": str(o.uuid),
"display_name": o.display_name, "display_name": o.display_name,
@@ -107,7 +107,7 @@ async def admin_create_org(
display_name = payload.get("display_name") or "New Organization" display_name = payload.get("display_name") or "New Organization"
permissions = payload.get("permissions") or [] permissions = payload.get("permissions") or []
org = OrgDC(uuid=org_uuid, display_name=display_name, permissions=permissions) org = OrgDC(uuid=org_uuid, display_name=display_name, permissions=permissions)
await db.create_organization(org) db.create_organization(org)
# Automatically create Administration role with org admin permission # Automatically create Administration role with org admin permission
role_uuid = uuid4() role_uuid = uuid4()
@@ -117,7 +117,7 @@ async def admin_create_org(
display_name="Administration", display_name="Administration",
permissions=[f"auth:org:{org_uuid}"], permissions=[f"auth:org:{org_uuid}"],
) )
await db.create_role(admin_role) db.create_role(admin_role)
return {"uuid": str(org_uuid)} return {"uuid": str(org_uuid)}
@@ -137,7 +137,7 @@ async def admin_update_org(
) )
from ..db import Org as OrgDC # local import to avoid cycles from ..db import Org as OrgDC # local import to avoid cycles
current = await db.get_organization(str(org_uuid)) current = db.get_organization(str(org_uuid))
display_name = payload.get("display_name") or current.display_name display_name = payload.get("display_name") or current.display_name
permissions = payload.get("permissions") permissions = payload.get("permissions")
if permissions is None: if permissions is None:
@@ -157,7 +157,7 @@ async def admin_update_org(
) )
org = OrgDC(uuid=org_uuid, display_name=display_name, permissions=permissions) org = OrgDC(uuid=org_uuid, display_name=display_name, permissions=permissions)
await db.update_organization(org) db.update_organization(org)
return {"status": "ok"} return {"status": "ok"}
@@ -175,7 +175,7 @@ async def admin_delete_org(org_uuid: UUID, request: Request, auth=AUTH_COOKIE):
# Delete organization-specific permissions # Delete organization-specific permissions
org_perm_pattern = f"org:{str(org_uuid).lower()}" org_perm_pattern = f"org:{str(org_uuid).lower()}"
all_permissions = await db.list_permissions() all_permissions = db.list_permissions()
for perm in all_permissions: for perm in all_permissions:
perm_id_lower = perm.id.lower() perm_id_lower = perm.id.lower()
# Check if permission contains "org:{uuid}" separated by colons or at boundaries # Check if permission contains "org:{uuid}" separated by colons or at boundaries
@@ -185,9 +185,9 @@ async def admin_delete_org(org_uuid: UUID, request: Request, auth=AUTH_COOKIE):
or perm_id_lower.endswith(f":{org_perm_pattern}") or perm_id_lower.endswith(f":{org_perm_pattern}")
or perm_id_lower == org_perm_pattern or perm_id_lower == org_perm_pattern
): ):
await db.delete_permission(perm.id) db.delete_permission(perm.id)
await db.delete_organization(org_uuid) db.delete_organization(org_uuid)
return {"status": "ok"} return {"status": "ok"}
@@ -201,7 +201,7 @@ async def admin_add_org_permission(
await authz.verify( await authz.verify(
auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all
) )
await db.add_permission_to_organization(str(org_uuid), permission_id) db.add_permission_to_organization(str(org_uuid), permission_id)
return {"status": "ok"} return {"status": "ok"}
@@ -215,7 +215,7 @@ async def admin_remove_org_permission(
await authz.verify( await authz.verify(
auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all
) )
await db.remove_permission_from_organization(str(org_uuid), permission_id) db.remove_permission_from_organization(str(org_uuid), permission_id)
return {"status": "ok"} return {"status": "ok"}
@@ -240,10 +240,10 @@ async def admin_create_role(
role_uuid = uuid4() role_uuid = uuid4()
display_name = payload.get("display_name") or "New Role" display_name = payload.get("display_name") or "New Role"
perms = payload.get("permissions") or [] perms = payload.get("permissions") or []
org = await db.get_organization(str(org_uuid)) org = db.get_organization(str(org_uuid))
grantable = set(org.permissions or []) grantable = set(org.permissions or [])
for pid in perms: for pid in perms:
await db.get_permission(pid) db.get_permission(pid)
if pid not in grantable: if pid not in grantable:
raise ValueError(f"Permission not grantable by org: {pid}") raise ValueError(f"Permission not grantable by org: {pid}")
role = RoleDC( role = RoleDC(
@@ -252,7 +252,7 @@ async def admin_create_role(
display_name=display_name, display_name=display_name,
permissions=perms, permissions=perms,
) )
await db.create_role(role) db.create_role(role)
return {"uuid": str(role_uuid)} return {"uuid": str(role_uuid)}
@@ -271,7 +271,7 @@ async def admin_update_role(
match=permutil.has_any, match=permutil.has_any,
host=request.headers.get("host"), host=request.headers.get("host"),
) )
role = await db.get_role(role_uuid) role = db.get_role(role_uuid)
if role.org_uuid != org_uuid: if role.org_uuid != org_uuid:
raise HTTPException(status_code=404, detail="Role not found in organization") raise HTTPException(status_code=404, detail="Role not found in organization")
from ..db import Role as RoleDC from ..db import Role as RoleDC
@@ -280,11 +280,11 @@ async def admin_update_role(
permissions = payload.get("permissions") permissions = payload.get("permissions")
if permissions is None: if permissions is None:
permissions = role.permissions permissions = role.permissions
org = await db.get_organization(str(org_uuid)) org = db.get_organization(str(org_uuid))
grantable = set(org.permissions or []) grantable = set(org.permissions or [])
existing_permissions = set(role.permissions) existing_permissions = set(role.permissions)
for pid in permissions: for pid in permissions:
await db.get_permission(pid) db.get_permission(pid)
if pid not in existing_permissions and pid not in grantable: if pid not in existing_permissions and pid not in grantable:
raise ValueError(f"Permission not grantable by org: {pid}") raise ValueError(f"Permission not grantable by org: {pid}")
@@ -302,7 +302,7 @@ async def admin_update_role(
display_name=display_name, display_name=display_name,
permissions=permissions, permissions=permissions,
) )
await db.update_role(updated) db.update_role(updated)
return {"status": "ok"} return {"status": "ok"}
@@ -320,7 +320,7 @@ async def admin_delete_role(
host=request.headers.get("host"), host=request.headers.get("host"),
max_age="5m", max_age="5m",
) )
role = await db.get_role(role_uuid) role = db.get_role(role_uuid)
if role.org_uuid != org_uuid: if role.org_uuid != org_uuid:
raise HTTPException(status_code=404, detail="Role not found in organization") raise HTTPException(status_code=404, detail="Role not found in organization")
@@ -328,7 +328,7 @@ async def admin_delete_role(
if ctx.role.uuid == role_uuid: if ctx.role.uuid == role_uuid:
raise ValueError("Cannot delete your own role") raise ValueError("Cannot delete your own role")
await db.delete_role(role_uuid) db.delete_role(role_uuid)
return {"status": "ok"} return {"status": "ok"}
@@ -354,7 +354,7 @@ async def admin_create_user(
raise ValueError("display_name and role are required") raise ValueError("display_name and role are required")
from ..db import User as UserDC from ..db import User as UserDC
roles = await db.get_roles_by_organization(str(org_uuid)) roles = db.get_roles_by_organization(str(org_uuid))
role_obj = next((r for r in roles if r.display_name == role_name), None) role_obj = next((r for r in roles if r.display_name == role_name), None)
if not role_obj: if not role_obj:
raise ValueError("Role not found in organization") raise ValueError("Role not found in organization")
@@ -366,7 +366,7 @@ async def admin_create_user(
visits=0, visits=0,
created_at=None, created_at=None,
) )
await db.create_user(user) db.create_user(user)
return {"uuid": str(user_uuid)} return {"uuid": str(user_uuid)}
@@ -388,12 +388,12 @@ async def admin_update_user_role(
if not new_role: if not new_role:
raise ValueError("role is required") raise ValueError("role is required")
try: try:
user_org, _current_role = await db.get_user_organization(user_uuid) user_org, _current_role = db.get_user_organization(user_uuid)
except ValueError: except ValueError:
raise ValueError("User not found") raise ValueError("User not found")
if user_org.uuid != org_uuid: if user_org.uuid != org_uuid:
raise ValueError("User does not belong to this organization") raise ValueError("User does not belong to this organization")
roles = await db.get_roles_by_organization(str(org_uuid)) roles = db.get_roles_by_organization(str(org_uuid))
if not any(r.display_name == new_role for r in roles): if not any(r.display_name == new_role for r in roles):
raise ValueError("Role not found in organization") raise ValueError("Role not found in organization")
@@ -410,7 +410,7 @@ async def admin_update_user_role(
"Cannot change your own role to one without admin permissions" "Cannot change your own role to one without admin permissions"
) )
await db.update_user_role_in_organization(user_uuid, new_role) db.update_user_role_in_organization(user_uuid, new_role)
return {"status": "ok"} return {"status": "ok"}
@@ -422,7 +422,7 @@ async def admin_create_user_registration_link(
auth=AUTH_COOKIE, auth=AUTH_COOKIE,
): ):
try: try:
user_org, _role_name = await db.get_user_organization(user_uuid) user_org, _role_name = db.get_user_organization(user_uuid)
except ValueError: except ValueError:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
if user_org.uuid != org_uuid: if user_org.uuid != org_uuid:
@@ -443,12 +443,12 @@ async def admin_create_user_registration_link(
) )
# Check if user has existing credentials # Check if user has existing credentials
credentials = await db.get_credentials_by_user_uuid(user_uuid) credentials = db.get_credentials_by_user_uuid(user_uuid)
token_type = "user registration" if not credentials else "account recovery" token_type = "user registration" if not credentials else "account recovery"
token = passphrase.generate() token = passphrase.generate()
expiry = reset_expires() expiry = reset_expires()
await db.create_reset_token( db.create_reset_token(
user_uuid=user_uuid, user_uuid=user_uuid,
key=tokens.reset_key(token), key=tokens.reset_key(token),
expiry=expiry, expiry=expiry,
@@ -473,7 +473,7 @@ async def admin_get_user_detail(
auth=AUTH_COOKIE, auth=AUTH_COOKIE,
): ):
try: try:
user_org, role_name = await db.get_user_organization(user_uuid) user_org, role_name = db.get_user_organization(user_uuid)
except ValueError: except ValueError:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
if user_org.uuid != org_uuid: if user_org.uuid != org_uuid:
@@ -491,13 +491,13 @@ async def admin_get_user_detail(
raise authz.AuthException( raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden" status_code=403, detail="Insufficient permissions", mode="forbidden"
) )
user = await db.get_user_by_uuid(user_uuid) user = db.get_user_by_uuid(user_uuid)
cred_ids = await db.get_credentials_by_user_uuid(user_uuid) cred_ids = db.get_credentials_by_user_uuid(user_uuid)
creds: list[dict] = [] creds: list[dict] = []
aaguids: set[str] = set() aaguids: set[str] = set()
for cid in cred_ids: for cid in cred_ids:
try: try:
c = await db.get_credential_by_id(cid) c = db.get_credential_by_id(cid)
except ValueError: # pragma: no cover - race condition handling except ValueError: # pragma: no cover - race condition handling
continue continue
aaguid_str = str(c.aaguid) aaguid_str = str(c.aaguid)
@@ -552,7 +552,7 @@ async def admin_get_user_detail(
# Get sessions for the user # Get sessions for the user
normalized_request_host = hostutil.normalize_host(request.headers.get("host")) normalized_request_host = hostutil.normalize_host(request.headers.get("host"))
session_records = await db.list_sessions_for_user(user_uuid) session_records = db.list_sessions_for_user(user_uuid)
current_session_key = session_key(auth) current_session_key = session_key(auth)
sessions_payload: list[dict] = [] sessions_payload: list[dict] = []
for entry in session_records: for entry in session_records:
@@ -623,7 +623,7 @@ async def admin_update_user_display_name(
auth=AUTH_COOKIE, auth=AUTH_COOKIE,
): ):
try: try:
user_org, _role_name = await db.get_user_organization(user_uuid) user_org, _role_name = db.get_user_organization(user_uuid)
except ValueError: except ValueError:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
if user_org.uuid != org_uuid: if user_org.uuid != org_uuid:
@@ -646,7 +646,7 @@ async def admin_update_user_display_name(
raise HTTPException(status_code=400, detail="display_name required") raise HTTPException(status_code=400, detail="display_name required")
if len(new_name) > 64: if len(new_name) > 64:
raise HTTPException(status_code=400, detail="display_name too long") raise HTTPException(status_code=400, detail="display_name too long")
await db.update_user_display_name(user_uuid, new_name) db.update_user_display_name(user_uuid, new_name)
return {"status": "ok"} return {"status": "ok"}
@@ -659,7 +659,7 @@ async def admin_delete_user_credential(
auth=AUTH_COOKIE, auth=AUTH_COOKIE,
): ):
try: try:
user_org, _role_name = await db.get_user_organization(user_uuid) user_org, _role_name = db.get_user_organization(user_uuid)
except ValueError: except ValueError:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
if user_org.uuid != org_uuid: if user_org.uuid != org_uuid:
@@ -678,7 +678,7 @@ async def admin_delete_user_credential(
raise authz.AuthException( raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden" status_code=403, detail="Insufficient permissions", mode="forbidden"
) )
await db.delete_credential(credential_uuid, user_uuid) db.delete_credential(credential_uuid, user_uuid)
return {"status": "ok"} return {"status": "ok"}
@@ -691,7 +691,7 @@ async def admin_delete_user_session(
auth=AUTH_COOKIE, auth=AUTH_COOKIE,
): ):
try: try:
user_org, _role_name = await db.get_user_organization(user_uuid) user_org, _role_name = db.get_user_organization(user_uuid)
except ValueError: except ValueError:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
if user_org.uuid != org_uuid: if user_org.uuid != org_uuid:
@@ -717,11 +717,11 @@ async def admin_delete_user_session(
status_code=400, detail="Invalid session identifier" status_code=400, detail="Invalid session identifier"
) from exc ) from exc
target_session = await db.get_session(target_key) target_session = db.get_session(target_key)
if not target_session or target_session.user_uuid != user_uuid: if not target_session or target_session.user_uuid != user_uuid:
raise HTTPException(status_code=404, detail="Session not found") raise HTTPException(status_code=404, detail="Session not found")
await db.delete_session(target_key) db.delete_session(target_key)
# Check if admin terminated their own session # Check if admin terminated their own session
current_terminated = target_key == session_key(auth) current_terminated = target_key == session_key(auth)
@@ -739,7 +739,7 @@ async def admin_list_permissions(request: Request, auth=AUTH_COOKIE):
match=permutil.has_any, match=permutil.has_any,
host=request.headers.get("host"), host=request.headers.get("host"),
) )
perms = await db.list_permissions() perms = db.list_permissions()
# Global admins see all permissions # Global admins see all permissions
if "auth:admin" in ctx.role.permissions: if "auth:admin" in ctx.role.permissions:
@@ -771,7 +771,7 @@ async def admin_create_permission(
if not perm_id or not display_name: if not perm_id or not display_name:
raise ValueError("id and display_name are required") raise ValueError("id and display_name are required")
querysafe.assert_safe(perm_id, field="id") querysafe.assert_safe(perm_id, field="id")
await db.create_permission(PermDC(id=perm_id, display_name=display_name)) db.create_permission(PermDC(id=perm_id, display_name=display_name))
return {"status": "ok"} return {"status": "ok"}
@@ -790,7 +790,7 @@ async def admin_update_permission(
if not display_name: if not display_name:
raise ValueError("display_name is required") raise ValueError("display_name is required")
querysafe.assert_safe(permission_id, field="permission_id") querysafe.assert_safe(permission_id, field="permission_id")
await db.update_permission( db.update_permission(
PermDC(id=permission_id, display_name=display_name) PermDC(id=permission_id, display_name=display_name)
) )
return {"status": "ok"} return {"status": "ok"}
@@ -818,10 +818,10 @@ async def admin_rename_permission(
querysafe.assert_safe(old_id, field="old_id") querysafe.assert_safe(old_id, field="old_id")
querysafe.assert_safe(new_id, field="new_id") querysafe.assert_safe(new_id, field="new_id")
if display_name is None: if display_name is None:
perm = await db.get_permission(old_id) perm = db.get_permission(old_id)
display_name = perm.display_name display_name = perm.display_name
# All current backends support rename_permission # All current backends support rename_permission
await db.rename_permission(old_id, new_id, display_name) db.rename_permission(old_id, new_id, display_name)
return {"status": "ok"} return {"status": "ok"}
@@ -844,5 +844,5 @@ async def admin_delete_permission(
if permission_id == "auth:admin": if permission_id == "auth:admin":
raise ValueError("Cannot delete the master admin permission") raise ValueError("Cannot delete the master admin permission")
await db.delete_permission(permission_id) db.delete_permission(permission_id)
return {"status": "ok"} return {"status": "ok"}
+2 -2
View File
@@ -227,7 +227,7 @@ async def api_token_info(token: str):
# Check if this is a reset token # Check if this is a reset token
try: try:
reset_token = await get_reset(token) reset_token = await get_reset(token)
user = await db.get_user_by_uuid(reset_token.user_uuid) user = db.get_user_by_uuid(reset_token.user_uuid)
return { return {
"type": "reset", "type": "reset",
"user_name": user.display_name, "user_name": user.display_name,
@@ -297,7 +297,7 @@ async def api_logout(request: Request, response: Response, auth=AUTH_COOKIE):
except ValueError: except ValueError:
return {"message": "Already logged out"} return {"message": "Already logged out"}
with suppress(Exception): with suppress(Exception):
await db.delete_session(session_key(auth)) db.delete_session(session_key(auth))
session.clear_session_cookie(response) session.clear_session_cookie(response)
return {"message": "Logged out successfully"} return {"message": "Logged out successfully"}
+3 -3
View File
@@ -324,7 +324,7 @@ async def websocket_remote_auth_permit(ws: WebSocket):
# Fetch and verify credential # Fetch and verify credential
try: try:
stored_cred = await db.get_credential_by_id( stored_cred = db.get_credential_by_id(
credential.raw_id credential.raw_id
) )
except ValueError: except ValueError:
@@ -338,7 +338,7 @@ async def websocket_remote_auth_permit(ws: WebSocket):
) )
# Update credential last_used # Update credential last_used
await db.login(stored_cred.user_uuid, stored_cred) db.login(stored_cred.user_uuid, stored_cred)
# Create a session for the REQUESTING device # Create a session for the REQUESTING device
assert stored_cred.uuid is not None assert stored_cred.uuid is not None
@@ -353,7 +353,7 @@ async def websocket_remote_auth_permit(ws: WebSocket):
token_str = passphrase.generate() token_str = passphrase.generate()
expiry = expires() expiry = expires()
await db.create_reset_token( db.create_reset_token(
user_uuid=stored_cred.user_uuid, user_uuid=stored_cred.user_uuid,
key=tokens.reset_key(token_str), key=tokens.reset_key(token_str),
expiry=expiry, expiry=expiry,
+5 -5
View File
@@ -55,7 +55,7 @@ async def user_update_display_name(
raise HTTPException(status_code=400, detail="display_name required") raise HTTPException(status_code=400, detail="display_name required")
if len(new_name) > 64: if len(new_name) > 64:
raise HTTPException(status_code=400, detail="display_name too long") raise HTTPException(status_code=400, detail="display_name too long")
await db.update_user_display_name(s.user_uuid, new_name) db.update_user_display_name(s.user_uuid, new_name)
return {"status": "ok"} return {"status": "ok"}
@@ -69,7 +69,7 @@ async def api_logout_all(request: Request, response: Response, auth=AUTH_COOKIE)
raise authz.AuthException( raise authz.AuthException(
status_code=401, detail="Session expired", mode="login" status_code=401, detail="Session expired", mode="login"
) )
await db.delete_sessions_for_user(s.user_uuid) db.delete_sessions_for_user(s.user_uuid)
session.clear_session_cookie(response) session.clear_session_cookie(response)
return {"message": "Logged out from all hosts"} return {"message": "Logged out from all hosts"}
@@ -99,11 +99,11 @@ async def api_delete_session(
status_code=400, detail="Invalid session identifier" status_code=400, detail="Invalid session identifier"
) from exc ) from exc
target_session = await db.get_session(target_key) target_session = db.get_session(target_key)
if not target_session or target_session.user_uuid != current_session.user_uuid: if not target_session or target_session.user_uuid != current_session.user_uuid:
raise HTTPException(status_code=404, detail="Session not found") raise HTTPException(status_code=404, detail="Session not found")
await db.delete_session(target_key) db.delete_session(target_key)
current_terminated = target_key == session_key(auth) current_terminated = target_key == session_key(auth)
if current_terminated: if current_terminated:
session.clear_session_cookie(response) # explicit because 200 session.clear_session_cookie(response) # explicit because 200
@@ -144,7 +144,7 @@ async def api_create_link(
) from e ) from e
token = passphrase.generate() token = passphrase.generate()
expiry = expires() expiry = expires()
await db.create_reset_token( db.create_reset_token(
user_uuid=s.user_uuid, user_uuid=s.user_uuid,
key=tokens.reset_key(token), key=tokens.reset_key(token),
expiry=expiry, expiry=expiry,
+6 -6
View File
@@ -66,13 +66,13 @@ async def websocket_register_add(
s = ctx.session s = ctx.session
# Get user information and determine effective user_name for this registration # Get user information and determine effective user_name for this registration
user = await db.get_user_by_uuid(user_uuid) user = db.get_user_by_uuid(user_uuid)
user_name = user.display_name user_name = user.display_name
if name is not None: if name is not None:
stripped = name.strip() stripped = name.strip()
if stripped: if stripped:
user_name = stripped user_name = stripped
challenge_ids = await db.get_credentials_by_user_uuid(user_uuid) challenge_ids = db.get_credentials_by_user_uuid(user_uuid)
# WebAuthn registration # WebAuthn registration
credential = await register_chat(ws, user_uuid, user_name, origin, challenge_ids) credential = await register_chat(ws, user_uuid, user_name, origin, challenge_ids)
@@ -80,7 +80,7 @@ async def websocket_register_add(
# Create a new session and store everything in database # Create a new session and store everything in database
token = create_token() token = create_token()
metadata = infodict(ws, "authenticated") metadata = infodict(ws, "authenticated")
await db.create_credential_session( # type: ignore[attr-defined] db.create_credential_session( # type: ignore[attr-defined]
user_uuid=user_uuid, user_uuid=user_uuid,
credential=credential, credential=credential,
reset_key=(s.key if reset is not None else None), reset_key=(s.key if reset is not None else None),
@@ -116,7 +116,7 @@ async def websocket_authenticate(ws: WebSocket, auth=AUTH_COOKIE):
try: try:
session = await get_session(auth, host=host) session = await get_session(auth, host=host)
session_user_uuid = session.user_uuid session_user_uuid = session.user_uuid
credential_ids = await db.get_credentials_by_user_uuid( credential_ids = db.get_credentials_by_user_uuid(
session_user_uuid session_user_uuid
) )
except ValueError: except ValueError:
@@ -130,7 +130,7 @@ async def websocket_authenticate(ws: WebSocket, auth=AUTH_COOKIE):
credential = passkey.instance.auth_parse(await ws.receive_json()) credential = passkey.instance.auth_parse(await ws.receive_json())
# Fetch from the database by credential ID # Fetch from the database by credential ID
try: try:
stored_cred = await db.get_credential_by_id(credential.raw_id) stored_cred = db.get_credential_by_id(credential.raw_id)
except ValueError: except ValueError:
raise ValueError( raise ValueError(
f"This passkey is no longer registered with {passkey.instance.rp_name}" f"This passkey is no longer registered with {passkey.instance.rp_name}"
@@ -143,7 +143,7 @@ async def websocket_authenticate(ws: WebSocket, auth=AUTH_COOKIE):
# Verify the credential matches the stored data # Verify the credential matches the stored data
passkey.instance.auth_verify(credential, challenge, stored_cred, origin) passkey.instance.auth_verify(credential, challenge, stored_cred, origin)
# Update both credential and user's last_seen timestamp # Update both credential and user's last_seen timestamp
await db.login(stored_cred.user_uuid, stored_cred) db.login(stored_cred.user_uuid, stored_cred)
# Create a session token for the authenticated user # Create a session token for the authenticated user
assert stored_cred.uuid is not None assert stored_cred.uuid is not None
+1 -1
View File
@@ -29,4 +29,4 @@ async def session_context(auth: str | None, host: str | None = None):
if not auth: if not auth:
return None return None
normalized_host = normalize_host(host) if host else None normalized_host = normalize_host(host) if host else None
return await db.get_session_context(session_key(auth), normalized_host) return db.get_session_context(session_key(auth), normalized_host)
+5 -5
View File
@@ -41,17 +41,17 @@ async def format_user_info(
- Sessions list - Sessions list
- Permissions - Permissions
""" """
u = await db.get_user_by_uuid(user_uuid) u = db.get_user_by_uuid(user_uuid)
ctx = await permutil.session_context(auth, request_host) ctx = await permutil.session_context(auth, request_host)
# Fetch and format credentials # Fetch and format credentials
credential_ids = await db.get_credentials_by_user_uuid(user_uuid) credential_ids = db.get_credentials_by_user_uuid(user_uuid)
credentials: list[dict] = [] credentials: list[dict] = []
user_aaguids: set[str] = set() user_aaguids: set[str] = set()
for cred_id in credential_ids: for cred_id in credential_ids:
try: try:
c = await db.get_credential_by_id(cred_id) c = db.get_credential_by_id(cred_id)
except ValueError: except ValueError:
continue continue
@@ -98,7 +98,7 @@ async def format_user_info(
# Format sessions # Format sessions
normalized_request_host = hostutil.normalize_host(request_host) normalized_request_host = hostutil.normalize_host(request_host)
session_records = await db.list_sessions_for_user(user_uuid) session_records = db.list_sessions_for_user(user_uuid)
current_session_key = session_key(auth) current_session_key = session_key(auth)
sessions_payload: list[dict] = [] sessions_payload: list[dict] = []
@@ -150,7 +150,7 @@ async def format_reset_user_info(user_uuid, reset_token) -> dict:
Returns: Returns:
Dictionary with minimal user info for password reset flow Dictionary with minimal user info for password reset flow
""" """
u = await db.get_user_by_uuid(user_uuid) u = db.get_user_by_uuid(user_uuid)
return { return {
"authenticated": False, "authenticated": False,
+15 -14
View File
@@ -21,6 +21,7 @@ import pytest
import pytest_asyncio import pytest_asyncio
import uuid7 import uuid7
from paskia import globals as paskia_globals
from paskia.db import Credential, Org, Permission, Role, User from paskia.db import Credential, Org, Permission, Role, User
from paskia.db.json import DB from paskia.db.json import DB
from paskia.fastapi.session import AUTH_COOKIE_NAME from paskia.fastapi.session import AUTH_COOKIE_NAME
@@ -46,7 +47,7 @@ async def test_db() -> AsyncGenerator[DB, None]:
with tempfile.NamedTemporaryFile(suffix=".jsonl", delete=True) as f: with tempfile.NamedTemporaryFile(suffix=".jsonl", delete=True) as f:
db = DB(f.name) db = DB(f.name)
await db.init_db() db.load() # Synchronous now
json_db._db = db json_db._db = db
yield db yield db
# Clean up # Clean up
@@ -61,9 +62,9 @@ async def passkey_instance() -> Passkey:
rp_name="Test RP", rp_name="Test RP",
origins=["http://localhost:4401"], origins=["http://localhost:4401"],
) )
globals.passkey._instance = pk paskia_globals.passkey._instance = pk
yield pk yield pk
globals.passkey._instance = None paskia_globals.passkey._instance = None
@pytest_asyncio.fixture(scope="function") @pytest_asyncio.fixture(scope="function")
@@ -74,7 +75,7 @@ async def test_org(test_db: DB, admin_permission: Permission) -> Org:
display_name="Test Organization", display_name="Test Organization",
permissions=["auth:admin"], # Org can grant this permission permissions=["auth:admin"], # Org can grant this permission
) )
await test_db.create_organization(org) test_db.create_organization(org)
return org return org
@@ -82,7 +83,7 @@ async def test_org(test_db: DB, admin_permission: Permission) -> Org:
async def admin_permission(test_db: DB) -> Permission: async def admin_permission(test_db: DB) -> Permission:
"""Create the auth:admin permission.""" """Create the auth:admin permission."""
perm = Permission(id="auth:admin", display_name="Master Admin") perm = Permission(id="auth:admin", display_name="Master Admin")
await test_db.create_permission(perm) test_db.create_permission(perm)
return perm return perm
@@ -95,7 +96,7 @@ async def test_role(test_db: DB, test_org: Org, admin_permission: Permission) ->
display_name="Test Admin Role", display_name="Test Admin Role",
permissions=["auth:admin", f"auth:org:{test_org.uuid}"], permissions=["auth:admin", f"auth:org:{test_org.uuid}"],
) )
await test_db.create_role(role) test_db.create_role(role)
return role return role
@@ -108,7 +109,7 @@ async def user_role(test_db: DB, test_org: Org) -> Role:
display_name="User Role", display_name="User Role",
permissions=[], permissions=[],
) )
await test_db.create_role(role) test_db.create_role(role)
return role return role
@@ -122,7 +123,7 @@ async def test_user(test_db: DB, test_role: Role) -> User:
created_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc),
visits=0, visits=0,
) )
await test_db.create_user(user) test_db.create_user(user)
return user return user
@@ -136,7 +137,7 @@ async def regular_user(test_db: DB, user_role: Role) -> User:
created_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc),
visits=0, visits=0,
) )
await test_db.create_user(user) test_db.create_user(user)
return user return user
@@ -154,7 +155,7 @@ async def test_credential(test_db: DB, test_user: User) -> Credential:
last_used=None, last_used=None,
last_verified=None, last_verified=None,
) )
await test_db.create_credential(credential) test_db.create_credential(credential)
return credential return credential
@@ -172,7 +173,7 @@ async def regular_credential(test_db: DB, regular_user: User) -> Credential:
last_used=None, last_used=None,
last_verified=None, last_verified=None,
) )
await test_db.create_credential(credential) test_db.create_credential(credential)
return credential return credential
@@ -182,7 +183,7 @@ async def session_token(
) -> str: ) -> str:
"""Create a session for the admin user and return the token.""" """Create a session for the admin user and return the token."""
token = create_token() token = create_token()
await test_db.create_session( test_db.create_session(
user_uuid=test_user.uuid, user_uuid=test_user.uuid,
credential_uuid=test_credential.uuid, credential_uuid=test_credential.uuid,
key=session_key(token), key=session_key(token),
@@ -200,7 +201,7 @@ async def regular_session_token(
) -> str: ) -> str:
"""Create a session for a regular user and return the token.""" """Create a session for a regular user and return the token."""
token = create_token() token = create_token()
await test_db.create_session( test_db.create_session(
user_uuid=regular_user.uuid, user_uuid=regular_user.uuid,
credential_uuid=regular_credential.uuid, credential_uuid=regular_credential.uuid,
key=session_key(token), key=session_key(token),
@@ -220,7 +221,7 @@ async def reset_token(test_db: DB, test_user: User, test_credential: Credential)
from paskia.util.tokens import reset_key from paskia.util.tokens import reset_key
token = generate() token = generate()
await test_db.create_reset_token( test_db.create_reset_token(
user_uuid=test_user.uuid, user_uuid=test_user.uuid,
key=reset_key(token), key=reset_key(token),
expiry=reset_expires(), expiry=reset_expires(),
+28 -26
View File
@@ -35,7 +35,7 @@ async def second_org(test_db: DB) -> Org:
display_name="Second Organization", display_name="Second Organization",
permissions=[], permissions=[],
) )
await test_db.create_organization(org) test_db.create_organization(org)
return org return org
@@ -50,7 +50,7 @@ async def second_org_role(
display_name="Second Org Admin Role", display_name="Second Org Admin Role",
permissions=["auth:admin"], permissions=["auth:admin"],
) )
await test_db.create_role(role) test_db.create_role(role)
return role return role
@@ -64,7 +64,7 @@ async def second_org_user(test_db: DB, second_org_role: Role) -> User:
created_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc),
visits=0, visits=0,
) )
await test_db.create_user(user) test_db.create_user(user)
return user return user
@@ -84,7 +84,7 @@ async def second_org_credential(test_db: DB, second_org_user: User) -> Credentia
last_used=datetime.now(timezone.utc), last_used=datetime.now(timezone.utc),
last_verified=datetime.now(timezone.utc), last_verified=datetime.now(timezone.utc),
) )
await test_db.create_credential(credential) test_db.create_credential(credential)
return credential return credential
@@ -94,7 +94,7 @@ async def second_org_session_token(
) -> str: ) -> str:
"""Create a session for the second org admin user.""" """Create a session for the second org admin user."""
token = create_token() token = create_token()
await test_db.create_session( test_db.create_session(
user_uuid=second_org_user.uuid, user_uuid=second_org_user.uuid,
credential_uuid=second_org_credential.uuid, credential_uuid=second_org_credential.uuid,
key=session_key(token), key=session_key(token),
@@ -115,7 +115,7 @@ async def org_admin_role(test_db: DB, test_org: Org) -> Role:
display_name="Org Admin Role", display_name="Org Admin Role",
permissions=[f"auth:org:{test_org.uuid}"], permissions=[f"auth:org:{test_org.uuid}"],
) )
await test_db.create_role(role) test_db.create_role(role)
return role return role
@@ -130,7 +130,7 @@ async def org_admin_user(test_db: DB, org_admin_role: Role) -> User:
visits=5, visits=5,
last_seen=datetime.now(timezone.utc), last_seen=datetime.now(timezone.utc),
) )
await test_db.create_user(user) test_db.create_user(user)
return user return user
@@ -150,7 +150,7 @@ async def org_admin_credential(test_db: DB, org_admin_user: User) -> Credential:
last_used=datetime.now(timezone.utc), last_used=datetime.now(timezone.utc),
last_verified=None, last_verified=None,
) )
await test_db.create_credential(credential) test_db.create_credential(credential)
return credential return credential
@@ -160,7 +160,7 @@ async def org_admin_session_token(
) -> str: ) -> str:
"""Create a session for the org admin user.""" """Create a session for the org admin user."""
token = create_token() token = create_token()
await test_db.create_session( test_db.create_session(
user_uuid=org_admin_user.uuid, user_uuid=org_admin_user.uuid,
credential_uuid=org_admin_credential.uuid, credential_uuid=org_admin_credential.uuid,
key=session_key(token), key=session_key(token),
@@ -176,9 +176,9 @@ async def org_admin_session_token(
async def grantable_permission(test_db: DB, test_org: Org) -> Permission: async def grantable_permission(test_db: DB, test_org: Org) -> Permission:
"""Create a permission and add it to org's grantable permissions.""" """Create a permission and add it to org's grantable permissions."""
perm = Permission(id="test:grantable:perm", display_name="Grantable Perm") perm = Permission(id="test:grantable:perm", display_name="Grantable Perm")
await test_db.create_permission(perm) test_db.create_permission(perm)
# Add to org's grantable permissions # Add to org's grantable permissions
await test_db.add_permission_to_organization(str(test_org.uuid), perm.id) test_db.add_permission_to_organization(str(test_org.uuid), perm.id)
return perm return perm
@@ -375,12 +375,12 @@ class TestAdminOrganizations:
org_admin_perm_id = f"auth:org:{test_org.uuid}" org_admin_perm_id = f"auth:org:{test_org.uuid}"
perm = Permission(id=org_admin_perm_id, display_name="Org Admin") perm = Permission(id=org_admin_perm_id, display_name="Org Admin")
try: try:
await test_db.create_permission(perm) test_db.create_permission(perm)
except Exception: except Exception:
pass # Permission may already exist pass # Permission may already exist
# Add it to the org's permissions # Add it to the org's permissions
await test_db.add_permission_to_organization( test_db.add_permission_to_organization(
str(test_org.uuid), org_admin_perm_id str(test_org.uuid), org_admin_perm_id
) )
@@ -424,13 +424,13 @@ class TestAdminOrganizations:
display_name="Org To Delete", display_name="Org To Delete",
permissions=[], permissions=[],
) )
await test_db.create_organization(org_to_delete) test_db.create_organization(org_to_delete)
# Create some org-specific permissions to test cleanup # Create some org-specific permissions to test cleanup
org_perm = Permission( org_perm = Permission(
id=f"test:org:{org_to_delete.uuid}:feature", display_name="Org Feature" id=f"test:org:{org_to_delete.uuid}:feature", display_name="Org Feature"
) )
await test_db.create_permission(org_perm) test_db.create_permission(org_perm)
response = await client.delete( response = await client.delete(
f"/auth/api/admin/orgs/{org_to_delete.uuid}", f"/auth/api/admin/orgs/{org_to_delete.uuid}",
@@ -603,7 +603,7 @@ class TestAdminRoles:
"""Creating role with non-grantable permission should fail.""" """Creating role with non-grantable permission should fail."""
# Create permission but don't add to org # Create permission but don't add to org
perm = Permission(id="test:not:grantable", display_name="Not Grantable") perm = Permission(id="test:not:grantable", display_name="Not Grantable")
await test_db.create_permission(perm) test_db.create_permission(perm)
response = await client.post( response = await client.post(
f"/auth/api/admin/orgs/{test_org.uuid}/roles", f"/auth/api/admin/orgs/{test_org.uuid}/roles",
@@ -673,7 +673,7 @@ class TestAdminRoles:
): ):
"""Adding non-grantable permission to role should fail.""" """Adding non-grantable permission to role should fail."""
perm = Permission(id="test:not:grantable:update", display_name="Not Grantable") perm = Permission(id="test:not:grantable:update", display_name="Not Grantable")
await test_db.create_permission(perm) test_db.create_permission(perm)
response = await client.put( response = await client.put(
f"/auth/api/admin/orgs/{test_org.uuid}/roles/{user_role.uuid}", f"/auth/api/admin/orgs/{test_org.uuid}/roles/{user_role.uuid}",
@@ -1087,7 +1087,7 @@ class TestAdminUsersInOrg:
created_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc),
visits=0, visits=0,
) )
await test_db.create_user(user_no_cred) test_db.create_user(user_no_cred)
response = await client.post( response = await client.post(
f"/auth/api/admin/orgs/{test_org.uuid}/users/{user_no_cred.uuid}/create-link", f"/auth/api/admin/orgs/{test_org.uuid}/users/{user_no_cred.uuid}/create-link",
@@ -1174,7 +1174,7 @@ class TestAdminSessions:
# Create an additional session to delete # Create an additional session to delete
extra_token = create_token() extra_token = create_token()
extra_key = session_key(extra_token) extra_key = session_key(extra_token)
await test_db.create_session( test_db.create_session(
user_uuid=test_user.uuid, user_uuid=test_user.uuid,
credential_uuid=test_credential.uuid, credential_uuid=test_credential.uuid,
key=extra_key, key=extra_key,
@@ -1301,7 +1301,7 @@ class TestAdminPermissions:
test_org, test_org,
grantable_permission, grantable_permission,
): ):
"""Org admin should only see grantable permissions.""" """Org admin should only see permissions their org can grant."""
response = await client.get( response = await client.get(
"/auth/api/admin/permissions", "/auth/api/admin/permissions",
headers={**auth_headers(org_admin_session_token), "Host": "localhost:4401"}, headers={**auth_headers(org_admin_session_token), "Host": "localhost:4401"},
@@ -1311,8 +1311,10 @@ class TestAdminPermissions:
# Should only see permissions the org can grant # Should only see permissions the org can grant
perm_ids = [p["id"] for p in data] perm_ids = [p["id"] for p in data]
assert grantable_permission.id in perm_ids assert grantable_permission.id in perm_ids
# Should NOT see auth:admin (not grantable by org) # test_org CAN grant auth:admin (it's in org.permissions), so org admin sees it
assert "auth:admin" not in perm_ids assert "auth:admin" in perm_ids
# Should also see auto-created org admin permission
assert f"auth:org:{test_org.uuid}" in perm_ids
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_permission( async def test_create_permission(
@@ -1364,7 +1366,7 @@ class TestAdminPermissions:
"""Admin should be able to update a permission.""" """Admin should be able to update a permission."""
# Create permission first # Create permission first
perm = Permission(id="test:updateable", display_name="Updateable") perm = Permission(id="test:updateable", display_name="Updateable")
await test_db.create_permission(perm) test_db.create_permission(perm)
response = await client.put( response = await client.put(
"/auth/api/admin/permission?permission_id=test:updateable&display_name=Updated%20Name", "/auth/api/admin/permission?permission_id=test:updateable&display_name=Updated%20Name",
@@ -1394,7 +1396,7 @@ class TestAdminPermissions:
"""Admin should be able to rename a permission.""" """Admin should be able to rename a permission."""
# Create permission first # Create permission first
perm = Permission(id="test:renameable2", display_name="Renameable") perm = Permission(id="test:renameable2", display_name="Renameable")
await test_db.create_permission(perm) test_db.create_permission(perm)
response = await client.post( response = await client.post(
"/auth/api/admin/permission/rename", "/auth/api/admin/permission/rename",
@@ -1437,7 +1439,7 @@ class TestAdminPermissions:
): ):
"""Renaming permission can also update display name.""" """Renaming permission can also update display name."""
perm = Permission(id="test:rename:withname", display_name="Old Name") perm = Permission(id="test:rename:withname", display_name="Old Name")
await test_db.create_permission(perm) test_db.create_permission(perm)
response = await client.post( response = await client.post(
"/auth/api/admin/permission/rename", "/auth/api/admin/permission/rename",
@@ -1457,7 +1459,7 @@ class TestAdminPermissions:
"""Admin should be able to delete a permission.""" """Admin should be able to delete a permission."""
# Create permission first # Create permission first
perm = Permission(id="test:deleteable", display_name="Deleteable") perm = Permission(id="test:deleteable", display_name="Deleteable")
await test_db.create_permission(perm) test_db.create_permission(perm)
response = await client.delete( response = await client.delete(
"/auth/api/admin/permission?permission_id=test:deleteable", "/auth/api/admin/permission?permission_id=test:deleteable",
+2 -2
View File
@@ -525,7 +525,7 @@ class TestValidateSessionRefresh:
# Create a session with an old renewed time to trigger refresh # Create a session with an old renewed time to trigger refresh
token = create_token() token = create_token()
old_time = datetime.now(timezone.utc) - timedelta(minutes=10) old_time = datetime.now(timezone.utc) - timedelta(minutes=10)
await test_db.create_session( test_db.create_session(
user_uuid=test_user.uuid, user_uuid=test_user.uuid,
credential_uuid=test_credential.uuid, credential_uuid=test_credential.uuid,
key=session_key(token), key=session_key(token),
@@ -536,7 +536,7 @@ class TestValidateSessionRefresh:
) )
# Delete the session right before validate tries to refresh # Delete the session right before validate tries to refresh
await test_db.delete_session(session_key(token)) test_db.delete_session(session_key(token))
response = await client.post( response = await client.post(
"/auth/api/validate", "/auth/api/validate",