2026-08-03 09:34:08 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-08-21 09:49:48 +08:00
|
|
|
import json
|
2026-08-12 15:21:23 +08:00
|
|
|
from fastapi import APIRouter, Body, 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_tenant_admin,
|
|
|
|
|
is_admin,
|
|
|
|
|
resource_in_user_tenant,
|
|
|
|
|
resource_record,
|
|
|
|
|
)
|
2026-08-03 09:34:08 +08:00
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/approvals", tags=["approval"])
|
|
|
|
|
|
2026-08-21 09:49:48 +08:00
|
|
|
# Approval actions are deliberately finite. A caller must not be able to
|
|
|
|
|
# create an arbitrary approval that no executor or audit policy understands.
|
|
|
|
|
ALLOWED_APPROVAL_ACTIONS = {
|
|
|
|
|
"resource.access",
|
|
|
|
|
"gpu.assign",
|
|
|
|
|
"tenant.quota.update",
|
|
|
|
|
"tenant.member.add",
|
|
|
|
|
"tenant.member.remove",
|
|
|
|
|
"model.use",
|
|
|
|
|
"dataset.use",
|
|
|
|
|
"dataset.delete",
|
|
|
|
|
"trained_model.merge",
|
|
|
|
|
"trained_model.export",
|
|
|
|
|
"trained_model.delete",
|
|
|
|
|
"fine_tune.stop",
|
|
|
|
|
"fine_tune.delete",
|
|
|
|
|
"eval.delete",
|
|
|
|
|
"inference.delete",
|
|
|
|
|
"project.archive",
|
|
|
|
|
"project.delete",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _available_gpu_options() -> list[dict[str, Any]]:
|
|
|
|
|
"""Return only online nodes and currently unassigned, idle GPUs."""
|
|
|
|
|
store = get_platform_store()
|
|
|
|
|
nodes = store.compute_nodes()
|
|
|
|
|
gpus = store.gpus()
|
|
|
|
|
assigned = {(str(item.get("node_id")), int(item.get("gpu_index"))) for item in store.gpu_assignments()}
|
|
|
|
|
by_node: dict[str, list[dict[str, Any]]] = {}
|
|
|
|
|
for gpu in gpus:
|
|
|
|
|
node_id = str(gpu.get("node_id") or "")
|
|
|
|
|
index = int(gpu.get("id") or 0)
|
|
|
|
|
if gpu.get("status") != "idle" or (node_id, index) in assigned:
|
|
|
|
|
continue
|
|
|
|
|
by_node.setdefault(node_id, []).append({
|
|
|
|
|
"index": index,
|
|
|
|
|
"name": gpu.get("name") or "GPU",
|
|
|
|
|
"memory_total_gb": float(gpu.get("memory_total_gb") or 0),
|
|
|
|
|
})
|
|
|
|
|
result = []
|
|
|
|
|
for node in nodes:
|
|
|
|
|
node_id = str(node.get("id") or "")
|
|
|
|
|
if not node.get("enabled") or node.get("scheduler_status") != "online" or not by_node.get(node_id):
|
|
|
|
|
continue
|
|
|
|
|
result.append({
|
|
|
|
|
"id": node_id,
|
|
|
|
|
"code": node.get("code") or node_id,
|
|
|
|
|
"name": node.get("name") or node.get("code") or node_id,
|
|
|
|
|
"gpus": sorted(by_node[node_id], key=lambda item: item["index"]),
|
|
|
|
|
})
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _validate_gpu_request(assignments: Any) -> list[dict[str, Any]]:
|
|
|
|
|
if not isinstance(assignments, list) or not assignments:
|
|
|
|
|
raise fail(400, "assignments 不能为空")
|
|
|
|
|
available = {
|
|
|
|
|
(node["id"], gpu["index"])
|
|
|
|
|
for node in _available_gpu_options()
|
|
|
|
|
for gpu in node["gpus"]
|
|
|
|
|
}
|
|
|
|
|
normalized = []
|
|
|
|
|
seen: set[tuple[str, int]] = set()
|
|
|
|
|
for item in assignments:
|
|
|
|
|
if not isinstance(item, dict) or not item.get("node_id") or item.get("gpu_index") is None:
|
|
|
|
|
raise fail(400, "每项必须包含 node_id 和 gpu_index")
|
|
|
|
|
try:
|
|
|
|
|
key = (str(item["node_id"]), int(item["gpu_index"]))
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
raise fail(400, "gpu_index 必须是整数")
|
|
|
|
|
if key not in available:
|
|
|
|
|
raise fail(409, f"GPU {key[0]}:{key[1]} 当前不可申请")
|
|
|
|
|
if key not in seen:
|
|
|
|
|
seen.add(key)
|
|
|
|
|
normalized.append({"node_id": key[0], "gpu_index": key[1]})
|
|
|
|
|
return normalized
|
|
|
|
|
|
2026-08-03 09:34:08 +08:00
|
|
|
|
|
|
|
|
@router.get("/templates")
|
2026-08-12 15:21:23 +08:00
|
|
|
def list_templates(current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
2026-08-21 09:49:48 +08:00
|
|
|
if not is_admin(current_user):
|
|
|
|
|
raise fail(403, "admin permission required")
|
2026-08-03 09:34:08 +08:00
|
|
|
return ok(get_platform_store().approval_templates())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/templates")
|
2026-08-12 15:21:23 +08:00
|
|
|
def create_template(payload: dict[str, Any] = Body(...), current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
|
|
|
|
if not is_admin(current_user): raise fail(403, "admin permission required")
|
2026-08-03 09:34:08 +08:00
|
|
|
if not payload.get("name"):
|
|
|
|
|
raise fail(400, "name 必填")
|
2026-08-21 09:49:48 +08:00
|
|
|
steps = payload.get("steps") or []
|
|
|
|
|
if not isinstance(steps, list) or any(not isinstance(step, dict) for step in steps):
|
|
|
|
|
raise fail(400, "steps 格式无效")
|
|
|
|
|
if payload.get("action") and payload["action"] not in ALLOWED_APPROVAL_ACTIONS:
|
|
|
|
|
raise fail(400, "不支持的审批动作")
|
|
|
|
|
payload = {
|
|
|
|
|
**payload,
|
|
|
|
|
"created_by": current_user.get("id"),
|
|
|
|
|
"tenant_id": payload.get("tenant_id") or current_user.get("tenant_id") or "default",
|
|
|
|
|
"scope": payload.get("scope") or "tenant",
|
|
|
|
|
"status": payload.get("status") or "active",
|
|
|
|
|
}
|
|
|
|
|
template = get_platform_store().create_approval_template(payload)
|
|
|
|
|
get_platform_store().record_audit(
|
|
|
|
|
action="approval.template.create", actor_id=current_user.get("id"),
|
|
|
|
|
target_type="approval_template", target_id=template["id"],
|
|
|
|
|
tenant_id=template.get("tenant_id"),
|
|
|
|
|
)
|
|
|
|
|
return ok(template)
|
2026-08-03 09:34:08 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/templates/{template_id}")
|
2026-08-21 09:49:48 +08:00
|
|
|
def get_template(template_id: str, current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
|
|
|
|
if not is_admin(current_user):
|
|
|
|
|
raise fail(403, "admin permission required")
|
2026-08-03 09:34:08 +08:00
|
|
|
try:
|
|
|
|
|
return ok(get_platform_store().approval_template(template_id))
|
|
|
|
|
except KeyError:
|
|
|
|
|
raise fail(404, "template not found")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/templates/{template_id}")
|
2026-08-12 15:21:23 +08:00
|
|
|
def update_template(template_id: str, payload: dict[str, Any] = Body(...), current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
|
|
|
|
if not is_admin(current_user): raise fail(403, "admin permission required")
|
2026-08-21 09:49:48 +08:00
|
|
|
if payload.get("action") and payload["action"] not in ALLOWED_APPROVAL_ACTIONS:
|
|
|
|
|
raise fail(400, "不支持的审批动作")
|
2026-08-03 09:34:08 +08:00
|
|
|
try:
|
2026-08-21 09:49:48 +08:00
|
|
|
template = get_platform_store().update_approval_template(template_id, payload)
|
|
|
|
|
get_platform_store().record_audit(
|
|
|
|
|
action="approval.template.update", actor_id=current_user.get("id"),
|
|
|
|
|
target_type="approval_template", target_id=template_id,
|
|
|
|
|
tenant_id=template.get("tenant_id"), detail=f"fields={','.join(payload.keys())}",
|
|
|
|
|
)
|
|
|
|
|
return ok(template)
|
2026-08-03 09:34:08 +08:00
|
|
|
except KeyError:
|
|
|
|
|
raise fail(404, "template not found")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/templates/{template_id}")
|
2026-08-12 15:21:23 +08:00
|
|
|
def delete_template(template_id: str, current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
|
|
|
|
if not is_admin(current_user): raise fail(403, "admin permission required")
|
2026-08-03 09:34:08 +08:00
|
|
|
try:
|
2026-08-21 09:49:48 +08:00
|
|
|
template = get_platform_store().delete_approval_template(template_id)
|
|
|
|
|
get_platform_store().record_audit(
|
|
|
|
|
action="approval.template.delete", actor_id=current_user.get("id"),
|
|
|
|
|
target_type="approval_template", target_id=template_id,
|
|
|
|
|
tenant_id=template.get("tenant_id"),
|
|
|
|
|
)
|
|
|
|
|
return ok(template)
|
2026-08-03 09:34:08 +08:00
|
|
|
except KeyError:
|
|
|
|
|
raise fail(404, "template not found")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("")
|
2026-08-21 09:49:48 +08:00
|
|
|
def list_instances(
|
|
|
|
|
status: str | None = None,
|
|
|
|
|
mine: bool = False,
|
|
|
|
|
current_user: dict = Depends(get_current_user),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
user_id = current_user.get("id")
|
|
|
|
|
items = get_platform_store().approval_instances(
|
|
|
|
|
status=status,
|
|
|
|
|
applicant_id=user_id if mine and not is_admin(current_user) else None,
|
|
|
|
|
)
|
|
|
|
|
if is_admin(current_user):
|
|
|
|
|
return ok(items)
|
|
|
|
|
if mine:
|
|
|
|
|
return ok(items)
|
|
|
|
|
visible = []
|
|
|
|
|
for item in items:
|
|
|
|
|
if item.get("applicant_id") == user_id:
|
|
|
|
|
visible.append(item)
|
|
|
|
|
continue
|
|
|
|
|
if any(step.get("approver_id") == user_id and step.get("status") == "pending" for step in item.get("steps", [])):
|
|
|
|
|
visible.append(item)
|
|
|
|
|
continue
|
|
|
|
|
if is_tenant_admin(current_user, item.get("tenant_id")) and item.get("status") == "pending":
|
|
|
|
|
visible.append(item)
|
|
|
|
|
return ok(visible)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/gpu-options")
|
|
|
|
|
def gpu_request_options(current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
|
|
|
|
"""Self-service GPU options; does not expose the admin compute page."""
|
|
|
|
|
return ok({"nodes": _available_gpu_options()})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/gpu-requests")
|
|
|
|
|
def create_gpu_request(payload: dict[str, Any] = Body(...), current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
|
|
|
|
assignments = _validate_gpu_request(payload.get("assignments"))
|
|
|
|
|
user_id = str(current_user.get("id") or "")
|
|
|
|
|
if is_admin(current_user):
|
|
|
|
|
try:
|
|
|
|
|
return ok({"approval_required": False, "assignments": get_platform_store().assign_gpus(
|
|
|
|
|
[{**item, "user_id": user_id} for item in assignments], assigned_by=user_id,
|
|
|
|
|
)})
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
raise fail(409, str(exc))
|
|
|
|
|
normalized = [{**item, "user_id": user_id} for item in assignments]
|
|
|
|
|
instance = get_platform_store().create_approval_instance({
|
|
|
|
|
"resource_type": "gpu",
|
|
|
|
|
"resource_id": f"batch:{user_id}",
|
|
|
|
|
"applicant_id": user_id,
|
|
|
|
|
"action": "gpu.assign",
|
|
|
|
|
"tenant_id": current_user.get("tenant_id") or "default",
|
|
|
|
|
"reason": json.dumps({"assignments": normalized, "reason": payload.get("reason")}, ensure_ascii=False),
|
|
|
|
|
})
|
|
|
|
|
get_platform_store().record_audit(
|
|
|
|
|
action="gpu.assign.request", actor_id=user_id, target_type="gpu",
|
|
|
|
|
target_id=instance["id"], tenant_id=current_user.get("tenant_id") or "default",
|
|
|
|
|
detail=f"count={len(normalized)}",
|
|
|
|
|
)
|
|
|
|
|
return ok({"approval_required": True, "approval_id": instance["id"], "approval": instance})
|
2026-08-03 09:34:08 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("")
|
2026-08-12 15:21:23 +08:00
|
|
|
def create_instance(payload: dict[str, Any] = Body(...), current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
|
|
|
|
payload["applicant_id"] = current_user.get("id")
|
|
|
|
|
for field in ("resource_type", "resource_id"):
|
2026-08-03 09:34:08 +08:00
|
|
|
if not payload.get(field):
|
|
|
|
|
raise fail(400, f"{field} 必填")
|
2026-08-21 09:49:48 +08:00
|
|
|
resource = resource_record(str(payload["resource_type"]), str(payload["resource_id"]))
|
|
|
|
|
if not resource and not is_admin(current_user):
|
|
|
|
|
raise fail(404, "resource not found")
|
|
|
|
|
action = str(payload.get("action") or "")
|
|
|
|
|
if not action or action not in ALLOWED_APPROVAL_ACTIONS:
|
|
|
|
|
raise fail(400, "不支持的审批动作")
|
|
|
|
|
if action != "resource.access" and not is_admin(current_user) and not has_resource_access(
|
|
|
|
|
str(payload["resource_type"]), str(payload["resource_id"]), current_user, "read"
|
|
|
|
|
):
|
|
|
|
|
raise fail(403, "no permission to request approval for this resource")
|
|
|
|
|
if resource and not is_admin(current_user) and not resource_in_user_tenant(str(payload["resource_type"]), resource, current_user):
|
|
|
|
|
raise fail(403, "resource belongs to another tenant")
|
|
|
|
|
requested = payload.get("requested_permissions") or []
|
|
|
|
|
allowed = {"read", "write", "execute", "download"}
|
|
|
|
|
if any(permission not in allowed for permission in requested):
|
|
|
|
|
raise fail(400, "invalid requested permission")
|
|
|
|
|
payload["tenant_id"] = current_user.get("tenant_id") or "default"
|
|
|
|
|
payload["requested_permissions"] = requested
|
2026-08-03 09:34:08 +08:00
|
|
|
try:
|
|
|
|
|
return ok(get_platform_store().create_approval_instance(payload))
|
|
|
|
|
except KeyError:
|
|
|
|
|
raise fail(404, "template not found")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{instance_id}")
|
2026-08-12 15:21:23 +08:00
|
|
|
def get_instance(instance_id: str, current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
2026-08-03 09:34:08 +08:00
|
|
|
try:
|
2026-08-12 15:21:23 +08:00
|
|
|
item = get_platform_store().approval_instance(instance_id)
|
2026-08-21 09:49:48 +08:00
|
|
|
if not is_admin(current_user) and item.get("applicant_id") != current_user.get("id") and not is_tenant_admin(current_user, item.get("tenant_id")):
|
2026-08-12 15:21:23 +08:00
|
|
|
raise fail(403, "no permission to access approval")
|
|
|
|
|
return ok(item)
|
2026-08-03 09:34:08 +08:00
|
|
|
except KeyError:
|
|
|
|
|
raise fail(404, "instance not found")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{instance_id}/steps/{step_index}/decision")
|
|
|
|
|
def decide(
|
|
|
|
|
instance_id: str,
|
|
|
|
|
step_index: int,
|
|
|
|
|
payload: dict[str, Any] = Body(...),
|
2026-08-21 09:49:48 +08:00
|
|
|
current_user: dict = Depends(get_current_user),
|
2026-08-03 09:34:08 +08:00
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
try:
|
2026-08-21 09:49:48 +08:00
|
|
|
instance = get_platform_store().approval_instance(instance_id)
|
|
|
|
|
if instance.get("applicant_id") == current_user.get("id"):
|
|
|
|
|
raise fail(403, "applicant cannot approve own request")
|
|
|
|
|
step = next((item for item in instance.get("steps", []) if int(item.get("step_index", -1)) == step_index), None)
|
|
|
|
|
if not step and is_admin(current_user) and not instance.get("steps"):
|
|
|
|
|
step = {"approver_id": None, "status": "pending"}
|
|
|
|
|
if not step:
|
|
|
|
|
raise fail(404, "approval step not found")
|
|
|
|
|
designated = step.get("approver_id")
|
|
|
|
|
if not is_admin(current_user) and designated != current_user.get("id") and not (
|
|
|
|
|
step.get("approver_type") == "admin" and is_tenant_admin(current_user, instance.get("tenant_id"))
|
|
|
|
|
):
|
|
|
|
|
raise fail(403, "current user is not the designated approver")
|
|
|
|
|
if instance.get("action") == "tenant.quota.update" and not is_admin(current_user):
|
|
|
|
|
raise fail(403, "only platform administrator can approve tenant quota changes")
|
|
|
|
|
submitted_approver = payload.get("approver_id")
|
|
|
|
|
if submitted_approver and submitted_approver != current_user.get("id"):
|
|
|
|
|
raise fail(403, "approver_id must match current session")
|
|
|
|
|
store = get_platform_store()
|
|
|
|
|
result = store.decide_approval_step(
|
2026-08-03 09:34:08 +08:00
|
|
|
instance_id,
|
|
|
|
|
step_index,
|
2026-08-21 09:49:48 +08:00
|
|
|
approver_id=str(current_user.get("id")),
|
2026-08-03 09:34:08 +08:00
|
|
|
approved=bool(payload.get("approved", False)),
|
|
|
|
|
comment=payload.get("comment"),
|
|
|
|
|
)
|
2026-08-21 09:49:48 +08:00
|
|
|
if result.get("status") == "approved" and result.get("execution_status") == "ready":
|
|
|
|
|
try:
|
|
|
|
|
effect = store.apply_approval_effect(result, str(current_user.get("id") or ""))
|
|
|
|
|
if effect is not None:
|
|
|
|
|
result = store.approval_instance(instance_id)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
with store.connect() as conn:
|
|
|
|
|
conn.execute(
|
|
|
|
|
"UPDATE approval_instances SET execution_status='failed', execution_error=? WHERE id=?",
|
|
|
|
|
(str(exc), instance_id),
|
|
|
|
|
)
|
|
|
|
|
raise fail(409, f"审批已通过,但执行失败:{exc}")
|
|
|
|
|
store.record_audit(
|
|
|
|
|
action="approval.decision", actor_id=current_user.get("id"),
|
|
|
|
|
target_type="approval_instance", target_id=instance_id,
|
|
|
|
|
tenant_id=result.get("tenant_id"), result="success" if payload.get("approved") else "rejected",
|
|
|
|
|
detail=f"step={step_index}", reason=payload.get("comment"),
|
2026-08-03 09:34:08 +08:00
|
|
|
)
|
2026-08-21 09:49:48 +08:00
|
|
|
return ok(result)
|
2026-08-03 09:34:08 +08:00
|
|
|
except (KeyError, ValueError) as e:
|
|
|
|
|
raise fail(400, str(e))
|
2026-08-21 09:49:48 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/resource-access/requests")
|
|
|
|
|
def create_resource_access_request(payload: dict[str, Any] = Body(...), current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
|
|
|
|
resource_type = str(payload.get("resource_type") or "")
|
|
|
|
|
resource_id = str(payload.get("resource_id") or "")
|
|
|
|
|
resource = resource_record(resource_type, resource_id)
|
|
|
|
|
if not resource:
|
|
|
|
|
raise fail(404, "resource not found")
|
|
|
|
|
if not is_admin(current_user) and not resource_in_user_tenant(resource_type, resource, current_user):
|
|
|
|
|
raise fail(403, "resource belongs to another tenant")
|
|
|
|
|
permissions = payload.get("requested_permissions") or ["read"]
|
|
|
|
|
allowed = {"read", "write", "execute", "download"}
|
|
|
|
|
if not permissions or any(permission not in allowed for permission in permissions):
|
|
|
|
|
raise fail(400, "invalid requested permissions")
|
|
|
|
|
result = get_platform_store().create_resource_access_request({
|
|
|
|
|
"resource_type": resource_type,
|
|
|
|
|
"resource_id": resource_id,
|
|
|
|
|
"applicant_id": current_user.get("id"),
|
|
|
|
|
"principal_type": "user",
|
|
|
|
|
"principal_id": current_user.get("id"),
|
|
|
|
|
"requested_permissions": permissions,
|
|
|
|
|
"reason": payload.get("reason"),
|
|
|
|
|
"template_id": payload.get("template_id"),
|
|
|
|
|
"tenant_id": current_user.get("tenant_id") or "default",
|
|
|
|
|
"expires_at": payload.get("expires_at"),
|
|
|
|
|
})
|
|
|
|
|
get_platform_store().record_audit(
|
|
|
|
|
action="resource.access.request", actor_id=current_user.get("id"),
|
|
|
|
|
target_type=resource_type, target_id=resource_id,
|
|
|
|
|
tenant_id=current_user.get("tenant_id") or "default",
|
|
|
|
|
detail=f"permissions={','.join(permissions)}",
|
|
|
|
|
)
|
|
|
|
|
return ok(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/resource-access/requests")
|
|
|
|
|
def list_resource_access_requests(status: str | None = None, current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
|
|
|
|
items = get_platform_store().resource_access_requests(
|
|
|
|
|
user_id=None if is_admin(current_user) else current_user.get("id"),
|
|
|
|
|
status=status,
|
|
|
|
|
)
|
|
|
|
|
return ok(items)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/resource-access/requests/{request_id}/cancel")
|
|
|
|
|
def cancel_resource_access_request(request_id: str, current_user: dict = Depends(get_current_user)) -> dict[str, Any]:
|
|
|
|
|
try:
|
|
|
|
|
item = get_platform_store().cancel_resource_access_request(
|
|
|
|
|
request_id,
|
|
|
|
|
str(current_user.get("id") or ""),
|
|
|
|
|
is_admin_actor=is_admin(current_user),
|
|
|
|
|
)
|
|
|
|
|
except KeyError:
|
|
|
|
|
raise fail(404, "access request not found")
|
|
|
|
|
except PermissionError as exc:
|
|
|
|
|
raise fail(403, str(exc))
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
raise fail(409, str(exc))
|
|
|
|
|
get_platform_store().record_audit(
|
|
|
|
|
action="resource.access.cancel", actor_id=current_user.get("id"),
|
|
|
|
|
target_type="resource_access_request", target_id=request_id,
|
|
|
|
|
tenant_id=item.get("tenant_id"),
|
|
|
|
|
)
|
|
|
|
|
return ok(item)
|