"""Universal entity permission routes — ACL management for ALL entities.""" from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.db import get_db from app.core.rate_limit import check_rate_limit from app.deps import get_current_user, get_redis_dep, require_permission from app.schemas.entity_permission import ( EntityPermissionCreate, EntityPermissionUpdate, ) from app.services import bulk_permission_service, entity_permission_service router = APIRouter(prefix="/api/v1/permissions", tags=["entity-permissions"]) # Rate limits for permission changes (prevent abuse/DoS) _PERM_RATE_LIMIT_MAX = 50 # max changes per minute _PERM_RATE_LIMIT_WINDOW = 60 # 60 seconds @router.get("/{entity_type}/{entity_id}") async def list_entity_permissions( entity_type: str, entity_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """List all permission entries for a specific entity.""" tenant_id = uuid.UUID(current_user["tenant_id"]) try: items = await entity_permission_service.list_permissions( db, tenant_id, entity_type, entity_id ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) from e return {"items": items, "total": len(items)} @router.post("/{entity_type}/{entity_id}", status_code=status.HTTP_201_CREATED) async def create_entity_permission( entity_type: str, entity_id: str, body: EntityPermissionCreate, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), redis = Depends(get_redis_dep), ): """Grant or update a permission on any entity for a user, group, role, or guest.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) is_system_admin = current_user.get("is_system_admin", False) # Permission check: only owner/admin/system_admin can manage permissions if not is_system_admin: access = await entity_permission_service.get_effective_access( db, tenant_id, user_id, entity_type, uuid.UUID(entity_id) ) if access not in ("owner", "admin", "delete"): raise HTTPException(status_code=403, detail="Sie benötigen Admin-Rechte auf diesen Datensatz, um Berechtigungen zu verwalten") # Rate limit: max 50 permission changes per minute per user await check_rate_limit( f"perm_change:{user_id}", _PERM_RATE_LIMIT_MAX, _PERM_RATE_LIMIT_WINDOW, ) try: result = await entity_permission_service.create_permission( db, tenant_id, entity_type, entity_id, body.principal_type, body.principal_id, body.permission_level, body.expires_at, created_by=user_id, redis=redis, ) return result except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) from e async def _check_entity_ownership( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, entity_type: str, entity_id: str, is_system_admin: bool, ) -> None: """Verify that the current user owns the entity or is system admin. Raises 403 if the user is neither owner nor system admin. """ if is_system_admin: return from sqlalchemy import select from app.services.entity_permission_service import ENTITY_MODELS model_info = ENTITY_MODELS.get(entity_type) if model_info is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"detail": f"Unknown entity type: {entity_type}", "code": "not_found"}, ) model = model_info["model"] try: eid = uuid.UUID(entity_id) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"detail": "Invalid entity_id", "code": "invalid_id"}, ) from None result = await db.execute( select(model.owner_id).where( model.id == eid, model.tenant_id == tenant_id, ) ) row = result.first() if row is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"detail": "Entity not found", "code": "not_found"}, ) owner_id = row[0] if owner_id is not None and owner_id != user_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={"detail": "Only the entity owner or system admin can modify permissions", "code": "forbidden"}, ) @router.put("/{entity_type}/{entity_id}/{permission_id}") async def update_entity_permission( entity_type: str, entity_id: str, permission_id: str, body: EntityPermissionUpdate, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Update an existing permission entry.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) await check_rate_limit( f"perm_change:{user_id}", _PERM_RATE_LIMIT_MAX, _PERM_RATE_LIMIT_WINDOW, ) # Ownership check: only owner or system admin can update permissions await _check_entity_ownership( db, tenant_id, user_id, entity_type, entity_id, current_user.get("is_system_admin", False), ) try: return await entity_permission_service.update_permission( db, tenant_id, permission_id, body.permission_level, body.expires_at, ) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) from e @router.delete("/{entity_type}/{entity_id}/{permission_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_entity_permission( entity_type: str, entity_id: str, permission_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Revoke a permission entry.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) await check_rate_limit( f"perm_change:{user_id}", _PERM_RATE_LIMIT_MAX, _PERM_RATE_LIMIT_WINDOW, ) try: await entity_permission_service.delete_permission(db, tenant_id, permission_id) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) from e @router.get("/{entity_type}/{entity_id}/access") async def get_entity_access( entity_type: str, entity_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Get effective access level for the current user on an entity.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) access = await entity_permission_service.get_effective_access( db, tenant_id, user_id, entity_type, uuid.UUID(entity_id) ) is_owner = access == "owner" is_shared = access not in ("none", "owner") return { "entity_type": entity_type, "entity_id": entity_id, "access_level": access, "is_owner": is_owner, "is_shared": is_shared, } @router.get("/all") async def list_all_permissions( db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("settings:read")), ): """List ALL permission entries for the current tenant.""" tenant_id = uuid.UUID(current_user["tenant_id"]) items = await entity_permission_service.list_all_permissions(db, tenant_id) return {"items": items, "total": len(items)} @router.get("/registry") async def list_entity_registry( current_user: dict = Depends(get_current_user), ): """List all registered entity types that support permissions.""" # Static list for now — will be dynamic from Permission Registry in Sprint 4 entity_types = [ {"entity_type": "contact", "label": "Kontakte", "table": "contacts"}, {"entity_type": "contact_folder", "label": "Ordner", "table": "contact_folders"}, {"entity_type": "address", "label": "Adressen", "table": "addresses"}, {"entity_type": "attachment", "label": "Anhänge", "table": "attachments"}, {"entity_type": "bank_account", "label": "Bankkonten", "table": "bank_accounts"}, {"entity_type": "workflow", "label": "Workflows", "table": "workflows"}, {"entity_type": "sequence", "label": "Sequenzen", "table": "sequences"}, {"entity_type": "saved_filter", "label": "Gespeicherte Filter", "table": "saved_filters"}, {"entity_type": "saved_view", "label": "Gespeicherte Ansichten", "table": "saved_views"}, {"entity_type": "webhook", "label": "Webhooks", "table": "webhooks"}, {"entity_type": "notification", "label": "Benachrichtigungen", "table": "notifications"}, {"entity_type": "custom_field_definition", "label": "Custom Fields", "table": "custom_field_definitions"}, {"entity_type": "ai_conversation", "label": "AI Konversationen", "table": "ai_conversations"}, ] return {"items": entity_types, "total": len(entity_types)} @router.post("/bulk", status_code=status.HTTP_201_CREATED) async def bulk_share_permissions( body: dict, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("settings:write")), ): """Bulk share multiple entities with a principal.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) try: result = await bulk_permission_service.bulk_share( db, tenant_id, body["entity_type"], body["entity_ids"], body["principal_type"], body["principal_id"], body["level"], created_by=user_id, ) return result except (ValueError, KeyError) as e: raise HTTPException(status_code=400, detail=str(e)) from e @router.post("/bulk/unshare", status_code=status.HTTP_200_OK) async def bulk_unshare_permissions( body: dict, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("settings:write")), ): """Bulk remove permissions for a principal from multiple entities.""" tenant_id = uuid.UUID(current_user["tenant_id"]) try: result = await bulk_permission_service.bulk_unshare( db, tenant_id, body["entity_type"], body["entity_ids"], body["principal_type"], body["principal_id"], ) return result except (ValueError, KeyError) as e: raise HTTPException(status_code=400, detail=str(e)) from e @router.get("/analytics") async def get_permission_analytics( db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("settings:read")), ): """Get permission analytics for the current tenant.""" tenant_id = uuid.UUID(current_user["tenant_id"]) try: result = await entity_permission_service.get_permission_analytics(db, tenant_id) return result except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) from e