use serde_json::Value;
use trust_tasks_rs::specs::vtc::auth::admin_session::v0_1 as admin_session;
use trust_tasks_rs::{Payload, StandardCode, TrustTask, TrustTaskCode};
use super::helpers::{
TrustTaskOutcome, app_error_to_reject, reject_with, reject_with_code, success_response,
task_error_to_reject,
};
use super::{JoinAuthCtx, parse_spec_payload};
use crate::member_portal::oob::HttpContext;
use crate::routes::auth::{admin_session_cookies, decode_admin_session_token};
use crate::server::AppState;
pub(crate) const ADMIN_SESSION_TYPE: &str = <admin_session::Payload as Payload>::TYPE_URI;
pub(crate) const URIS: &[&str] = &[ADMIN_SESSION_TYPE];
pub(super) async fn dispatch(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
type_uri: &str,
) -> Option<TrustTaskOutcome> {
if type_uri != ADMIN_SESSION_TYPE {
return None;
}
Some(handle(state, ctx, doc).await)
}
async fn handle(state: &AppState, ctx: &JoinAuthCtx, doc: TrustTask<Value>) -> TrustTaskOutcome {
let Some(signer) = ctx.verified_signer.clone() else {
return reject_with(&doc, trust_tasks_rs::RejectReason::ProofRequired);
};
let Some(http) = HttpContext::current() else {
return reject_with_code(
&doc,
TrustTaskCode::Standard(StandardCode::PermissionDenied),
"vtc/auth/admin-session is served only over HTTPS, where the session is set as cookies",
None,
);
};
let payload: admin_session::Payload = match parse_spec_payload(&doc) {
Ok(p) => p,
Err(reject) => return reject,
};
let access_token = payload.access_token.as_str();
let claims = match decode_admin_session_token(state, access_token) {
Ok(c) => c,
Err(e) => return task_error_to_reject(&doc, &e),
};
let as_subject = match signer_speaks_for(state, &signer, &claims.sub).await {
Ok(Some(as_subject)) => as_subject,
Ok(None) => {
tracing::warn!(
signer = %signer,
subject = %claims.sub,
security_alert = true,
"admin-session refused: the document's signer is not the access token's subject"
);
return reject_with_code(
&doc,
TrustTaskCode::Standard(StandardCode::PermissionDenied),
"the document must be signed by the access token's subject, or by a console key it enrolled",
None,
);
}
Err(e) => return app_error_to_reject(&doc, &e),
};
let session_key = match crate::routes::auth::session_key_from_ext(doc.payload.get("ext")) {
Ok(k) => k,
Err(e) => return app_error_to_reject(&doc, &e),
};
if let Some(key) = session_key
&& let Err(reject) =
bind_session_key(state, &doc, &claims.session_id, &key, as_subject).await
{
return reject;
}
let refresh_token = crate::routes::auth::refresh_token_from_ext(doc.payload.get("ext"));
let (response, cookies) =
admin_session_cookies(state, access_token, &claims, refresh_token).await;
http.set_cookies(cookies);
success_response(&doc, response)
}
async fn bind_session_key(
state: &AppState,
doc: &TrustTask<Value>,
session_id: &str,
key: &str,
as_subject: bool,
) -> Result<(), TrustTaskOutcome> {
let mut held_by_another = false;
let updated =
vti_common::auth::session::update_session_with(&state.sessions_ks, session_id, |s| match s
.session_pubkey_b58btc
.as_deref()
{
Some(bound) if bound == key => false,
Some(_) if !as_subject => {
held_by_another = true;
false
}
_ => {
held_by_another = false;
s.session_pubkey_b58btc = Some(key.to_string());
true
}
})
.await
.map_err(|e| app_error_to_reject(doc, &e))?;
match updated {
None => Err(reject_with_code(
doc,
TrustTaskCode::Standard(StandardCode::PermissionDenied),
"the access token's session has ended",
None,
)),
Some(_) if held_by_another => Err(reject_with_code(
doc,
TrustTaskCode::Standard(StandardCode::PermissionDenied),
"this session is bound to another key; only its subject may rebind it",
None,
)),
Some(_) => Ok(()),
}
}
async fn signer_speaks_for(
state: &AppState,
signer: &str,
subject: &str,
) -> Result<Option<bool>, crate::error::AppError> {
if signer == subject {
return Ok(Some(true));
}
if crate::acl::get_acl_entry(&state.acl_ks, signer)
.await?
.is_some()
{
return Ok(None);
}
Ok(
crate::acl::console_key::resolve_delegated_admin(&state.console_keys_ks, signer)
.await?
.is_some_and(|d| d.admin_did == subject)
.then_some(false),
)
}