P3-A: Add account_id to audit_logs model + migration (backfill via user_id → users.account_id). log_audit() gains optional account_id param with fallback SELECT to avoid churn across 40 call sites. P3-B: Add account_id to tree_shares model + migration (backfill via created_by → users.account_id). TreeShare constructor updated in trees.py. P3-C: Enable RLS on 6 remaining tables: step_ratings, step_usage_log, target_lists, session_shares, audit_logs, tree_shares. P3-D: Drop team_id from target_lists — endpoint, schema, and model now use account_id as the sole isolation key. P3-E: Append Phase 3 RLS isolation tests for all 6 tables. test_target_lists.py: fix cross-account test to use Account model (not Team) and set account_id on new User. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
34 lines
1.0 KiB
Python
34 lines
1.0 KiB
Python
"""Centralized audit logging for admin and destructive actions."""
|
|
from uuid import UUID
|
|
from typing import Optional
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from app.models.audit_log import AuditLog
|
|
|
|
|
|
async def log_audit(
|
|
db: AsyncSession,
|
|
user_id: UUID,
|
|
action: str,
|
|
resource_type: str,
|
|
resource_id: Optional[UUID] = None,
|
|
details: Optional[dict] = None,
|
|
account_id: Optional[UUID] = None,
|
|
) -> None:
|
|
"""Record an audit log entry. Does not commit — piggybacks on the caller's commit."""
|
|
if account_id is None:
|
|
# Derive from the acting user's account as a fallback (one extra query).
|
|
from sqlalchemy import select
|
|
from app.models.user import User
|
|
result = await db.execute(select(User.account_id).where(User.id == user_id))
|
|
account_id = result.scalar_one()
|
|
|
|
entry = AuditLog(
|
|
user_id=user_id,
|
|
account_id=account_id,
|
|
action=action,
|
|
resource_type=resource_type,
|
|
resource_id=resource_id,
|
|
details=details,
|
|
)
|
|
db.add(entry)
|