"""Permission template service — CRUD + apply_template for reusable permission presets. Templates define default sharing rules. When applied to an entity, they evaluate trigger_condition and auto_share_with to create entity_permissions entries automatically. """ from __future__ import annotations import logging import uuid from typing import Any from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.entity_permission import EntityPermission from app.models.permission_template import PermissionTemplate logger = logging.getLogger(__name__) def _serialize_template(t: PermissionTemplate) -> dict: return { "id": str(t.id), "name": t.name, "entity_type": t.entity_type, "trigger_condition": t.trigger_condition, "auto_share_with": t.auto_share_with, "level": t.level, "tenant_id": str(t.tenant_id), "created_at": t.created_at.isoformat() if t.created_at else None, "updated_at": t.updated_at.isoformat() if t.updated_at else None, } async def list_templates( db: AsyncSession, tenant_id: uuid.UUID, entity_type: str | None = None, ) -> list[dict]: """List all permission templates for a tenant, optionally filtered by entity_type.""" query = select(PermissionTemplate).where(PermissionTemplate.tenant_id == tenant_id) if entity_type: query = query.where(PermissionTemplate.entity_type == entity_type) query = query.order_by(PermissionTemplate.name) result = await db.execute(query) templates = result.scalars().all() return [_serialize_template(t) for t in templates] async def create_template( db: AsyncSession, tenant_id: uuid.UUID, name: str, entity_type: str, level: str = "read", trigger_condition: dict | None = None, auto_share_with: list | None = None, ) -> dict: """Create a new permission template.""" template = PermissionTemplate( tenant_id=tenant_id, name=name, entity_type=entity_type, level=level, trigger_condition=trigger_condition, auto_share_with=auto_share_with, ) db.add(template) await db.commit() await db.refresh(template) return _serialize_template(template) async def update_template( db: AsyncSession, tenant_id: uuid.UUID, template_id: str, **kwargs: Any, ) -> dict: """Update an existing permission template.""" result = await db.execute( select(PermissionTemplate) .where(PermissionTemplate.id == uuid.UUID(template_id)) .where(PermissionTemplate.tenant_id == tenant_id) ) template = result.scalar_one_or_none() if template is None: raise ValueError(f"Permission template {template_id} not found") updatable_fields = {"name", "entity_type", "level", "trigger_condition", "auto_share_with"} for key, value in kwargs.items(): if key in updatable_fields and value is not None: setattr(template, key, value) await db.commit() await db.refresh(template) return _serialize_template(template) async def delete_template( db: AsyncSession, tenant_id: uuid.UUID, template_id: str, ) -> None: """Delete a permission template.""" result = await db.execute( select(PermissionTemplate) .where(PermissionTemplate.id == uuid.UUID(template_id)) .where(PermissionTemplate.tenant_id == tenant_id) ) template = result.scalar_one_or_none() if template is None: raise ValueError(f"Permission template {template_id} not found") await db.delete(template) await db.commit() async def apply_template( db: AsyncSession, tenant_id: uuid.UUID, entity_type: str, entity_id: str, template_id: str | None = None, created_by: uuid.UUID | None = None, ) -> list[dict]: """Apply a permission template to an entity, creating entity_permissions entries. If template_id is provided, applies that specific template. Otherwise, finds all matching templates for the entity_type and applies them. Returns the list of created entity_permissions. """ entity_uuid = uuid.UUID(entity_id) created_permissions: list[dict] = [] if template_id: result = await db.execute( select(PermissionTemplate) .where(PermissionTemplate.id == uuid.UUID(template_id)) .where(PermissionTemplate.tenant_id == tenant_id) ) templates = [result.scalar_one_or_none()] else: result = await db.execute( select(PermissionTemplate) .where(PermissionTemplate.tenant_id == tenant_id) .where(PermissionTemplate.entity_type == entity_type) ) templates = list(result.scalars().all()) for template in templates: if template is None: continue # Evaluate trigger_condition if present if template.trigger_condition: # For now, simple evaluation: if trigger_condition exists, check if it matches # In a full implementation, this would evaluate against entity attributes if not _evaluate_trigger(template.trigger_condition, entity_type, entity_uuid): continue # Create entity_permissions from auto_share_with if template.auto_share_with: for share_entry in template.auto_share_with: principal_type = share_entry.get("principal_type", "user") principal_id = share_entry.get("principal_id") level = share_entry.get("level", template.level) if not principal_id: continue # Check if permission already exists existing_q = await db.execute( select(EntityPermission) .where(EntityPermission.entity_type == entity_type) .where(EntityPermission.entity_id == entity_uuid) .where(EntityPermission.principal_type == principal_type) .where(EntityPermission.principal_id == uuid.UUID(principal_id)) .where(EntityPermission.tenant_id == tenant_id) ) if existing_q.scalar_one_or_none(): continue perm = EntityPermission( tenant_id=tenant_id, entity_type=entity_type, entity_id=entity_uuid, principal_type=principal_type, principal_id=uuid.UUID(principal_id), permission_level=level, created_by=created_by, ) db.add(perm) await db.flush() created_permissions.append({ "id": str(perm.id), "entity_type": entity_type, "entity_id": str(entity_uuid), "principal_type": principal_type, "principal_id": principal_id, "permission_level": level, }) await db.commit() return created_permissions def _evaluate_trigger( trigger_condition: dict, entity_type: str, entity_id: uuid.UUID, ) -> bool: """Evaluate a trigger condition against an entity. Simple implementation: always returns True for now. In production, this would query entity attributes and evaluate conditions. """ # For now, always apply if trigger_condition exists # Future: evaluate against entity fields return True