use super::helpers::TrustTaskOutcome;
use serde_json::{Value, json};
use trust_tasks_rs::{RejectReason, TrustTask};
use vta_sdk::protocols::auth::{RevokeSessionRequest, RevokeSessionResponse, epoch_to_rfc3339};
use crate::acl::{Role, check_acl_entry, effective_capabilities};
use crate::audit::audit;
use crate::auth::AuthClaims;
use crate::auth::session::{SessionState, delete_session, get_session, list_sessions, now_epoch};
use crate::server::AppState;
use super::helpers::{app_error_to_reject, reject_with, success_response};
pub(super) async fn handle_revoke_session(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: RevokeSessionRequest = match serde_json::from_value(doc.payload.clone()) {
Ok(r) => r,
Err(e) => {
return reject_with(
&doc,
RejectReason::MalformedRequest {
reason: format!("revoke-session payload parse: {e}"),
},
);
}
};
let session_id = match (&req.session_id, req.all.unwrap_or(false)) {
(Some(id), false) => id.clone(),
(None, true) => {
return reject_with(
&doc,
RejectReason::TaskFailed {
reason: "auth:revoke_all_unsupported — this maintainer revokes one named \
session; resend with `sessionId`"
.to_string(),
details: None,
},
);
}
_ => {
return reject_with(
&doc,
RejectReason::MalformedRequest {
reason: "revoke-session takes exactly one of `sessionId` or `all`".to_string(),
},
);
}
};
let session = match get_session(&state.sessions_ks, &session_id).await {
Ok(s) => s,
Err(e) => {
tracing::error!(error = %e, "session lookup failed in revoke-session");
return reject_with(
&doc,
RejectReason::InternalError {
reason: format!("session lookup: {e}"),
},
);
}
};
if session
.filter(|s| s.did == auth.did || auth.role == Role::Admin)
.is_none()
{
tracing::warn!(
caller = %auth.did,
session_id = %session_id,
"revoke-session: no session revoked (absent, or not the caller's)"
);
audit!(
"session.revoke",
actor = &auth.did,
resource = &session_id,
outcome = "no-op"
);
return success_response(&doc, RevokeSessionResponse { revoked_count: 0 });
}
if let Err(e) = delete_session(&state.sessions_ks, &session_id).await {
tracing::error!(error = %e, session_id = %session_id, "session delete failed");
return reject_with(
&doc,
RejectReason::InternalError {
reason: format!("session delete: {e}"),
},
);
}
audit!(
"session.revoke",
actor = &auth.did,
resource = &session_id,
outcome = "success"
);
if let Err(e) = crate::audit::record_with_detail(
&state.audit_sink,
"session.revoke",
&auth.did,
Some(&session_id),
"success",
Some(super::helpers::TRANSPORT_TRUST_TASK),
None,
None,
)
.await
{
tracing::warn!(error = %e, "audit record failed for session.revoke");
}
tracing::info!(
caller = %auth.did,
session_id = %session_id,
"session revoked via trust-task"
);
success_response(&doc, RevokeSessionResponse { revoked_count: 1 })
}
pub(super) async fn handle_whoami(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let session = match get_session(&state.sessions_ks, &auth.session_id).await {
Ok(Some(s)) => s,
Ok(None) => {
return reject_with(
&doc,
RejectReason::TaskFailed {
reason: format!("session not found: {}", auth.session_id),
details: None,
},
);
}
Err(e) => {
tracing::error!(error = %e, "session lookup failed in whoami");
return reject_with(
&doc,
RejectReason::InternalError {
reason: format!("session lookup: {e}"),
},
);
}
};
let entry = match check_acl_entry(&state.acl_ks, &auth.did).await {
Ok(entry) => entry,
Err(e) => return app_error_to_reject(&doc, e),
};
let (role, contexts) = (entry.role.clone(), entry.allowed_contexts.clone());
let mut session_info = json!({
"id": auth.session_id,
"subject": auth.did,
"issuedAt": epoch_to_rfc3339(session.created_at),
"expiresAt": epoch_to_rfc3339(auth.access_expires_at),
"amr": session.amr,
});
if !session.acr.is_empty() {
session_info["acr"] = Value::String(session.acr.clone());
}
let scopes: Vec<String> = contexts.iter().map(|c| format!("ctx:{c}")).collect();
let capabilities: Vec<String> = effective_capabilities(&entry.role, &entry.capabilities)
.into_iter()
.map(|c| {
serde_json::to_value(c)
.ok()
.and_then(|v| v.as_str().map(str::to_string))
.unwrap_or_else(|| format!("{c:?}"))
})
.collect();
let body = json!({
"session": session_info,
"roles": [role.to_string()],
"scopes": scopes,
"capabilities": capabilities,
});
audit!(
"auth.whoami",
actor = &auth.did,
resource = &auth.session_id,
outcome = "success"
);
success_response(&doc, body)
}
pub(super) async fn handle_sessions_list(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let all = match list_sessions(&state.sessions_ks).await {
Ok(s) => s,
Err(e) => {
tracing::error!(error = %e, "session list failed in sessions/list");
return reject_with(
&doc,
RejectReason::InternalError {
reason: format!("session list: {e}"),
},
);
}
};
let now = now_epoch();
let sessions: Vec<Value> = all
.into_iter()
.filter(|s| {
s.did == auth.did
&& s.state == SessionState::Authenticated
&& s.refresh_expires_at.is_none_or(|exp| exp > now)
})
.map(|s| {
let expires_at = s.refresh_expires_at.unwrap_or(s.created_at);
let mut item = json!({
"id": s.session_id,
"subject": s.did,
"issuedAt": epoch_to_rfc3339(s.created_at),
"expiresAt": epoch_to_rfc3339(expires_at),
"amr": s.amr,
});
if !s.acr.is_empty() {
item["acr"] = Value::String(s.acr);
}
item
})
.collect();
audit!(
"auth.sessions-list",
actor = &auth.did,
resource = &auth.session_id,
outcome = "success"
);
success_response(&doc, json!({ "sessions": sessions }))
}