use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use serde::Deserialize;
use vta_sdk::protocols::acl_management::{create::CreateAclResultBody, list::ListAclResultBody};
use crate::acl::Role;
use crate::auth::{AdminAuth, AuthClaims, ManageAuth};
use crate::error::AppError;
use crate::operations;
use crate::server::AppState;
use crate::trust_tasks::{AclChangeRoleOp, AclGrantOp, AclRevokeOp, AclSwapKeyOp, RequireStepUp};
#[derive(Debug, Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
#[into_params(parameter_in = Query)]
pub struct ListAclQuery {
pub context: Option<String>,
}
#[utoipa::path(
get, path = "/acl", tag = "acl",
security(("bearer_jwt" = [])),
params(ListAclQuery),
responses(
(status = 200, description = "ACL entries", body = ListAclResultBody),
(status = 401, description = "Missing or invalid bearer token"),
(status = 403, description = "Caller cannot manage ACL entries"),
),
)]
pub async fn list_acl(
auth: ManageAuth,
State(state): State<AppState>,
Query(query): Query<ListAclQuery>,
) -> Result<Json<ListAclResultBody>, AppError> {
let result =
operations::acl::list_acl(&state.acl_ks, &auth.0, query.context.as_deref(), "rest").await?;
Ok(Json(result))
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct CreateAclRequest {
pub did: String,
pub role: Role,
pub label: Option<String>,
#[serde(default)]
pub allowed_contexts: Vec<String>,
#[serde(default)]
pub expires_at: Option<u64>,
#[serde(default)]
pub step_up_approver: Option<String>,
#[serde(default)]
pub step_up_require: Option<String>,
}
#[utoipa::path(
post, path = "/acl", tag = "acl",
security(("bearer_jwt" = [])),
request_body = CreateAclRequest,
responses(
(status = 201, description = "ACL entry created", body = CreateAclResultBody),
(status = 401, description = "Missing or invalid bearer token"),
(status = 403, description = "Caller cannot manage ACL entries"),
),
)]
pub async fn create_acl(
auth: ManageAuth,
_step_up: RequireStepUp<AclGrantOp>,
State(state): State<AppState>,
Json(req): Json<CreateAclRequest>,
) -> Result<(StatusCode, Json<CreateAclResultBody>), AppError> {
let result = operations::acl::create_acl(
&state.acl_ks,
&state.audit_ks,
&state.contexts_ks,
&auth.0,
&req.did,
req.role,
req.label,
req.allowed_contexts,
req.expires_at,
req.step_up_approver,
req.step_up_require,
"rest",
)
.await?;
Ok((StatusCode::CREATED, Json(result)))
}
#[utoipa::path(
get, path = "/acl/{did}", tag = "acl",
security(("bearer_jwt" = [])),
params(("did" = String, Path, description = "Subject DID")),
responses(
(status = 200, description = "ACL entry", body = CreateAclResultBody),
(status = 401, description = "Missing or invalid bearer token"),
(status = 403, description = "Caller cannot manage ACL entries"),
(status = 404, description = "ACL entry not found"),
),
)]
pub async fn get_acl(
auth: ManageAuth,
State(state): State<AppState>,
Path(did): Path<String>,
) -> Result<Json<CreateAclResultBody>, AppError> {
let result = operations::acl::get_acl(&state.acl_ks, &auth.0, &did, "rest").await?;
Ok(Json(result))
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct UpdateAclRequest {
pub role: Option<Role>,
pub label: Option<String>,
pub allowed_contexts: Option<Vec<String>>,
#[serde(default)]
pub step_up_approver: Option<String>,
#[serde(default)]
pub step_up_require: Option<String>,
}
#[utoipa::path(
patch, path = "/acl/{did}", tag = "acl",
security(("bearer_jwt" = [])),
params(("did" = String, Path, description = "Subject DID")),
request_body = UpdateAclRequest,
responses(
(status = 200, description = "ACL entry updated", body = CreateAclResultBody),
(status = 401, description = "Missing or invalid bearer token"),
(status = 403, description = "Caller is not an admin"),
(status = 404, description = "ACL entry not found"),
),
)]
pub async fn update_acl(
auth: AdminAuth,
_step_up: RequireStepUp<AclChangeRoleOp>,
State(state): State<AppState>,
Path(did): Path<String>,
Json(req): Json<UpdateAclRequest>,
) -> Result<Json<CreateAclResultBody>, AppError> {
let result = operations::acl::update_acl(
&state.acl_ks,
&state.audit_ks,
&state.contexts_ks,
&auth.0,
&did,
operations::acl::UpdateAclParams {
role: req.role,
label: req.label,
allowed_contexts: req.allowed_contexts,
step_up_approver: req.step_up_approver,
step_up_require: req.step_up_require,
},
"rest",
)
.await?;
Ok(Json(result))
}
#[utoipa::path(
delete, path = "/acl/{did}", tag = "acl",
security(("bearer_jwt" = [])),
params(("did" = String, Path, description = "Subject DID")),
responses(
(status = 204, description = "ACL entry removed"),
(status = 401, description = "Missing or invalid bearer token"),
(status = 403, description = "Caller cannot manage ACL entries"),
(status = 404, description = "ACL entry not found"),
),
)]
pub async fn delete_acl(
auth: ManageAuth,
_step_up: RequireStepUp<AclRevokeOp>,
State(state): State<AppState>,
Path(did): Path<String>,
) -> Result<StatusCode, AppError> {
operations::acl::delete_acl(&state.acl_ks, &state.audit_ks, &auth.0, &did, "rest").await?;
Ok(StatusCode::NO_CONTENT)
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
#[derive(utoipa::ToSchema)]
pub enum SwapAclRequest {
Canonical {
#[serde(alias = "current_subject")]
current_subject: String,
#[serde(alias = "new_subject")]
new_subject: String,
#[serde(alias = "link_proof")]
link_proof: String,
#[serde(default)]
#[allow(dead_code)]
reason: Option<String>,
},
Legacy {
presentation: String,
},
}
#[utoipa::path(
post, path = "/acl/swap", tag = "acl",
security(("bearer_jwt" = [])),
request_body = SwapAclRequest,
responses(
(status = 200, description = "ACL entry swapped onto the new DID", body = CreateAclResultBody),
(status = 401, description = "Missing or invalid bearer token"),
),
)]
pub async fn swap_acl(
auth: AuthClaims,
_step_up: RequireStepUp<AclSwapKeyOp>,
State(state): State<AppState>,
Json(req): Json<SwapAclRequest>,
) -> Result<Json<CreateAclResultBody>, AppError> {
let (presentation, claimed_new_subject) = match req {
SwapAclRequest::Canonical {
current_subject,
new_subject,
link_proof,
reason: _,
} => {
if current_subject != auth.did {
return Err(AppError::Validation(format!(
"acl/swap-key: currentSubject {} does not equal authenticated caller {}",
current_subject, auth.did
)));
}
(link_proof, Some(new_subject))
}
SwapAclRequest::Legacy { presentation } => (presentation, None),
};
let did_resolver = state
.did_resolver
.as_ref()
.ok_or_else(|| AppError::Internal("DID resolver not available".into()))?;
let vta_did = {
let config = state.config.read().await;
config
.vta_did
.clone()
.ok_or_else(|| AppError::Internal("VTA DID not configured".into()))?
};
let result = operations::acl::swap_acl(
&state.acl_ks,
&state.audit_ks,
&auth,
&presentation,
did_resolver,
&vta_did,
"rest",
)
.await?;
if let Some(claimed) = claimed_new_subject
&& claimed != result.did
{
return Err(AppError::Validation(format!(
"acl/swap-key: newSubject {} does not match verified VP holder {}",
claimed, result.did
)));
}
Ok(Json(result))
}