T01: core infrastructure + auth + multi-tenant + RLS
- 10 models: tenants, users, user_tenants, roles, sessions, audit_log, deletion_log, notifications, password_reset_tokens, api_tokens - Session-based auth (Redis + PostgreSQL audit trail) - Multi-tenant with ORM-level filtering + PostgreSQL RLS (set_config) - RBAC with roles/permissions + field-level permissions - CSRF protection via Origin header validation - Auth rate limiting (Redis counters with TTL) - CORS with explicit origins (no wildcard) - Health endpoint (no auth required) - Notification service + audit log middleware - 29 tests, 26 ACs, all passing - Coverage: 62% (infrastructure modules pending coverage in later tasks)
This commit is contained in:
+96
@@ -0,0 +1,96 @@
|
||||
"""FastAPI dependencies: auth, db, tenant context, RBAC."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
import redis.asyncio as aioredis
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import get_settings
|
||||
from app.core.auth import get_redis, get_session_data
|
||||
from app.core.db import get_db, set_tenant_context
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
async def get_redis_dep() -> aioredis.Redis:
|
||||
"""FastAPI dependency for Redis client."""
|
||||
return get_redis()
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis_dep),
|
||||
) -> dict[str, Any]:
|
||||
"""Get the current authenticated user from session cookie.
|
||||
Returns session data dict with user_id, tenant_id, email, name, role.
|
||||
"""
|
||||
settings = get_settings()
|
||||
session_id = request.cookies.get(settings.session_cookie_name)
|
||||
if not session_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"detail": "Not authenticated", "code": "not_authenticated"},
|
||||
)
|
||||
|
||||
session_data = await get_session_data(redis, session_id)
|
||||
if session_data is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"detail": "Session expired or invalid", "code": "session_invalid"},
|
||||
)
|
||||
|
||||
if not session_data.get("is_active", True):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"detail": "User account deactivated", "code": "user_inactive"},
|
||||
)
|
||||
|
||||
# Set RLS tenant context
|
||||
tenant_id = uuid.UUID(session_data["tenant_id"])
|
||||
await set_tenant_context(db, tenant_id)
|
||||
|
||||
return session_data
|
||||
|
||||
|
||||
async def require_admin(
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
"""Require admin role."""
|
||||
if current_user.get("role") != "admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={"detail": "Admin access required", "code": "forbidden"},
|
||||
)
|
||||
return current_user
|
||||
|
||||
|
||||
async def require_write(
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
"""Require write permission (admin or editor)."""
|
||||
role = current_user.get("role", "viewer")
|
||||
if role not in ("admin", "editor"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={"detail": "Write access required", "code": "forbidden"},
|
||||
)
|
||||
return current_user
|
||||
|
||||
|
||||
async def get_tenant_id(
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
) -> uuid.UUID:
|
||||
"""Extract tenant_id from current user session."""
|
||||
return uuid.UUID(current_user["tenant_id"])
|
||||
|
||||
|
||||
async def get_current_user_id(
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
) -> uuid.UUID:
|
||||
"""Extract user_id from current user session."""
|
||||
return uuid.UUID(current_user["user_id"])
|
||||
Reference in New Issue
Block a user