from __future__ import annotations from dataclasses import dataclass from collections.abc import Mapping, Sequence from typing import Protocol, runtime_checkable from sqlalchemy.orm import Session from govoplan_core.admin.common import AdminValidationError from govoplan_core.admin.settings import get_system_settings from govoplan_core.core.access import CAPABILITY_ACCESS_ADMINISTRATION, AccessAdministration from govoplan_core.core.runtime import get_registry from govoplan_core.tenancy.scope import Tenant @dataclass(frozen=True) class EffectiveTenantGovernance: allow_custom_groups: bool allow_custom_roles: bool allow_api_keys: bool @runtime_checkable class _AccessTenantCountsBatchProvider(Protocol): def tenant_counts_many( self, session: object, tenant_ids: Sequence[str], ) -> Mapping[str, Mapping[str, int]]: ... def _narrowing_bool(system_allows: bool, tenant_override: bool | None) -> bool: if not system_allows: return False return tenant_override is not False def effective_tenant_governance(session: Session, tenant: Tenant) -> EffectiveTenantGovernance: defaults = get_system_settings(session) return EffectiveTenantGovernance( allow_custom_groups=_narrowing_bool(defaults.allow_tenant_custom_groups, tenant.allow_custom_groups), allow_custom_roles=_narrowing_bool(defaults.allow_tenant_custom_roles, tenant.allow_custom_roles), allow_api_keys=_narrowing_bool(defaults.allow_tenant_api_keys, tenant.allow_api_keys), ) def assert_tenant_governance_override_allowed(session: Session, *, field: str, value: bool | None) -> None: if value is not True: return defaults = get_system_settings(session) blocked = { "allow_custom_groups": not defaults.allow_tenant_custom_groups, "allow_custom_roles": not defaults.allow_tenant_custom_roles, "allow_api_keys": not defaults.allow_tenant_api_keys, } if blocked.get(field): raise AdminValidationError("Tenant governance cannot explicitly allow a capability denied by system settings.") def _tenant_module_counts( session: Session, tenant_id: str, *, module_ids: tuple[str, ...] | None = None, ) -> dict[str, int]: registry = get_registry() if registry is None or not hasattr(registry, "tenant_summary_providers"): return {} counts: dict[str, int] = {} providers = registry.tenant_summary_providers() if module_ids is not None: providers = { module_id: provider for module_id, provider in providers.items() if module_id in module_ids } for provider in providers.values(): provided = provider(session, tenant_id) counts.update({str(key): int(value) for key, value in provided.items()}) return counts def _access_administration() -> AccessAdministration | None: registry = get_registry() if registry is None or not hasattr(registry, "has_capability") or not registry.has_capability(CAPABILITY_ACCESS_ADMINISTRATION): return None capability = registry.require_capability(CAPABILITY_ACCESS_ADMINISTRATION) if not isinstance(capability, AccessAdministration): raise TypeError("Access administration capability is invalid") return capability def tenant_counts( session: Session, tenant_id: str, *, module_ids: tuple[str, ...] | None = None, ) -> dict[str, int]: module_counts = _tenant_module_counts( session, tenant_id, module_ids=module_ids, ) access_administration = _access_administration() access_counts = access_administration.tenant_counts(session, tenant_id) if access_administration is not None else {} return { **module_counts, "users": int(access_counts.get("users", 0)), "active_users": int(access_counts.get("active_users", 0)), "groups": int(access_counts.get("groups", 0)), "campaigns": int(module_counts.get("campaigns", 0)), "files": int(module_counts.get("files", 0)), "api_keys": int(access_counts.get("api_keys", 0)), "active_api_keys": int(access_counts.get("active_api_keys", access_counts.get("api_keys", 0))), } def tenant_counts_many( session: Session, tenant_ids: Sequence[str], *, module_ids: tuple[str, ...] | None = None, ) -> dict[str, dict[str, int]]: """Collect list-page summaries while preserving legacy provider support.""" normalized_ids = tuple( dict.fromkeys( str(tenant_id).strip() for tenant_id in tenant_ids if str(tenant_id).strip() ) ) counts_by_tenant: dict[str, dict[str, int]] = { tenant_id: {} for tenant_id in normalized_ids } if not normalized_ids: return counts_by_tenant registry = get_registry() if registry is not None and hasattr(registry, "tenant_summary_providers"): providers = dict(registry.tenant_summary_providers()) if module_ids is not None: providers = { module_id: provider for module_id, provider in providers.items() if module_id in module_ids } batch_providers = ( dict(registry.tenant_summary_batch_providers()) if hasattr(registry, "tenant_summary_batch_providers") else {} ) for module_id, provider in providers.items(): batch_provider = batch_providers.get(module_id) if batch_provider is None: provided_by_tenant = { tenant_id: provider(session, tenant_id) for tenant_id in normalized_ids } else: provided_by_tenant = batch_provider(session, normalized_ids) _merge_tenant_counts( counts_by_tenant, provided_by_tenant, tenant_ids=normalized_ids, ) access_administration = _access_administration() if isinstance(access_administration, _AccessTenantCountsBatchProvider): access_counts = access_administration.tenant_counts_many( session, normalized_ids, ) elif access_administration is not None: access_counts = { tenant_id: access_administration.tenant_counts(session, tenant_id) for tenant_id in normalized_ids } else: access_counts = {} _merge_tenant_counts( counts_by_tenant, access_counts, tenant_ids=normalized_ids, ) return { tenant_id: _normalized_tenant_counts(counts_by_tenant[tenant_id]) for tenant_id in normalized_ids } def _merge_tenant_counts( target: dict[str, dict[str, int]], provided_by_tenant: Mapping[str, Mapping[str, int]], *, tenant_ids: Sequence[str], ) -> None: for tenant_id in tenant_ids: provided = provided_by_tenant.get(tenant_id, {}) target[tenant_id].update( {str(key): int(value) for key, value in provided.items()} ) def _normalized_tenant_counts(counts: Mapping[str, int]) -> dict[str, int]: return { **{str(key): int(value) for key, value in counts.items()}, "users": int(counts.get("users", 0)), "active_users": int(counts.get("active_users", 0)), "groups": int(counts.get("groups", 0)), "campaigns": int(counts.get("campaigns", 0)), "files": int(counts.get("files", 0)), "api_keys": int(counts.get("api_keys", 0)), "active_api_keys": int( counts.get("active_api_keys", counts.get("api_keys", 0)) ), }