"""Canonical RBAC and privacy projection for Intelligence read surfaces.""" from __future__ import annotations from collections.abc import Iterable from dataclasses import dataclass from typing import Any from uuid import UUID from sqlalchemy.ext.asyncio import AsyncSession from app.core.exceptions import AppException from app.core.rbac import ( ASSET_ADMIN_ROLE, ASSET_GLOBAL_ADMIN_ROLE, AUDITOR_ROLE, CLOUD_ADMIN_ROLE, FINANCE_ROLE, GLOBAL_ADMIN_ROLE, HR_ROLE, MANAGEMENT_ROLE, normalize_role_names, ) from app.intelligence.models import IntelligenceSnapshot from app.intelligence.provider_registry import BUSINESS_AUDIENCE_KEY, PLATFORM_DIMENSIONS from app.intelligence.regional_audiences import ( GLOBAL_ASSET_AUDIENCE_ROLES, active_location_ids_managed_by, regional_asset_audience, ) from app.intelligence.scoring import score_health from app.intelligence.snapshot_service import SnapshotSubject _ALL = frozenset(PLATFORM_DIMENSIONS) _ROLE_DIMENSIONS: dict[str, frozenset[str]] = { GLOBAL_ADMIN_ROLE: _ALL, MANAGEMENT_ROLE: _ALL, HR_ROLE: frozenset({"people_org", "lifecycle"}), FINANCE_ROLE: frozenset({"assets", "software_api", "cloud_network", "cost_efficiency"}), CLOUD_ADMIN_ROLE: frozenset({"cloud_network", "cost_efficiency"}), ASSET_GLOBAL_ADMIN_ROLE: frozenset({"assets"}), ASSET_ADMIN_ROLE: frozenset({"assets"}), AUDITOR_ROLE: frozenset({"security_compliance"}), } _PAID_COST_ROLES = frozenset({GLOBAL_ADMIN_ROLE, FINANCE_ROLE, CLOUD_ADMIN_ROLE}) @dataclass(frozen=True) class DimensionAccess: dimension: str subject: SnapshotSubject @dataclass(frozen=True) class IntelligenceAccessPlan: dimensions: tuple[DimensionAccess, ...] can_manage_recommendations: bool can_force_generate: bool can_view_paid_cost: bool @dataclass(frozen=True) class SnapshotProjection: """Public aggregate plus a server-only, already-trimmed reference map.""" public: dict[str, Any] local_reference_map: dict[str, Any] async def resolve_access_plan( db: AsyncSession, *, person_id: UUID, role_names: Iterable[str], ) -> IntelligenceAccessPlan: """Resolve the current narrowed role view into exact snapshot subjects.""" roles = frozenset(normalize_role_names(role_names)) visible = set().union(*(_ROLE_DIMENSIONS.get(role, frozenset()) for role in roles)) if not visible: raise AppException( code="INTELLIGENCE_FORBIDDEN", message="You do not have access to Intelligence Center.", status_code=403, ) regional_subject: SnapshotSubject | None = None if "assets" in visible and not roles.intersection(GLOBAL_ASSET_AUDIENCE_ROLES): location_ids = await active_location_ids_managed_by(db, person_id) if not location_ids: visible.remove("assets") else: regional_subject = SnapshotSubject( "domain", "assets", regional_asset_audience(location_ids).key, ) if not visible: raise AppException( code="INTELLIGENCE_FORBIDDEN", message="You do not have an active Intelligence data scope.", status_code=403, ) global_subject = SnapshotSubject("platform", "platform", BUSINESS_AUDIENCE_KEY) dimensions = tuple( DimensionAccess( dimension, regional_subject if dimension == "assets" and regional_subject else global_subject, ) for dimension in PLATFORM_DIMENSIONS if dimension in visible ) is_global_admin = GLOBAL_ADMIN_ROLE in roles return IntelligenceAccessPlan( dimensions=dimensions, can_manage_recommendations=is_global_admin, can_force_generate=is_global_admin, can_view_paid_cost=bool(roles.intersection(_PAID_COST_ROLES)), ) def project_snapshot( snapshot: IntelligenceSnapshot, *, dimensions: Iterable[str], ) -> SnapshotProjection: """Project aggregate facts without ever exposing local object identifiers.""" allowed = set(dimensions) unknown = allowed - set(PLATFORM_DIMENSIONS) if not allowed or unknown: raise ValueError(f"Invalid Intelligence projection dimensions: {sorted(unknown)}") requested = tuple(key for key in PLATFORM_DIMENSIONS if key in allowed) selected = [row for row in snapshot.dimensions if row.get("key") in allowed] if {row["key"] for row in selected} != allowed: raise ValueError("Snapshot does not contain every requested dimension") used_refs = { ref for dimension in selected for metric in dimension.get("metrics", []) for ref in metric.get("evidence_refs", []) } health = score_health(selected, expected_dimensions=requested) public = { "snapshot_id": str(snapshot.id), "score_version": snapshot.score_version, "facts_hash": snapshot.facts_hash, "overall_score": str(health["overall_score"]), "confidence_score": str(health["confidence_score"]), "insufficient_data": health["insufficient_data"], "grade": health["grade"], "dimensions": health["dimensions"], "aggregate_facts": { key: snapshot.aggregate_facts[key] for key in requested if key in snapshot.aggregate_facts }, "data_as_of": snapshot.data_as_of.isoformat() if snapshot.data_as_of else None, "created_at": snapshot.created_at.isoformat(), } return SnapshotProjection( public=public, local_reference_map={ ref: snapshot.reference_map[ref] for ref in sorted(used_refs) if ref in snapshot.reference_map }, ) __all__ = [ "DimensionAccess", "IntelligenceAccessPlan", "SnapshotProjection", "project_snapshot", "resolve_access_plan", ]