Files
leocrm/app/services/bulk_permission_service.py
T
Agent Zero 627360113f fix(permissions): fix 10 high-priority permission system issues
P8: Invalidate all Redis sessions when is_system_admin changes
- Added is_system_admin to UserUpdate schema and UserResponse
- Added invalidate_all_user_sessions call in users.py route
- Added is_system_admin param to user_service.update_user

P9: Remove no-op permission resolution strategies
- Only highest_wins supported, others removed as no-ops
- Updated tenant.py CheckConstraint to only allow highest_wins
- Added KI-Kommentar in permissions.py

P10: Remove legacy check_permission from auth.py
- Removed duplicate check_permission and filter_fields_by_permission
- Fixed ai_copilot_service.py to use permissions.check_permission
- Updated ai_copilot route to pass resolved permissions dict

P11: Verified — no guest_users remnants found

P12: Migrate ContactFolderPermission to EntityPermission
- contact_folder_permission_service now delegates to entity_permission_service
- contact_folder_service uses EntityPermission queries
- Removed ContactFolderPermission from models/__init__.py
- Created migration 0114 to migrate data and drop table

P13: Added RLS migration history comment in alembic/env.py

P14: Verified — services already apply visibility_filter
- saved_filters/views filter by user_id (personal data)
- workspaces are UI context only
- notifications already filter by entity access

P15: Split entity_permission_service.py (932 lines) into 4 modules
- permission_resolver.py: get_effective_access, get_visible_ids, etc.
- permission_cache.py: Redis caching functions
- permission_audit.py: Audit logging helpers
- entity_permission_service.py: CRUD operations + re-exports

P16: Centralize PERM_RANK in permissions.py
- Single source: app.core.permissions.PERM_RANK
- Updated all services to import from permissions.py

P17: Fix MIGRATION_DATABASE_URL to use crm_migration
- docker-compose.yaml defaults changed from crm_user to crm_migration
- .env.docker.example updated
- prestart.sh comment updated
2026-08-06 12:05:09 +02:00

156 lines
4.8 KiB
Python

"""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__)
from app.core.permissions import PERM_RANK as _PERM_RANK
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),
}