from __future__ import annotations from collections import defaultdict from collections.abc import Mapping, Sequence from urllib.parse import quote from sqlalchemy import func from sqlalchemy.orm import Session from govoplan_core.core.modules import ModuleContext from govoplan_core.core.search import ( SearchAuthorizationRequest, SearchBackfillPage, SearchBackfillRequest, SearchDocument, SearchResourceType, ) from govoplan_reporting.backend.db.models import ( ReportingDefinitionGrant, ReportingDefinitionIdentity, ReportingDefinitionRevision, ) from govoplan_reporting.backend.definitions import can_read_definition PROVIDER_ID = "reporting.reports" RESOURCE_TYPE = "report" class ReportingSearchSource: def resource_types(self) -> Sequence[SearchResourceType]: return ( SearchResourceType( provider_id=PROVIDER_ID, module_id="reporting", resource_type=RESOURCE_TYPE, label="Reports", requires_authorization_recheck=True, ), ) def backfill( self, session: object, *, request: SearchBackfillRequest, ) -> SearchBackfillPage: if request.provider_id != PROVIDER_ID or request.resource_type != RESOURCE_TYPE: raise ValueError("Unsupported Reporting search source.") db = _session(session) query = db.query(ReportingDefinitionRevision).filter( ReportingDefinitionRevision.tenant_id == request.tenant_id, ReportingDefinitionRevision.definition_kind == "report", ReportingDefinitionRevision.status != "retired", ReportingDefinitionRevision.superseded_at.is_(None), ) if request.cursor: query = query.filter(ReportingDefinitionRevision.id > request.cursor) rows = ( query.order_by(ReportingDefinitionRevision.id.asc()) .limit(request.limit + 1) .all() ) has_more = len(rows) > request.limit selected = rows[: request.limit] tokens = _acl_tokens(db, selected) high_watermark = ( db.query(func.max(ReportingDefinitionRevision.updated_at)) .filter( ReportingDefinitionRevision.tenant_id == request.tenant_id, ReportingDefinitionRevision.definition_kind == "report", ReportingDefinitionRevision.superseded_at.is_(None), ) .scalar() ) return SearchBackfillPage( documents=tuple( _document(row, tokens[row.definition_id]) for row in selected ), next_cursor=selected[-1].id if has_more and selected else None, complete=not has_more, high_watermark=( high_watermark.isoformat() if high_watermark is not None else None ), ) def authorize( self, session: object, principal: object, *, requests: Sequence[SearchAuthorizationRequest], ) -> Mapping[str, bool]: db = _session(session) tenant_id = str(getattr(principal, "tenant_id", "") or "") decisions = {request.reference.key: False for request in requests} for request in requests: reference = request.reference if ( reference.tenant_id != tenant_id or reference.module_id != "reporting" or reference.resource_type != RESOURCE_TYPE ): continue decisions[reference.key] = can_read_definition( db, principal, definition_kind="report", definition_id=reference.resource_id, ) return decisions def create_reporting_search_source( context: ModuleContext, ) -> ReportingSearchSource: del context return ReportingSearchSource() def _document( row: ReportingDefinitionRevision, tokens: tuple[str, ...], ) -> SearchDocument: restricted_tokens = tuple( dict.fromkeys((*tokens, "scope:reporting:definition:admin")) ) return SearchDocument( tenant_id=row.tenant_id, module_id="reporting", provider_id=PROVIDER_ID, resource_type=RESOURCE_TYPE, resource_id=row.definition_id, title=row.name, url=f"/reporting?reportId={quote(row.definition_id, safe='')}", summary=row.description, body=row.description, keywords=(row.definition_key, row.status), visibility=row.visibility, acl_tokens=restricted_tokens if row.visibility == "restricted" else (), source_revision=str(row.revision), source_updated_at=row.updated_at or row.recorded_at, metadata={ "definition_key": row.definition_key, "status": row.status, "content_hash": row.content_hash, }, requires_authorization_recheck=True, ) def _acl_tokens( session: Session, rows: Sequence[ReportingDefinitionRevision], ) -> Mapping[str, tuple[str, ...]]: result: dict[str, list[str]] = defaultdict(list) if not rows: return result identity_ids = {row.identity_id for row in rows} for identity in ( session.query(ReportingDefinitionIdentity) .filter(ReportingDefinitionIdentity.id.in_(identity_ids)) .all() ): if identity.created_by: result[identity.definition_id].append(f"account:{identity.created_by}") definition_ids = {row.definition_id for row in rows} grants = ( session.query(ReportingDefinitionGrant) .filter( ReportingDefinitionGrant.tenant_id == rows[0].tenant_id, ReportingDefinitionGrant.definition_kind == "report", ReportingDefinitionGrant.definition_id.in_(definition_ids), ReportingDefinitionGrant.active.is_(True), ) .all() ) for grant in grants: prefix = ( "function" if grant.subject_kind == "function_assignment" else grant.subject_kind ) result[grant.definition_id].append(f"{prefix}:{grant.subject_id}") return { definition_id: tuple(dict.fromkeys(values)) for definition_id, values in result.items() } def _session(value: object) -> Session: if not isinstance(value, Session): raise TypeError("Reporting search requires a SQLAlchemy session.") return value __all__ = [ "PROVIDER_ID", "RESOURCE_TYPE", "ReportingSearchSource", "create_reporting_search_source", ]