131 lines
5.3 KiB
Python
131 lines
5.3 KiB
Python
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
|