from __future__ import annotations import uuid from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.api_endpoint_permission import ApiEndpointPermission from app.models.study import Study from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, OPERATION_PREREQUISITES, PROJECT_PERMISSION_ROLES from app.core.permission_cache import get_permission_cache async def _get_project_permission_overrides( db: AsyncSession, study_id: uuid.UUID, ) -> dict[str, dict[str, bool]]: cache = get_permission_cache() cached_permissions = cache.get_project_permissions(study_id) from app.core.permission_monitor import get_permission_monitor monitor = get_permission_monitor() if cached_permissions is not None: monitor.record_cache_hit() return cached_permissions monitor.record_cache_miss() result = await db.execute( select(ApiEndpointPermission).where( ApiEndpointPermission.study_id == study_id, ) ) rows = result.scalars().all() permissions: dict[str, dict[str, bool]] = {} for row in rows: permissions.setdefault(row.role, {})[row.endpoint_key] = row.allowed cache.set_project_permissions(study_id, permissions) return permissions async def role_has_api_permission( db: AsyncSession, study_id: uuid.UUID, role: str | None, endpoint_key: str, check_prerequisites: bool = True, ) -> bool: """检查角色是否有权访问特定接口""" if endpoint_key not in API_ENDPOINT_PERMISSIONS: return False if role == "ADMIN": return True permissions = await _get_project_permission_overrides(db, study_id) if role == "PM": override = permissions.get("PM", {}).get(endpoint_key) if override is not None: return override return True allowed = permissions.get(role or "", {}).get(endpoint_key) if allowed is None: config = API_ENDPOINT_PERMISSIONS.get(endpoint_key) allowed = bool(config and role in config.get("default_roles", [])) if not allowed: return False if check_prerequisites: prerequisites = OPERATION_PREREQUISITES.get(endpoint_key, []) for prereq_endpoint in prerequisites: has_prereq = await role_has_api_permission( db, study_id, role, prereq_endpoint, check_prerequisites=False ) if not has_prereq: return False return True async def get_missing_prerequisites( db: AsyncSession, study_id: uuid.UUID, role: str | None, endpoint_key: str, ) -> list[str]: """获取缺失的前置权限列表""" if role == "ADMIN": return [] missing = [] prerequisites = OPERATION_PREREQUISITES.get(endpoint_key, []) for prereq_endpoint in prerequisites: has_prereq = await role_has_api_permission( db, study_id, role, prereq_endpoint, check_prerequisites=False ) if not has_prereq: missing.append(prereq_endpoint) return missing async def get_api_endpoint_permissions( db: AsyncSession, study_id: uuid.UUID, ) -> dict[str, dict[str, dict[str, bool]]]: """获取项目的接口级权限矩阵 返回格式: {role: {endpoint_key: {allowed: bool}}} """ overrides = await _get_project_permission_overrides(db, study_id) study_result = await db.execute(select(Study).where(Study.id == study_id)) study = study_result.scalar_one_or_none() active_roles = [role for role in (study.active_roles if study else []) if isinstance(role, str) and role.strip()] roles = list(dict.fromkeys([*PROJECT_PERMISSION_ROLES, *active_roles, *overrides.keys()])) matrix: dict[str, dict[str, dict[str, bool]]] = {} for role in roles: matrix[role] = {} for endpoint_key, config in API_ENDPOINT_PERMISSIONS.items(): if role == "PM": matrix[role][endpoint_key] = {"allowed": True} else: default_allowed = role in config.get("default_roles", []) matrix[role][endpoint_key] = {"allowed": default_allowed} for role, endpoints in overrides.items(): if role not in matrix: matrix[role] = {} for endpoint_key, allowed in endpoints.items(): if endpoint_key not in API_ENDPOINT_PERMISSIONS: continue matrix[role][endpoint_key] = {"allowed": allowed} return matrix async def replace_api_endpoint_permissions( db: AsyncSession, study_id: uuid.UUID, payload: dict[str, dict[str, bool]], ) -> dict[str, dict[str, dict[str, bool]]]: """更新 payload 中指定的角色权限项。未提交的权限项保持不变。 ADMIN 与 PM 权限不会被持久化:ADMIN 始终拥有全部权限,PM 默认拥有 全部项目权限,应当通过专门的渠道而不是项目权限矩阵调整。 """ for role, endpoints in payload.items(): if role in ("ADMIN", "PM"): continue for endpoint_key, allowed in endpoints.items(): if endpoint_key not in API_ENDPOINT_PERMISSIONS: continue result = await db.execute( select(ApiEndpointPermission).where( ApiEndpointPermission.study_id == study_id, ApiEndpointPermission.role == role, ApiEndpointPermission.endpoint_key == endpoint_key, ) ) existing = result.scalar_one_or_none() if existing: existing.allowed = allowed else: db.add( ApiEndpointPermission( study_id=study_id, role=role, endpoint_key=endpoint_key, allowed=allowed, ) ) await db.commit() cache = get_permission_cache() cache.invalidate_project_permissions(study_id) cache.invalidate_all_member_roles(study_id) from app.core.permission_monitor import get_permission_monitor get_permission_monitor().record_cache_invalidation() return await get_api_endpoint_permissions(db, study_id)