from __future__ import annotations from sqlalchemy.orm import Session from govoplan_access.backend.db.models import Group, GroupRoleAssignment, Role, UserGroupMembership, UserRoleAssignment from govoplan_core.admin.common import AdminConflictError from govoplan_core.core.access import AccessGovernanceMaterializer, GovernanceTemplateMaterialization from govoplan_core.core.runtime import get_registry class SqlAccessGovernanceMaterializer(AccessGovernanceMaterializer): def sync_template(self, session: object, template: GovernanceTemplateMaterialization) -> None: db = _session(session) if template.kind == "group": group = ( db.query(Group) .filter(Group.tenant_id == template.tenant_id, Group.system_template_id == template.template_id) .first() ) if group is None: group = Group( tenant_id=template.tenant_id, slug=_available_slug(db, Group, template.tenant_id, template.slug), name=template.name, description=template.description, is_active=template.is_active, system_template_id=template.template_id, system_required=template.required, ) db.add(group) else: group.name = template.name group.description = template.description group.system_required = template.required if template.required: group.is_active = template.is_active db.flush() return role = ( db.query(Role) .filter(Role.tenant_id == template.tenant_id, Role.system_template_id == template.template_id) .first() ) if role is None: role = Role( tenant_id=template.tenant_id, slug=_available_slug(db, Role, template.tenant_id, template.slug), name=template.name, description=template.description, permissions=list(template.permissions), is_builtin=False, is_assignable=template.is_active, system_template_id=template.template_id, system_required=template.required, ) db.add(role) else: role.name = template.name role.description = template.description role.permissions = list(template.permissions) role.system_required = template.required if template.required: role.is_assignable = template.is_active db.flush() def remove_template(self, session: object, template: GovernanceTemplateMaterialization) -> None: db = _session(session) if template.kind == "group": group = ( db.query(Group) .filter(Group.tenant_id == template.tenant_id, Group.system_template_id == template.template_id) .first() ) if group is None: return membership_count = db.query(UserGroupMembership).filter(UserGroupMembership.group_id == group.id).count() role_count = db.query(GroupRoleAssignment).filter(GroupRoleAssignment.group_id == group.id).count() if membership_count or role_count: raise AdminConflictError( f"Cannot remove {template.name!r} from the tenant while its managed group has members or roles." ) _run_delete_vetoes(db, "group", template.tenant_id, group.id) db.delete(group) db.flush() return role = ( db.query(Role) .filter(Role.tenant_id == template.tenant_id, Role.system_template_id == template.template_id) .first() ) if role is None: return user_count = db.query(UserRoleAssignment).filter(UserRoleAssignment.role_id == role.id).count() group_count = db.query(GroupRoleAssignment).filter(GroupRoleAssignment.role_id == role.id).count() if user_count or group_count: raise AdminConflictError( f"Cannot remove {template.name!r} from the tenant while its managed role is assigned to users or groups." ) db.delete(role) db.flush() def _session(session: object) -> Session: if not isinstance(session, Session): raise TypeError("Access governance materializer requires a SQLAlchemy Session") return session def _run_delete_vetoes(session: Session, resource_type: str, tenant_id: str, resource_id: str) -> None: registry = get_registry() if registry is None or not hasattr(registry, "delete_veto_providers"): return for provider in registry.delete_veto_providers(resource_type): try: provider(session, tenant_id, resource_id) except AdminConflictError: raise except Exception as exc: raise AdminConflictError(str(exc)) from exc def _available_slug(session: Session, model: type[Group] | type[Role], tenant_id: str, base: str) -> str: candidate = base suffix = 2 while session.query(model).filter(model.tenant_id == tenant_id, model.slug == candidate).first(): candidate = f"{base}-{suffix}" suffix += 1 return candidate