"""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 datetime import datetime, UTC from typing import Any from sqlalchemy import and_, or_, select from sqlalchemy.ext.asyncio import AsyncSession from app.models.entity_permission import EntityPermission from app.models.group import UserGroup from app.models.user import User, UserTenant logger = logging.getLogger(__name__) _PERM_RANK = {"none": 0, "read": 1, "write": 2, "admin": 3, "delete": 4, "owner": 5} 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) await db.commit() 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), }) await db.commit() return { "deleted": deleted_count, "errors": errors, "total": len(entity_ids), }