Database cleanup continues, build functions replaced by post init. Simplified some APIs.

This commit is contained in:
2026-01-27 16:53:13 +00:00
parent dbdd1dbd3c
commit f0d1b86d6b
4 changed files with 48 additions and 105 deletions
+3 -14
View File
@@ -30,29 +30,18 @@ def _log_reset_link(message: str, passphrase: str) -> str:
return reset_link return reset_link
async def bootstrap_system() -> dict: async def bootstrap_system() -> None:
""" """
Bootstrap the entire system with default data. Bootstrap the entire system with default data.
Uses db.bootstrap() which performs all operations in a single transaction. Uses db.bootstrap() which performs all operations in a single transaction.
The transaction log will show a single "bootstrap" action with all changes. The transaction log will show a single "bootstrap" action with all changes.
Returns:
dict: Contains information about created entities and reset link
""" """
# Call the single-transaction bootstrap function # Call the single-transaction bootstrap function
result = db.bootstrap() reset_passphrase = db.bootstrap()
# Log the reset link (this is separate from the transaction log) # Log the reset link (this is separate from the transaction log)
reset_link = _log_reset_link("✅ Bootstrap completed!", result["reset_passphrase"]) _log_reset_link("✅ Bootstrap completed!", reset_passphrase)
return {
"user": result["user"],
"org": result["org"],
"role": result["role"],
"permissions": [result["perm_admin"], result["perm_org_admin"]],
"reset_link": reset_link,
}
async def check_admin_credentials() -> bool: async def check_admin_credentials() -> bool:
-6
View File
@@ -31,13 +31,7 @@ from paskia.db.operations import (
add_permission_to_organization, add_permission_to_organization,
add_permission_to_role, add_permission_to_role,
bootstrap, bootstrap,
build_credential,
build_org, build_org,
build_permission,
build_reset_token,
build_role,
build_session,
build_user,
cleanup_expired, cleanup_expired,
create_credential, create_credential,
create_credential_session, create_credential_session,
+28 -85
View File
@@ -163,53 +163,16 @@ async def init(*args, **kwargs):
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
def build_permission(uuid: UUID) -> Permission:
perm = _db._data.permissions[uuid]
perm.uuid = uuid
return perm
def build_user(uuid: UUID) -> User:
user = _db._data.users[uuid]
user.uuid = uuid
return user
def build_role(uuid: UUID) -> Role:
r = _db._data.roles[uuid]
r.uuid = uuid
return r
def build_org(uuid: UUID, include_roles: bool = False) -> Org: def build_org(uuid: UUID, include_roles: bool = False) -> Org:
o = _db._data.orgs[uuid] o = _db._data.orgs[uuid]
o.uuid = uuid
o.permissions = {pid for pid, p in _db._data.permissions.items() if uuid in p.orgs} o.permissions = {pid for pid, p in _db._data.permissions.items() if uuid in p.orgs}
if include_roles: if include_roles:
o.roles = [ o.roles = [
build_role(rid) for rid, r in _db._data.roles.items() if r.org == uuid _db._data.roles[rid] for rid, r in _db._data.roles.items() if r.org == uuid
] ]
return o return o
def build_credential(uuid: UUID) -> Credential:
cred = _db._data.credentials[uuid]
cred.uuid = uuid
return cred
def build_session(key: str) -> Session:
s = _db._data.sessions[key]
s.key = key
return s
def build_reset_token(key: bytes) -> ResetToken:
t = _db._data.reset_tokens[key]
t.key = key
return t
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Read/lookup functions # Read/lookup functions
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -228,9 +191,7 @@ def get_permission(uuid: UUID) -> Permission | None:
- Get permission for renaming its scope (admin.py:1031) - Get permission for renaming its scope (admin.py:1031)
- Get permission to check scope before deleting (admin.py:1071) - Get permission to check scope before deleting (admin.py:1071)
""" """
if uuid in _db._data.permissions: return _db._data.permissions.get(uuid)
return build_permission(uuid)
return None
def get_permission_by_scope(scope: str) -> Permission | None: def get_permission_by_scope(scope: str) -> Permission | None:
@@ -239,9 +200,9 @@ def get_permission_by_scope(scope: str) -> Permission | None:
Call sites: Call sites:
- Check if system is already bootstrapped by looking for auth:admin permission (bootstrap.py:113) - Check if system is already bootstrapped by looking for auth:admin permission (bootstrap.py:113)
""" """
for uuid, p in _db._data.permissions.items(): for p in _db._data.permissions.values():
if p.scope == scope: if p.scope == scope:
return build_permission(uuid) return p
return None return None
@@ -251,11 +212,7 @@ def get_permissions_by_scope(scope: str) -> list[Permission]:
Since scopes are not unique, this returns all matching permissions. Since scopes are not unique, this returns all matching permissions.
Use this for scope-based permission checking. Use this for scope-based permission checking.
""" """
return [ return [p for p in _db._data.permissions.values() if p.scope == scope]
build_permission(uuid)
for uuid, p in _db._data.permissions.items()
if p.scope == scope
]
def list_permissions() -> list[Permission]: def list_permissions() -> list[Permission]:
@@ -268,7 +225,7 @@ def list_permissions() -> list[Permission]:
- List permissions to check admin permissions when deleting permission (admin.py:882) - List permissions to check admin permissions when deleting permission (admin.py:882)
- Admin API endpoint to list permissions (admin.py:914) - Admin API endpoint to list permissions (admin.py:914)
""" """
return [build_permission(uuid) for uuid in _db._data.permissions] return list(_db._data.permissions.values())
def get_permission_organizations(scope: str) -> list[Org]: def get_permission_organizations(scope: str) -> list[Org]:
@@ -316,8 +273,8 @@ def get_organization_users(org_uuid: UUID) -> list[tuple[User, str]]:
rid: r.display_name for rid, r in _db._data.roles.items() if r.org == org_uuid rid: r.display_name for rid, r in _db._data.roles.items() if r.org == org_uuid
} }
return [ return [
(build_user(uid), role_map[u.role]) (u, role_map[u.role])
for uid, u in _db._data.users.items() for u in _db._data.users.values()
if u.role in role_map if u.role in role_map
] ]
@@ -331,7 +288,7 @@ def get_role(uuid: UUID) -> Role | None:
- Get role to remove permission from it (admin.py:380) - Get role to remove permission from it (admin.py:380)
- Get role to delete it (admin.py:421) - Get role to delete it (admin.py:421)
""" """
return build_role(uuid) if uuid in _db._data.roles else None return _db._data.roles.get(uuid)
def get_roles_by_organization(org_uuid: UUID) -> list[Role]: def get_roles_by_organization(org_uuid: UUID) -> list[Role]:
@@ -341,7 +298,7 @@ def get_roles_by_organization(org_uuid: UUID) -> list[Role]:
- Get roles by organization when creating a user to find the role by name (admin.py:459) - Get roles by organization when creating a user to find the role by name (admin.py:459)
- Get roles by organization when updating user role to validate the new role name (admin.py:498) - Get roles by organization when updating user role to validate the new role name (admin.py:498)
""" """
return [build_role(rid) for rid, r in _db._data.roles.items() if r.org == org_uuid] return [r for r in _db._data.roles.values() if r.org == org_uuid]
def get_user_by_uuid(uuid: UUID) -> User | None: def get_user_by_uuid(uuid: UUID) -> User | None:
@@ -352,7 +309,7 @@ def get_user_by_uuid(uuid: UUID) -> User | None:
- Get user from reset token for registration info (api.py:127) - Get user from reset token for registration info (api.py:127)
- Get user for listing user credentials in admin API (admin.py:594) - Get user for listing user credentials in admin API (admin.py:594)
""" """
return build_user(uuid) if uuid in _db._data.users else None return _db._data.users.get(uuid)
def get_user_organization(user_uuid: UUID) -> tuple[Org, str]: def get_user_organization(user_uuid: UUID) -> tuple[Org, str]:
@@ -385,9 +342,9 @@ def get_credential_by_id(credential_id: bytes) -> Credential | None:
- Get credential by ID for WebAuthn authentication (ws.py:132) - Get credential by ID for WebAuthn authentication (ws.py:132)
- Get credential by ID for remote authentication (remote.py:325) - Get credential by ID for remote authentication (remote.py:325)
""" """
for uuid, c in _db._data.credentials.items(): for c in _db._data.credentials.values():
if c.credential_id == credential_id: if c.credential_id == credential_id:
return build_credential(uuid) return c
return None return None
@@ -402,11 +359,7 @@ def get_credentials_by_user_uuid(user_uuid: UUID) -> list[Credential]:
- Get credentials to check if admin user has credentials (bootstrap.py:81) - Get credentials to check if admin user has credentials (bootstrap.py:81)
- Get credentials for user info formatting (userinfo.py:51) - Get credentials for user info formatting (userinfo.py:51)
""" """
return [ return [c for c in _db._data.credentials.values() if c.user == user_uuid]
build_credential(cid)
for cid, c in _db._data.credentials.items()
if c.user == user_uuid
]
def get_session(key: str) -> Session | None: def get_session(key: str) -> Session | None:
@@ -423,7 +376,7 @@ def get_session(key: str) -> Session | None:
s = _db._data.sessions[key] s = _db._data.sessions[key]
if s.expiry < datetime.now(timezone.utc): if s.expiry < datetime.now(timezone.utc):
return None return None
return build_session(key) return s
def list_sessions_for_user(user_uuid: UUID) -> list[Session]: def list_sessions_for_user(user_uuid: UUID) -> list[Session]:
@@ -434,11 +387,7 @@ def list_sessions_for_user(user_uuid: UUID) -> list[Session]:
- List sessions for user details API (admin.py:651) - List sessions for user details API (admin.py:651)
""" """
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
return [ return [s for s in _db._data.sessions.values() if s.user == user_uuid and s.expiry >= now]
build_session(key)
for key, s in _db._data.sessions.items()
if s.user == user_uuid and s.expiry >= now
]
def _reset_key(passphrase: str) -> bytes: def _reset_key(passphrase: str) -> bytes:
@@ -464,7 +413,7 @@ def get_reset_token(passphrase: str) -> ResetToken | None:
t = _db._data.reset_tokens[key] t = _db._data.reset_tokens[key]
if t.expiry < datetime.now(timezone.utc): if t.expiry < datetime.now(timezone.utc):
return None return None
return build_reset_token(key) return t
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -521,15 +470,15 @@ def get_session_context(
if org_uuid not in _db._data.orgs: if org_uuid not in _db._data.orgs:
return None return None
session = build_session(session_key) session = _db._data.sessions[session_key]
user = build_user(s.user) user = _db._data.users[s.user]
role = build_role(role_uuid) role = _db._data.roles[role_uuid]
org = build_org(org_uuid) org = build_org(org_uuid)
# Credential must exist (sessions are cascade-deleted when credential is deleted) # Credential must exist (sessions are cascade-deleted when credential is deleted)
if s.credential not in _db._data.credentials: if s.credential not in _db._data.credentials:
return None return None
credential = build_credential(s.credential) credential = _db._data.credentials[s.credential]
# Effective permissions: role's permissions that the org can grant # Effective permissions: role's permissions that the org can grant
# Also filter by domain if host is provided # Also filter by domain if host is provided
@@ -547,7 +496,7 @@ def get_session_context(
# Check domain restriction # Check domain restriction
if p.domain is not None and p.domain != host_without_port: if p.domain is not None and p.domain != host_without_port:
continue continue
effective_perms.append(build_permission(perm_uuid)) effective_perms.append(_db._data.permissions[perm_uuid])
return SessionContext( return SessionContext(
session=session, session=session,
@@ -624,9 +573,11 @@ def create_organization(org: Org, *, ctx: SessionContext | None = None) -> None:
if org.uuid in _db._data.orgs: if org.uuid in _db._data.orgs:
raise ValueError(f"Organization {org.uuid} already exists") raise ValueError(f"Organization {org.uuid} already exists")
with _db.transaction("Created organization", ctx): with _db.transaction("Created organization", ctx):
_db._data.orgs[org.uuid] = Org( new_org = Org(
display_name=org.display_name, created_at=datetime.now(timezone.utc) display_name=org.display_name, created_at=datetime.now(timezone.utc)
) )
_db._data.orgs[org.uuid] = new_org
new_org.uuid = org.uuid
# Create Administration role with org admin permission # Create Administration role with org admin permission
import uuid7 import uuid7
@@ -1182,7 +1133,7 @@ def bootstrap(
admin_name: str = "Admin", admin_name: str = "Admin",
reset_passphrase: str | None = None, reset_passphrase: str | None = None,
reset_expiry: datetime | None = None, reset_expiry: datetime | None = None,
) -> dict: ) -> str:
"""Bootstrap the entire system in a single transaction. """Bootstrap the entire system in a single transaction.
Creates: Creates:
@@ -1202,7 +1153,7 @@ def bootstrap(
reset_expiry: Expiry datetime for the reset token (default: 14 days) reset_expiry: Expiry datetime for the reset token (default: 14 days)
Returns: Returns:
dict with keys: perm_admin, perm_org_admin, org, role, user, reset_passphrase The reset passphrase for admin registration.
""" """
import uuid7 import uuid7
@@ -1286,12 +1237,4 @@ def bootstrap(
token_type="admin bootstrap", token_type="admin bootstrap",
) )
# Return info about what was created (for logging by caller) return reset_passphrase
return {
"perm_admin": build_permission(perm_admin_uuid),
"perm_org_admin": build_permission(perm_org_admin_uuid),
"org": build_org(org_uuid),
"role": build_role(role_uuid),
"user": build_user(user_uuid),
"reset_passphrase": reset_passphrase,
}
+17
View File
@@ -208,3 +208,20 @@ class DatabaseData(msgspec.Struct, omit_defaults=True):
sessions: dict[str, Session] sessions: dict[str, Session]
reset_tokens: dict[bytes, ResetToken] reset_tokens: dict[bytes, ResetToken]
v: int = 0 v: int = 0
def __post_init__(self):
# Set the key fields on all stored objects
for uuid, perm in self.permissions.items():
perm.uuid = uuid
for uuid, org in self.orgs.items():
org.uuid = uuid
for uuid, role in self.roles.items():
role.uuid = uuid
for uuid, user in self.users.items():
user.uuid = uuid
for uuid, cred in self.credentials.items():
cred.uuid = uuid
for key, session in self.sessions.items():
session.key = key
for key, token in self.reset_tokens.items():
token.key = key