223 lines
7.4 KiB
Python
223 lines
7.4 KiB
Python
"""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
|