2026-07-29 02:47:03 +02:00
|
|
|
"""Bulk permission service — mass share/unshare operations for entity permissions.
|
|
|
|
|
|
|
|
|
|
Provides efficient batch operations for sharing multiple entities at once
|
|
|
|
|
with the same principal and permission level.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
import uuid
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
2026-08-16 01:17:18 +02:00
|
|
|
from sqlalchemy import select
|
2026-07-29 02:47:03 +02:00
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
from app.models.entity_permission import EntityPermission
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2026-08-16 01:17:18 +02:00
|
|
|
from app.core.permissions import PERM_RANK as _PERM_RANK # noqa: E402
|
2026-07-29 02:47:03 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _rank(level: str) -> int:
|
|
|
|
|
return _PERM_RANK.get(level, 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def bulk_share(
|
|
|
|
|
db: AsyncSession,
|
|
|
|
|
tenant_id: uuid.UUID,
|
|
|
|
|
entity_type: str,
|
|
|
|
|
entity_ids: list[str],
|
|
|
|
|
principal_type: str,
|
|
|
|
|
principal_id: str,
|
|
|
|
|
level: str,
|
|
|
|
|
created_by: uuid.UUID | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
"""Share multiple entities with a principal at a given permission level.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
db: Database session
|
|
|
|
|
tenant_id: Tenant UUID
|
|
|
|
|
entity_type: Type of entity (e.g. 'contact', 'document')
|
|
|
|
|
entity_ids: List of entity UUID strings
|
|
|
|
|
principal_type: 'user', 'group', or 'role'
|
|
|
|
|
principal_id: UUID string of the principal
|
|
|
|
|
level: Permission level ('read', 'write', 'admin', 'delete')
|
|
|
|
|
created_by: User UUID who initiated the bulk share
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Dict with counts of created, updated, skipped, and errors
|
|
|
|
|
"""
|
|
|
|
|
principal_uuid = uuid.UUID(principal_id)
|
|
|
|
|
entity_uuids = [uuid.UUID(eid) for eid in entity_ids]
|
|
|
|
|
|
|
|
|
|
created_count = 0
|
|
|
|
|
updated_count = 0
|
|
|
|
|
skipped_count = 0
|
|
|
|
|
errors: list[dict] = []
|
|
|
|
|
|
|
|
|
|
for entity_uuid in entity_uuids:
|
|
|
|
|
try:
|
|
|
|
|
# Check for existing permission
|
|
|
|
|
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 == principal_uuid)
|
|
|
|
|
.where(EntityPermission.tenant_id == tenant_id)
|
|
|
|
|
)
|
|
|
|
|
existing = existing_q.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
if existing:
|
|
|
|
|
# Update if new level is higher
|
|
|
|
|
if _rank(level) > _rank(existing.permission_level):
|
|
|
|
|
existing.permission_level = level
|
|
|
|
|
updated_count += 1
|
|
|
|
|
else:
|
|
|
|
|
skipped_count += 1
|
|
|
|
|
else:
|
|
|
|
|
perm = EntityPermission(
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
entity_type=entity_type,
|
|
|
|
|
entity_id=entity_uuid,
|
|
|
|
|
principal_type=principal_type,
|
|
|
|
|
principal_id=principal_uuid,
|
|
|
|
|
permission_level=level,
|
|
|
|
|
created_by=created_by,
|
|
|
|
|
)
|
|
|
|
|
db.add(perm)
|
|
|
|
|
created_count += 1
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
errors.append({
|
|
|
|
|
"entity_id": str(entity_uuid),
|
|
|
|
|
"error": str(e),
|
|
|
|
|
})
|
|
|
|
|
logger.warning("Bulk share error for %s/%s: %s", entity_type, entity_uuid, e)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"created": created_count,
|
|
|
|
|
"updated": updated_count,
|
|
|
|
|
"skipped": skipped_count,
|
|
|
|
|
"errors": errors,
|
|
|
|
|
"total": len(entity_ids),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def bulk_unshare(
|
|
|
|
|
db: AsyncSession,
|
|
|
|
|
tenant_id: uuid.UUID,
|
|
|
|
|
entity_type: str,
|
|
|
|
|
entity_ids: list[str],
|
|
|
|
|
principal_type: str,
|
|
|
|
|
principal_id: str,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
"""Remove permissions for a principal from multiple entities."""
|
|
|
|
|
principal_uuid = uuid.UUID(principal_id)
|
|
|
|
|
entity_uuids = [uuid.UUID(eid) for eid in entity_ids]
|
|
|
|
|
|
|
|
|
|
deleted_count = 0
|
|
|
|
|
errors: list[dict] = []
|
|
|
|
|
|
|
|
|
|
for entity_uuid in entity_uuids:
|
|
|
|
|
try:
|
|
|
|
|
result = 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 == principal_uuid)
|
|
|
|
|
.where(EntityPermission.tenant_id == tenant_id)
|
|
|
|
|
)
|
|
|
|
|
perm = result.scalar_one_or_none()
|
|
|
|
|
if perm:
|
|
|
|
|
await db.delete(perm)
|
|
|
|
|
deleted_count += 1
|
|
|
|
|
except Exception as e:
|
|
|
|
|
errors.append({
|
|
|
|
|
"entity_id": str(entity_uuid),
|
|
|
|
|
"error": str(e),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"deleted": deleted_count,
|
|
|
|
|
"errors": errors,
|
|
|
|
|
"total": len(entity_ids),
|
|
|
|
|
}
|