from __future__ import annotations from fastapi import APIRouter, Body, Request, Depends from typing import Any from app.api.v1.endpoints.platform import ok, fail from app.db.platform_store import get_platform_store from app.core.auth import ( get_current_user, has_resource_access, is_admin, resource_record, resource_tenant_id, user_tenant_ids, ) from app.core.audit import audit_log, AuditActions router = APIRouter(prefix="/resources", tags=["resource"]) def _actor(request: Request, current_user: dict[str, Any]) -> str | None: return str(current_user.get("id") or "") or None @router.get("/{resource_type}/{resource_id}/acl") def get_acl(resource_type: str, resource_id: str, current_user: dict = Depends(get_current_user)) -> dict[str, Any]: """查询资源 ACL,返回按主体分组的权限列表。""" if not has_resource_access(resource_type, resource_id, current_user, "read"): raise fail(403, "no permission to access resource ACL") return ok(get_platform_store().resource_acl(resource_type, resource_id)) @router.put("/{resource_type}/{resource_id}/acl") @audit_log( action=AuditActions.GRANT_ACL, target_type="", detail_template="设置资源授权: {resource_type}/{resource_id}", ) def set_acl( resource_type: str, resource_id: str, payload: dict[str, Any] = Body(...), request: Request = None, current_user: dict = Depends(get_current_user), ) -> dict[str, Any]: """设置资源 ACL,body: { entries: [{ subject_type, subject_id, permissions: [] }] }""" resource = resource_record(resource_type, resource_id) if not resource and not is_admin(current_user): raise fail(404, "resource not found") if not is_admin(current_user) and not has_resource_access(resource_type, resource_id, current_user, "write"): raise fail(403, "only resource owner or admin can update ACL") entries = payload.get("entries") or [] allowed = {"read", "write", "execute", "download", "delete", "admin"} owner_allowed = {"read", "write", "execute", "download"} tenant_id = resource_tenant_id(resource_type, resource) if resource else None tenant_ids = user_tenant_ids(current_user) for entry in entries: if entry.get("principal_type") not in {"user", "role"} or not entry.get("principal_id"): raise fail(400, "invalid ACL principal") permissions = set(entry.get("permissions") or []) if any(permission not in allowed for permission in permissions): raise fail(400, "invalid ACL permission") if not is_admin(current_user) and permissions - owner_allowed: raise fail(403, "resource owners cannot grant delete or admin permission") if entry.get("principal_type") == "user": with get_platform_store().connect() as conn: principal = conn.execute( "SELECT id, tenant_id, status FROM users WHERE id=?", (entry["principal_id"],), ).fetchone() if not principal or principal.get("status") != "active": raise fail(400, "ACL user does not exist or is inactive") principal_tenant = str(principal.get("tenant_id") or "default") if not is_admin(current_user) and tenant_id and principal_tenant not in tenant_ids: raise fail(403, "cannot grant resource access across tenants") elif not is_admin(current_user): # Role ACLs are global in the legacy schema and therefore cannot # be safely scoped to one tenant by a normal resource owner. raise fail(403, "only administrators can grant role-based ACLs") result = get_platform_store().set_resource_acl( resource_type, resource_id, entries, granted_by=str(current_user.get("id") or "") or None, ) get_platform_store().record_audit( action="resource.acl.set", actor_id=_actor(request, current_user) if request else current_user.get("id"), target_type=resource_type, target_id=resource_id, detail=f"entries={len(entries)}", ) return ok(result)