2026-08-03 09:34:08 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-08-12 15:21:23 +08:00
|
|
|
|
from fastapi import APIRouter, Body, Request, Depends
|
2026-08-03 09:34:08 +08:00
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
from app.api.v1.endpoints.platform import ok, fail
|
|
|
|
|
|
from app.db.platform_store import get_platform_store
|
2026-08-21 09:49:48 +08:00
|
|
|
|
from app.core.auth import (
|
|
|
|
|
|
get_current_user,
|
|
|
|
|
|
has_resource_access,
|
|
|
|
|
|
is_admin,
|
|
|
|
|
|
resource_record,
|
|
|
|
|
|
resource_tenant_id,
|
|
|
|
|
|
user_tenant_ids,
|
|
|
|
|
|
)
|
2026-08-17 16:04:04 +08:00
|
|
|
|
from app.core.audit import audit_log, AuditActions
|
2026-08-03 09:34:08 +08:00
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/resources", tags=["resource"])
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-21 09:49:48 +08:00
|
|
|
|
def _actor(request: Request, current_user: dict[str, Any]) -> str | None:
|
|
|
|
|
|
return str(current_user.get("id") or "") or None
|
2026-08-03 09:34:08 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{resource_type}/{resource_id}/acl")
|
2026-08-12 15:21:23 +08:00
|
|
|
|
def get_acl(resource_type: str, resource_id: str, current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
2026-08-03 09:34:08 +08:00
|
|
|
|
"""查询资源 ACL,返回按主体分组的权限列表。"""
|
2026-08-12 15:21:23 +08:00
|
|
|
|
if not has_resource_access(resource_type, resource_id, current_user, "read"):
|
|
|
|
|
|
raise fail(403, "no permission to access resource ACL")
|
2026-08-03 09:34:08 +08:00
|
|
|
|
return ok(get_platform_store().resource_acl(resource_type, resource_id))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/{resource_type}/{resource_id}/acl")
|
2026-08-17 16:04:04 +08:00
|
|
|
|
@audit_log(
|
|
|
|
|
|
action=AuditActions.GRANT_ACL,
|
|
|
|
|
|
target_type="",
|
|
|
|
|
|
detail_template="设置资源授权: {resource_type}/{resource_id}",
|
|
|
|
|
|
)
|
2026-08-03 09:34:08 +08:00
|
|
|
|
def set_acl(
|
|
|
|
|
|
resource_type: str,
|
|
|
|
|
|
resource_id: str,
|
|
|
|
|
|
payload: dict[str, Any] = Body(...),
|
|
|
|
|
|
request: Request = None,
|
2026-08-12 15:21:23 +08:00
|
|
|
|
current_user: dict = Depends(get_current_user),
|
2026-08-03 09:34:08 +08:00
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""设置资源 ACL,body: { entries: [{ subject_type, subject_id, permissions: [] }] }"""
|
2026-08-21 09:49:48 +08:00
|
|
|
|
resource = resource_record(resource_type, resource_id)
|
|
|
|
|
|
if not resource and not is_admin(current_user):
|
|
|
|
|
|
raise fail(404, "resource not found")
|
2026-08-12 15:21:23 +08:00
|
|
|
|
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")
|
2026-08-03 09:34:08 +08:00
|
|
|
|
entries = payload.get("entries") or []
|
2026-08-12 15:21:23 +08:00
|
|
|
|
allowed = {"read", "write", "execute", "download", "delete", "admin"}
|
2026-08-21 09:49:48 +08:00
|
|
|
|
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)
|
2026-08-12 15:21:23 +08:00
|
|
|
|
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")
|
2026-08-21 09:49:48 +08:00
|
|
|
|
permissions = set(entry.get("permissions") or [])
|
|
|
|
|
|
if any(permission not in allowed for permission in permissions):
|
2026-08-12 15:21:23 +08:00
|
|
|
|
raise fail(400, "invalid ACL permission")
|
2026-08-21 09:49:48 +08:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
2026-08-03 09:34:08 +08:00
|
|
|
|
get_platform_store().record_audit(
|
|
|
|
|
|
action="resource.acl.set",
|
2026-08-21 09:49:48 +08:00
|
|
|
|
actor_id=_actor(request, current_user) if request else current_user.get("id"),
|
2026-08-03 09:34:08 +08:00
|
|
|
|
target_type=resource_type,
|
|
|
|
|
|
target_id=resource_id,
|
|
|
|
|
|
detail=f"entries={len(entries)}",
|
|
|
|
|
|
)
|
|
|
|
|
|
return ok(result)
|