"""Central capability resolution and FastAPI enforcement dependencies.""" from __future__ import annotations import os from typing import Any, Callable from fastapi import Depends, HTTPException from rights_registry import load_grants_for_roles from services.audit import log_audit from tenant_context import TenantContext, get_tenant_context, require_tenant_context CAPABILITY_ENFORCE_ENV = "CAPABILITY_ENFORCE" def capability_enforcement_mode() -> str: raw = os.getenv(CAPABILITY_ENFORCE_ENV, "probe").strip().lower() return "enforce" if raw in ("1", "true", "enforce", "yes") else "probe" def resolve_capabilities(*, portal_role: str, tenant_role: str | None) -> frozenset[str]: return frozenset(load_grants_for_roles(portal_role=portal_role, tenant_role=tenant_role)) def has_capability(ctx: TenantContext, capability_key: str) -> bool: return capability_key in ctx.capabilities def check_capability(ctx: TenantContext, capability_key: str) -> dict[str, Any]: allowed = has_capability(ctx, capability_key) return { "capability_key": capability_key, "allowed": allowed, "reason": None if allowed else "missing_grant", } def require_capability(capability_key: str) -> Callable[..., TenantContext]: """FastAPI dependency — blocks in enforce mode, probes in probe mode.""" def _dependency(ctx: TenantContext = Depends(require_tenant_context)) -> TenantContext: return _enforce_capability(ctx, capability_key) return _dependency def require_capability_ctx(capability_key: str) -> Callable[..., TenantContext]: """Like require_capability but allows portal context without active tenant.""" def _dependency(ctx: TenantContext = Depends(get_tenant_context)) -> TenantContext: return _enforce_capability(ctx, capability_key) return _dependency def _enforce_capability(ctx: TenantContext, capability_key: str) -> TenantContext: result = check_capability(ctx, capability_key) if result["allowed"]: return ctx log_audit( "capability.denied", user_id=ctx.user_id, tenant_id=ctx.tenant_id, details={ "capability_key": capability_key, "mode": capability_enforcement_mode(), "portal_role": ctx.portal_role, "tenant_role": ctx.tenant_role, }, ) if capability_enforcement_mode() == "enforce": raise HTTPException( status_code=403, detail=f"Capability fehlt: {capability_key}", ) return ctx