diff --git a/paskia/db/background.py b/paskia/db/background.py index 9b6104a..7a94640 100644 --- a/paskia/db/background.py +++ b/paskia/db/background.py @@ -57,7 +57,6 @@ async def flush() -> None: async def _background_loop(): """Background task that periodically flushes changes and cleans up.""" - _logger.info("Background loop starting") # Run cleanup immediately on startup to clear old expired items cleanup() await flush() @@ -77,18 +76,17 @@ async def _background_loop(): await flush() # Flush cleanup changes last_cleanup = now except asyncio.CancelledError: - _logger.info("Background loop cancelled, final flush") # Final flush before exit await flush() break except Exception: - _logger.exception("Error in database background loop") + _logger.debug("Error in database background loop", exc_info=True) async def start_background(): """Start the background flush/cleanup task.""" global _background_task - + # Check if task exists but is no longer running (e.g., after uvicorn reload) if _background_task is not None: if _background_task.done(): @@ -106,10 +104,9 @@ async def start_background(): except Exception as e: _logger.debug("Error checking background task loop: %s, restarting", e) _background_task = None - + if _background_task is None: _background_task = asyncio.create_task(_background_loop()) - _logger.info("Database background task started") else: _logger.debug("Background task already running: %s", _background_task) diff --git a/paskia/db/jsonl.py b/paskia/db/jsonl.py index 645d4da..b3701d7 100644 --- a/paskia/db/jsonl.py +++ b/paskia/db/jsonl.py @@ -84,7 +84,9 @@ def compute_diff(previous: dict, current: dict) -> dict | None: return diff if diff else None -def create_change_record(action: str, diff: dict, user: str | None = None) -> _ChangeRecord: +def create_change_record( + action: str, diff: dict, user: str | None = None +) -> _ChangeRecord: """Create a change record for persistence.""" return _ChangeRecord( ts=datetime.now(timezone.utc), @@ -121,9 +123,6 @@ async def flush_changes( # Append all lines in a single write (binary mode for Windows compatibility) async with aiofiles.open(db_path, "ab") as f: await f.write(b"\n".join(lines) + b"\n") - _logger.debug( - "Flushed %d change(s) to %s", len(changes_to_write), db_path - ) return True except OSError: _logger.exception("Failed to flush database changes") diff --git a/paskia/db/operations.py b/paskia/db/operations.py index b476058..29b5bc7 100644 --- a/paskia/db/operations.py +++ b/paskia/db/operations.py @@ -7,9 +7,11 @@ Write operations: Functions that validate and commit, or raise ValueError. """ import hashlib +import json import logging import os import secrets +import sys from collections import deque from contextlib import contextmanager from datetime import datetime, timezone @@ -113,19 +115,15 @@ class DB: user_display = self._data.users[user_uuid].display_name except (ValueError, KeyError): user_display = self._current_user - import json + diff_json = json.dumps(diff, default=str) if user_display: - _logger.info( - "DB %s by %s: %s", - self._current_action, - user_display, - json.dumps(diff, default=str), + print( + f"{self._current_action} by {user_display}: {diff_json}", + file=sys.stderr, ) else: - _logger.info( - "DB %s: %s", self._current_action, json.dumps(diff, default=str) - ) + print(f"{self._current_action}: {diff_json}", file=sys.stderr) @contextmanager def transaction( @@ -527,9 +525,7 @@ def get_session_context( # ------------------------------------------------------------------------- -def create_permission( - perm: Permission, *, ctx: SessionContext | None = None -) -> None: +def create_permission(perm: Permission, *, ctx: SessionContext | None = None) -> None: """Create a new permission.""" if perm.uuid in _db._data.permissions: raise ValueError(f"Permission {perm.uuid} already exists") @@ -542,9 +538,7 @@ def create_permission( ) -def update_permission( - perm: Permission, *, ctx: SessionContext | None = None -) -> None: +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._data.permissions: raise ValueError(f"Permission {perm.uuid} not found") @@ -583,9 +577,7 @@ def rename_permission( _db._data.permissions[key].domain = domain -def delete_permission( - uuid: str | UUID, *, ctx: SessionContext | None = None -) -> None: +def delete_permission(uuid: str | UUID, *, ctx: SessionContext | None = None) -> None: """Delete a permission and remove it from all roles.""" if isinstance(uuid, str): uuid = UUID(uuid) @@ -598,9 +590,7 @@ def delete_permission( del _db._data.permissions[uuid] -def create_organization( - org: Org, *, ctx: SessionContext | None = None -) -> None: +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. @@ -651,9 +641,7 @@ def update_organization_name( _db._data.orgs[uuid].display_name = display_name -def delete_organization( - uuid: str | UUID, *, ctx: SessionContext | None = None -) -> None: +def delete_organization(uuid: str | UUID, *, ctx: SessionContext | None = None) -> None: """Delete organization and all its roles/users.""" if isinstance(uuid, str): uuid = UUID(uuid) @@ -742,9 +730,7 @@ def remove_permission_from_organization( _db._data.permissions[permission_uuid].orgs.pop(org_uuid, None) -def create_role( - role: Role, *, ctx: SessionContext | None = None -) -> None: +def create_role(role: Role, *, ctx: SessionContext | None = None) -> None: """Create a new role.""" if role.uuid in _db._data.roles: raise ValueError(f"Role {role.uuid} already exists") @@ -809,9 +795,7 @@ def remove_permission_from_role( _db._data.roles[role_uuid].permissions.pop(permission_uuid, None) -def delete_role( - uuid: str | UUID, *, ctx: SessionContext | None = None -) -> None: +def delete_role(uuid: str | UUID, *, ctx: SessionContext | None = None) -> None: """Delete a role.""" if isinstance(uuid, str): uuid = UUID(uuid) @@ -824,9 +808,7 @@ def delete_role( del _db._data.roles[uuid] -def create_user( - new_user: User, *, ctx: SessionContext | None = None -) -> None: +def create_user(new_user: User, *, ctx: SessionContext | None = None) -> None: """Create a new user.""" if new_user.uuid in _db._data.users: raise ValueError(f"User {new_user.uuid} already exists") @@ -849,7 +831,7 @@ def update_user_display_name( 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. """ @@ -909,9 +891,7 @@ def update_user_role_in_organization( _db._data.users[user_uuid].role = new_role_uuid -def delete_user( - uuid: str | UUID, *, ctx: SessionContext | None = None -) -> None: +def delete_user(uuid: str | UUID, *, ctx: SessionContext | None = None) -> None: """Delete user and their credentials/sessions.""" if isinstance(uuid, str): uuid = UUID(uuid) @@ -933,9 +913,7 @@ def delete_user( del _db._data.users[uuid] -def create_credential( - cred: Credential, *, ctx: SessionContext | None = None -) -> None: +def create_credential(cred: Credential, *, ctx: SessionContext | None = None) -> None: """Create a new credential.""" if cred.uuid in _db._data.credentials: raise ValueError(f"Credential {cred.uuid} already exists") @@ -1056,7 +1034,7 @@ def set_session_host(key: str, host: str, *, ctx: SessionContext | None = None) 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. """ @@ -1068,9 +1046,11 @@ def delete_session(key: str, *, ctx: SessionContext | None = None) -> None: del _db._data.sessions[key] -def delete_sessions_for_user(user_uuid: str | UUID, *, ctx: SessionContext | None = None) -> None: +def delete_sessions_for_user( + user_uuid: str | 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. """ @@ -1093,7 +1073,7 @@ def create_reset_token( 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. """ diff --git a/paskia/fastapi/admin.py b/paskia/fastapi/admin.py index 4e23814..de8dc05 100644 --- a/paskia/fastapi/admin.py +++ b/paskia/fastapi/admin.py @@ -214,9 +214,7 @@ async def admin_add_org_permission( ctx = await authz.verify( auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all ) - db.add_permission_to_organization( - str(org_uuid), permission_id, ctx=ctx - ) + db.add_permission_to_organization(str(org_uuid), permission_id, ctx=ctx) return {"status": "ok"} @@ -240,9 +238,7 @@ async def admin_remove_org_permission( "This would lock you out of admin access." ) - db.remove_permission_from_organization( - str(org_uuid), permission_id, ctx=ctx - ) + db.remove_permission_from_organization(str(org_uuid), permission_id, ctx=ctx) return {"status": "ok"} @@ -273,7 +269,7 @@ async def admin_create_role( perms = payload.get("permissions") or [] org = db.get_organization(str(org_uuid)) grantable = set(org.permissions or []) - + # Normalize permission IDs to UUIDs permission_uuids = [] for pid in perms: @@ -284,7 +280,7 @@ async def admin_create_role( if perm_uuid_str not in grantable: raise ValueError(f"Permission not grantable by org: {pid}") permission_uuids.append(perm_uuid_str) - + role = RoleDC( uuid=role_uuid, org_uuid=org_uuid, @@ -1060,9 +1056,7 @@ async def admin_rename_permission( _check_admin_lockout(str(perm.uuid), domain_value, request.headers.get("host")) # All current backends support rename_permission - db.rename_permission( - old_scope, new_scope, display_name, domain_value, ctx=ctx - ) + db.rename_permission(old_scope, new_scope, display_name, domain_value, ctx=ctx) return {"status": "ok"} diff --git a/tests/test_admin.py b/tests/test_admin.py index f7ee8ad..9d7c12a 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -120,7 +120,9 @@ async def second_org_session_token( @pytest_asyncio.fixture(scope="function") -async def org_admin_role(test_db: DB, test_org: Org, org_admin_permission: Permission) -> Role: +async def org_admin_role( + test_db: DB, test_org: Org, org_admin_permission: Permission +) -> Role: """Create a role with org admin permission only (no global admin).""" role = Role( uuid=uuid7.create(),