Files
paskia/paskia/db/operations.py
T

939 lines
30 KiB
Python

"""
Database for WebAuthn passkey authentication.
Read operations: Access _db directly, use build_* helpers to get public structs.
Context lookup: get_session_context() returns full SessionContext with effective permissions.
Write operations: Functions that validate and commit, or raise ValueError.
"""
import hashlib
import logging
import os
import secrets
from datetime import datetime, timezone
from uuid import UUID
from paskia.db.jsonl import (
DB_PATH_DEFAULT,
JsonlStore,
)
from paskia.db.structs import (
DB,
Credential,
Org,
Permission,
ResetToken,
Role,
Session,
SessionContext,
User,
)
from paskia.util.passphrase import is_well_formed as _is_passphrase
_logger = logging.getLogger(__name__)
# Global database instance (empty until init() loads data)
_db = DB()
_store = JsonlStore(_db)
_db._store = _store
async def init(*args, **kwargs):
"""Load database from JSONL file."""
global _db
db_path = os.environ.get("PASKIA_DB", DB_PATH_DEFAULT)
if db_path.startswith("json:"):
db_path = db_path[5:]
await _store.load(db_path)
_db = _store.db
# -------------------------------------------------------------------------
# Read/lookup functions
# -------------------------------------------------------------------------
def get_user_organization(user_uuid: UUID) -> tuple[Org, str]:
"""Get the organization a user belongs to and their role name.
Raises ValueError if user not found.
Call sites:
- Get user's organization when updating user role (admin.py:493)
- Get user's organization for user credential listing (admin.py:530)
- Get user's organization for user details API (admin.py:579)
- Get user's organization for updating user display name (admin.py:721)
- Get user's organization for deleting user credential (admin.py:754)
- Get user's organization for deleting user session (admin.py:783)
"""
if user_uuid not in _db.users:
raise ValueError(f"User {user_uuid} not found")
role_uuid = _db.users[user_uuid].role
if role_uuid not in _db.roles:
raise ValueError(f"Role {role_uuid} not found")
role_data = _db.roles[role_uuid]
org_uuid = role_data.org
return _db.orgs[org_uuid], role_data.display_name
def get_organization_users(org_uuid: UUID) -> list[tuple[User, str]]:
"""Get all users in an organization with their role names.
Returns list of (User, role_display_name) tuples.
"""
role_map = {
rid: r.display_name for rid, r in _db.roles.items() if r.org == org_uuid
}
return [(u, role_map[u.role]) for u in _db.users.values() if u.role in role_map]
def get_user_credential_ids(user_uuid: UUID) -> list[bytes]:
"""Get credential IDs for a user (for WebAuthn exclude lists).
Returns empty list if user has no credentials.
"""
return [c.credential_id for c in _db.credentials.values() if c.user == user_uuid]
def _reset_key(passphrase: str) -> bytes:
"""Hash a passphrase to bytes for reset token storage."""
if not _is_passphrase(passphrase):
raise ValueError(
"Trying to reset with a session token in place of a passphrase"
if len(passphrase) == 16
else "Invalid passphrase format"
)
return hashlib.sha512(passphrase.encode()).digest()[:9]
def get_reset_token(passphrase: str) -> ResetToken | None:
"""Get reset token by passphrase.
Call sites:
- Get reset token to validate it (authsession.py:34)
"""
key = _reset_key(passphrase)
return _db.reset_tokens.get(key)
# -------------------------------------------------------------------------
# Context lookup
# -------------------------------------------------------------------------
def get_session_context(
session_key: str, host: str | None = None
) -> SessionContext | None:
"""Get full session context with effective permissions.
Args:
session_key: The session key string
host: Optional host for binding/validation and domain-scoped permissions
Returns:
SessionContext if valid, None if session not found, expired, or host mismatch
Call sites:
- Example usage in docstring (db/__init__.py:16)
- Get session context from auth token (util/permutil.py:43)
"""
from paskia.util.hostutil import normalize_host
if session_key not in _db.sessions:
return None
s = _db.sessions[session_key]
if s.expiry < datetime.now(timezone.utc):
return None
# Handle host binding
if host is not None:
if s.host is None:
# Bind session to this host
with _db.transaction("host_binding"):
s.host = host
elif s.host != host:
# Session bound to different host
return None
# Validate user exists
if s.user not in _db.users:
return None
# Validate role exists
role_uuid = _db.users[s.user].role
if role_uuid not in _db.roles:
return None
# Validate org exists
org_uuid = _db.roles[role_uuid].org
if org_uuid not in _db.orgs:
return None
session = _db.sessions[session_key]
user = _db.users[s.user]
role = _db.roles[role_uuid]
org = _db.orgs[org_uuid]
# Credential must exist (sessions are cascade-deleted when credential is deleted)
if s.credential not in _db.credentials:
return None
credential = _db.credentials[s.credential]
# Effective permissions: role's permissions that the org can grant
# Also filter by domain if host is provided
org_perm_uuids = {pid for pid, p in _db.permissions.items() if org_uuid in p.orgs}
normalized_host = normalize_host(host)
host_without_port = normalized_host.rsplit(":", 1)[0] if normalized_host else None
effective_perms = []
for perm_uuid in role.permission_set:
if perm_uuid not in org_perm_uuids:
continue
if perm_uuid not in _db.permissions:
continue
p = _db.permissions[perm_uuid]
# Check domain restriction
if p.domain is not None and p.domain != host_without_port:
continue
effective_perms.append(_db.permissions[perm_uuid])
return SessionContext(
session=session,
user=user,
org=org,
role=role,
credential=credential,
permissions=effective_perms,
)
# -------------------------------------------------------------------------
# Write operations (validate, modify, commit or raise ValueError)
# -------------------------------------------------------------------------
def create_permission(perm: Permission, *, ctx: SessionContext | None = None) -> None:
"""Create a new permission."""
if perm.uuid in _db.permissions:
raise ValueError(f"Permission {perm.uuid} already exists")
with _db.transaction("Created permission", ctx):
_db.permissions[perm.uuid] = perm
def update_permission(perm: Permission, *, ctx: SessionContext | None = None) -> None:
"""Update a permission's scope, display_name, and domain."""
if perm.uuid not in _db.permissions:
raise ValueError(f"Permission {perm.uuid} not found")
with _db.transaction("Updated permission", ctx):
_db.permissions[perm.uuid].scope = perm.scope
_db.permissions[perm.uuid].display_name = perm.display_name
_db.permissions[perm.uuid].domain = perm.domain
def rename_permission(
uuid: UUID,
new_scope: str,
display_name: str,
domain: str | None = None,
*,
ctx: SessionContext | None = None,
) -> None:
"""Rename a permission's scope. The UUID remains the same.
Since roles reference permissions by UUID, no role updates are needed.
Note: Scopes do not need to be unique (same scope with different domains is valid).
"""
if uuid not in _db.permissions:
raise ValueError(f"Permission {uuid} not found")
with _db.transaction("Renamed permission", ctx):
# Update the permission
_db.permissions[uuid].scope = new_scope
_db.permissions[uuid].display_name = display_name
_db.permissions[uuid].domain = domain
def delete_permission(uuid: UUID, *, ctx: SessionContext | None = None) -> None:
"""Delete a permission and remove it from all roles."""
if uuid not in _db.permissions:
raise ValueError(f"Permission {uuid} not found")
with _db.transaction("Deleted permission", ctx):
# Remove this permission from all roles
for role in _db.roles.values():
role.permissions.pop(uuid, None)
del _db.permissions[uuid]
def create_organization(org: Org, *, ctx: SessionContext | None = None) -> None:
"""Create a new organization with an Administration role.
Automatically creates an 'Administration' role with auth:org:admin permission.
"""
if org.uuid in _db.orgs:
raise ValueError(f"Organization {org.uuid} already exists")
with _db.transaction("Created organization", ctx):
new_org = Org(
display_name=org.display_name, created_at=datetime.now(timezone.utc)
)
_db.orgs[org.uuid] = new_org
new_org.uuid = org.uuid
# Create Administration role with org admin permission
import uuid7
admin_role_uuid = uuid7.create()
# Find the auth:org:admin permission UUID
org_admin_perm_uuid = None
for pid, p in _db.permissions.items():
if p.scope == "auth:org:admin":
org_admin_perm_uuid = pid
break
role_permissions = {org_admin_perm_uuid: True} if org_admin_perm_uuid else {}
admin_role = Role(
org=org.uuid,
display_name="Administration",
permissions=role_permissions,
)
admin_role.uuid = admin_role_uuid
_db.roles[admin_role_uuid] = admin_role
def update_organization_name(
uuid: UUID,
display_name: str,
*,
ctx: SessionContext | None = None,
) -> None:
"""Update organization display name."""
if uuid not in _db.orgs:
raise ValueError(f"Organization {uuid} not found")
with _db.transaction("Renamed organization", ctx):
_db.orgs[uuid].display_name = display_name
def delete_organization(uuid: UUID, *, ctx: SessionContext | None = None) -> None:
"""Delete organization and all its roles/users."""
if uuid not in _db.orgs:
raise ValueError(f"Organization {uuid} not found")
with _db.transaction("Deleted organization", ctx):
# Remove org from all permissions
for p in _db.permissions.values():
p.orgs.pop(uuid, None)
# Delete roles in this org
role_uuids = [rid for rid, r in _db.roles.items() if r.org == uuid]
for rid in role_uuids:
del _db.roles[rid]
# Delete users with those roles
user_uuids = [uid for uid, u in _db.users.items() if u.role in role_uuids]
for uid in user_uuids:
del _db.users[uid]
del _db.orgs[uuid]
def add_permission_to_organization(
org_uuid: UUID,
permission_uuid: UUID,
*,
ctx: SessionContext | None = None,
) -> None:
"""Grant a permission to an organization by UUID."""
if org_uuid not in _db.orgs:
raise ValueError(f"Organization {org_uuid} not found")
if permission_uuid not in _db.permissions:
raise ValueError(f"Permission {permission_uuid} not found")
with _db.transaction("Granted org permission", ctx):
_db.permissions[permission_uuid].orgs[org_uuid] = True
def remove_permission_from_organization(
org_uuid: UUID,
permission_uuid: UUID,
*,
ctx: SessionContext | None = None,
) -> None:
"""Remove a permission from an organization by UUID."""
if org_uuid not in _db.orgs:
raise ValueError(f"Organization {org_uuid} not found")
if permission_uuid not in _db.permissions:
return # Permission not found, silently return
with _db.transaction("Revoked org permission", ctx):
_db.permissions[permission_uuid].orgs.pop(org_uuid, None)
def create_role(role: Role, *, ctx: SessionContext | None = None) -> None:
"""Create a new role."""
if role.uuid in _db.roles:
raise ValueError(f"Role {role.uuid} already exists")
if role.org not in _db.orgs:
raise ValueError(f"Organization {role.org} not found")
with _db.transaction("Created role", ctx):
_db.roles[role.uuid] = role
def update_role_name(
uuid: UUID,
display_name: str,
*,
ctx: SessionContext | None = None,
) -> None:
"""Update role display name."""
if uuid not in _db.roles:
raise ValueError(f"Role {uuid} not found")
with _db.transaction("Renamed role", ctx):
_db.roles[uuid].display_name = display_name
def add_permission_to_role(
role_uuid: UUID,
permission_uuid: UUID,
*,
ctx: SessionContext | None = None,
) -> None:
"""Add permission to role by UUID."""
if role_uuid not in _db.roles:
raise ValueError(f"Role {role_uuid} not found")
if permission_uuid not in _db.permissions:
raise ValueError(f"Permission {permission_uuid} not found")
with _db.transaction("Granted role permission", ctx):
_db.roles[role_uuid].permissions[permission_uuid] = True
def remove_permission_from_role(
role_uuid: UUID,
permission_uuid: UUID,
*,
ctx: SessionContext | None = None,
) -> None:
"""Remove permission from role by UUID."""
if role_uuid not in _db.roles:
raise ValueError(f"Role {role_uuid} not found")
with _db.transaction("Revoked role permission", ctx):
_db.roles[role_uuid].permissions.pop(permission_uuid, None)
def delete_role(uuid: UUID, *, ctx: SessionContext | None = None) -> None:
"""Delete a role."""
if uuid not in _db.roles:
raise ValueError(f"Role {uuid} not found")
# Check no users have this role
if any(u.role == uuid for u in _db.users.values()):
raise ValueError(f"Cannot delete role {uuid}: users still assigned")
with _db.transaction("Deleted role", ctx):
del _db.roles[uuid]
def create_user(new_user: User, *, ctx: SessionContext | None = None) -> None:
"""Create a new user."""
if new_user.uuid in _db.users:
raise ValueError(f"User {new_user.uuid} already exists")
if new_user.role not in _db.roles:
raise ValueError(f"Role {new_user.role} not found")
with _db.transaction("Created user", ctx):
_db.users[new_user.uuid] = new_user
def update_user_display_name(
uuid: UUID,
display_name: str,
*,
ctx: SessionContext | None = None,
) -> None:
"""Update user display name.
For self-service (user updating own name), ctx can be None and user is derived from uuid.
For admin operations, ctx should be provided.
"""
if isinstance(uuid, str):
uuid = UUID(uuid)
if uuid not in _db.users:
raise ValueError(f"User {uuid} not found")
# For self-service, derive user from the uuid being modified
user_str = str(uuid) if not ctx else None
with _db.transaction("Renamed user", ctx, user=user_str):
_db.users[uuid].display_name = display_name
def update_user_role(
uuid: UUID,
role_uuid: UUID,
*,
ctx: SessionContext | None = None,
) -> None:
"""Update user's role."""
if uuid not in _db.users:
raise ValueError(f"User {uuid} not found")
if role_uuid not in _db.roles:
raise ValueError(f"Role {role_uuid} not found")
with _db.transaction("Changed user role", ctx):
_db.users[uuid].role = role_uuid
def update_user_role_in_organization(
user_uuid: UUID,
role_name: str,
*,
ctx: SessionContext | None = None,
) -> None:
"""Update user's role by role name within their current organization."""
if user_uuid not in _db.users:
raise ValueError(f"User {user_uuid} not found")
current_role_uuid = _db.users[user_uuid].role
if current_role_uuid not in _db.roles:
raise ValueError("Current role not found")
org_uuid = _db.roles[current_role_uuid].org
# Find role by name in the same org
new_role_uuid = None
for rid, r in _db.roles.items():
if r.org == org_uuid and r.display_name == role_name:
new_role_uuid = rid
break
if new_role_uuid is None:
raise ValueError(f"Role '{role_name}' not found in organization")
with _db.transaction("Changed user role", ctx):
_db.users[user_uuid].role = new_role_uuid
def delete_user(uuid: UUID, *, ctx: SessionContext | None = None) -> None:
"""Delete user and their credentials/sessions."""
if uuid not in _db.users:
raise ValueError(f"User {uuid} not found")
with _db.transaction("Deleted user", ctx):
# Delete credentials
cred_uuids = [cid for cid, c in _db.credentials.items() if c.user == uuid]
for cid in cred_uuids:
del _db.credentials[cid]
# Delete sessions
sess_keys = [k for k, s in _db.sessions.items() if s.user == uuid]
for k in sess_keys:
del _db.sessions[k]
# Delete reset tokens
token_keys = [k for k, t in _db.reset_tokens.items() if t.user == uuid]
for k in token_keys:
del _db.reset_tokens[k]
del _db.users[uuid]
def create_credential(cred: Credential, *, ctx: SessionContext | None = None) -> None:
"""Create a new credential."""
if cred.uuid in _db.credentials:
raise ValueError(f"Credential {cred.uuid} already exists")
if cred.user not in _db.users:
raise ValueError(f"User {cred.user} not found")
with _db.transaction("Added credential", ctx):
_db.credentials[cred.uuid] = cred
def update_credential_sign_count(
uuid: UUID,
sign_count: int,
last_used: datetime | None = None,
*,
ctx: SessionContext | None = None,
) -> None:
"""Update credential sign count and last_used."""
if uuid not in _db.credentials:
raise ValueError(f"Credential {uuid} not found")
with _db.transaction("Updated credential", ctx):
_db.credentials[uuid].sign_count = sign_count
if last_used:
_db.credentials[uuid].last_used = last_used
def delete_credential(
uuid: UUID,
user_uuid: UUID | None = None,
*,
ctx: SessionContext | None = None,
) -> None:
"""Delete a credential and all sessions using it.
If user_uuid is provided, validates that the credential belongs to that user.
"""
if uuid not in _db.credentials:
raise ValueError(f"Credential {uuid} not found")
if user_uuid is not None:
cred_user = _db.credentials[uuid].user
if cred_user != user_uuid:
raise ValueError(f"Credential {uuid} does not belong to user {user_uuid}")
with _db.transaction("Deleted credential", ctx):
# Delete all sessions using this credential
keys = [k for k, s in _db.sessions.items() if s.credential == uuid]
for k in keys:
del _db.sessions[k]
del _db.credentials[uuid]
def create_session(
key: str,
user_uuid: UUID,
credential_uuid: UUID,
host: str | None,
ip: str | None,
user_agent: str | None,
expiry: datetime,
*,
ctx: SessionContext | None = None,
) -> None:
"""Create a new session."""
if key in _db.sessions:
raise ValueError("Session already exists")
if user_uuid not in _db.users:
raise ValueError(f"User {user_uuid} not found")
if credential_uuid not in _db.credentials:
raise ValueError(f"Credential {credential_uuid} not found")
with _db.transaction("Created session", ctx):
_db.sessions[key] = Session(
user=user_uuid,
credential=credential_uuid,
host=host,
ip=ip,
user_agent=user_agent,
expiry=expiry,
)
def update_session(
key: str,
host: str | None = None,
ip: str | None = None,
user_agent: str | None = None,
expiry: datetime | None = None,
*,
ctx: SessionContext | None = None,
) -> None:
"""Update session metadata."""
if key not in _db.sessions:
raise ValueError("Session not found")
with _db.transaction("Updated session", ctx):
s = _db.sessions[key]
if host is not None:
s.host = host
if ip is not None:
s.ip = ip
if user_agent is not None:
s.user_agent = user_agent
if expiry is not None:
s.expiry = expiry
def set_session_host(key: str, host: str, *, ctx: SessionContext | None = None) -> None:
"""Set the host for a session (first-time binding)."""
update_session(key, host=host, ctx=ctx)
def delete_session(key: str, *, ctx: SessionContext | None = None) -> None:
"""Delete a session.
For logout (user deleting own session), ctx can be None and user is derived from session.
For admin operations, ctx should be provided.
"""
if key not in _db.sessions:
raise ValueError("Session not found")
# For self-service logout, derive user from the session being deleted
user_str = str(_db.sessions[key].user) if not ctx else None
with _db.transaction("Deleted session", ctx, user=user_str):
del _db.sessions[key]
def delete_sessions_for_user(
user_uuid: UUID, *, ctx: SessionContext | None = None
) -> None:
"""Delete all sessions for a user.
For logout-all (user deleting own sessions), ctx can be None and user is derived from user_uuid.
For admin operations, ctx should be provided.
"""
# For self-service, derive user from the user_uuid param
user_str = str(user_uuid) if not ctx else None
with _db.transaction("Deleted user sessions", ctx, user=user_str):
keys = [k for k, s in _db.sessions.items() if s.user == user_uuid]
for k in keys:
del _db.sessions[k]
def create_reset_token(
passphrase: str,
user_uuid: UUID,
expiry: datetime,
token_type: str,
*,
ctx: SessionContext | None = None,
) -> None:
"""Create a reset token from a passphrase.
For self-service (user creating own recovery link), ctx can be None and user is derived from user_uuid.
For admin operations, ctx should be provided.
"""
key = _reset_key(passphrase)
if key in _db.reset_tokens:
raise ValueError("Reset token already exists")
if user_uuid not in _db.users:
raise ValueError(f"User {user_uuid} not found")
# For self-service, derive user from the user_uuid param
user_str = str(user_uuid) if not ctx else None
with _db.transaction("Created reset token", ctx, user=user_str):
_db.reset_tokens[key] = ResetToken(
user=user_uuid, expiry=expiry, token_type=token_type
)
def delete_reset_token(key: bytes, *, ctx: SessionContext | None = None) -> None:
"""Delete a reset token."""
if key not in _db.reset_tokens:
raise ValueError("Reset token not found")
with _db.transaction("Deleted reset token", ctx):
del _db.reset_tokens[key]
# -------------------------------------------------------------------------
# Cleanup (called by background task)
# -------------------------------------------------------------------------
def cleanup_expired() -> int:
"""Remove expired sessions and reset tokens. Returns count removed."""
now = datetime.now(timezone.utc)
count = 0
with _db.transaction("Cleaned up expired"):
expired_sessions = [k for k, s in _db.sessions.items() if s.expiry < now]
for k in expired_sessions:
del _db.sessions[k]
count += 1
expired_tokens = [k for k, t in _db.reset_tokens.items() if t.expiry < now]
for k in expired_tokens:
del _db.reset_tokens[k]
count += 1
return count
# -------------------------------------------------------------------------
# Composite operations (used by app code)
# -------------------------------------------------------------------------
def _create_token() -> str:
"""Generate a 16-character URL-safe session token."""
return secrets.token_urlsafe(12)
def login(
user_uuid: UUID,
credential: Credential,
host: str | None,
ip: str | None,
user_agent: str | None,
expiry: datetime,
) -> str:
"""Update user/credential on login and create session in a single transaction.
Updates:
- user.last_seen, user.visits
- credential.sign_count, credential.last_used
Creates:
- new session
Returns the generated session token.
"""
if isinstance(user_uuid, str):
user_uuid = UUID(user_uuid)
now = datetime.now(timezone.utc)
if user_uuid not in _db.users:
raise ValueError(f"User {user_uuid} not found")
if credential.uuid not in _db.credentials:
raise ValueError(f"Credential {credential.uuid} not found")
session_key = _create_token()
user_str = str(user_uuid)
with _db.transaction("User logged in", user=user_str):
# Update user
_db.users[user_uuid].last_seen = now
_db.users[user_uuid].visits += 1
# Update credential
_db.credentials[credential.uuid].sign_count = credential.sign_count
_db.credentials[credential.uuid].last_used = now
# Create session
_db.sessions[session_key] = Session(
user=user_uuid,
credential=credential.uuid,
host=host,
ip=ip,
user_agent=user_agent,
expiry=expiry,
)
return session_key
def create_credential_session(
user_uuid: UUID,
credential: Credential,
host: str | None,
ip: str | None,
user_agent: str | None,
display_name: str | None = None,
reset_key: bytes | None = None,
) -> str:
"""Create a credential and session together, optionally consuming a reset token.
Used during registration to atomically:
1. Update user display_name if provided
2. Create the credential
3. Create the session
4. Delete the reset token if provided
Returns the generated session token.
"""
from paskia.config import SESSION_LIFETIME
now = datetime.now(timezone.utc)
expiry = now + SESSION_LIFETIME
session_key = _create_token()
if user_uuid not in _db.users:
raise ValueError(f"User {user_uuid} not found")
user_str = str(user_uuid)
with _db.transaction("Registered credential", user=user_str):
# Update display name if provided
if display_name:
_db.users[user_uuid].display_name = display_name
# Create credential
_db.credentials[credential.uuid] = credential
# Create session
_db.sessions[session_key] = Session(
user=user_uuid,
credential=credential.uuid,
host=host,
ip=ip,
user_agent=user_agent,
expiry=expiry,
)
# Delete reset token if provided
if reset_key:
if reset_key in _db.reset_tokens:
del _db.reset_tokens[reset_key]
return session_key
# -------------------------------------------------------------------------
# Bootstrap (single transaction for initial system setup)
# -------------------------------------------------------------------------
def bootstrap(
org_name: str = "Organization",
admin_name: str = "Admin",
reset_passphrase: str | None = None,
reset_expiry: datetime | None = None,
) -> str:
"""Bootstrap the entire system in a single transaction.
Creates:
- auth:admin permission (Master Admin)
- auth:org:admin permission (Org Admin)
- Organization with Administration role
- Admin user with Administration role
- Reset token for admin registration
This is the only way to create a new database file (besides migrate).
All data is created atomically - if any step fails, nothing is written.
Args:
org_name: Display name for the organization (default: "Organization")
admin_name: Display name for the admin user (default: "Admin")
reset_passphrase: Passphrase for the reset token (generated if not provided)
reset_expiry: Expiry datetime for the reset token (default: 14 days)
Returns:
The reset passphrase for admin registration.
"""
import uuid7
from paskia.authsession import reset_expires
from paskia.util.passphrase import generate as generate_passphrase
# Check if system is already bootstrapped
for p in _db.permissions.values():
if p.scope == "auth:admin":
raise ValueError(
"System already bootstrapped (auth:admin permission exists)"
)
# Generate UUIDs upfront
perm_admin_uuid = uuid7.create()
perm_org_admin_uuid = uuid7.create()
org_uuid = uuid7.create()
role_uuid = uuid7.create()
user_uuid = uuid7.create()
# Generate reset token components
if reset_passphrase is None:
reset_passphrase = generate_passphrase()
if reset_expiry is None:
reset_expiry = reset_expires()
reset_key = _reset_key(reset_passphrase)
now = datetime.now(timezone.utc)
with _db.transaction("bootstrap"):
# Create auth:admin permission
perm_admin = Permission(
scope="auth:admin",
display_name="Master Admin",
orgs={org_uuid: True}, # Grant to org
)
perm_admin.uuid = perm_admin_uuid
_db.permissions[perm_admin_uuid] = perm_admin
# Create auth:org:admin permission
perm_org_admin = Permission(
scope="auth:org:admin",
display_name="Org Admin",
orgs={org_uuid: True}, # Grant to org
)
perm_org_admin.uuid = perm_org_admin_uuid
_db.permissions[perm_org_admin_uuid] = perm_org_admin
# Create organization
new_org = Org(
display_name=org_name,
created_at=now,
)
new_org.uuid = org_uuid
_db.orgs[org_uuid] = new_org
# Create Administration role with both permissions
admin_role = Role(
org=org_uuid,
display_name="Administration",
permissions={perm_admin_uuid: True, perm_org_admin_uuid: True},
)
admin_role.uuid = role_uuid
_db.roles[role_uuid] = admin_role
# Create admin user
admin_user = User(
display_name=admin_name,
role=role_uuid,
created_at=now,
last_seen=None,
visits=0,
)
admin_user.uuid = user_uuid
_db.users[user_uuid] = admin_user
# Create reset token
_db.reset_tokens[reset_key] = ResetToken(
user=user_uuid,
expiry=reset_expiry,
token_type="admin bootstrap",
)
return reset_passphrase