use std::sync::Arc;
use crate::acl::{VtcAclEntry, VtcRole, list_acl_entries, store_acl_entry};
use axum::http::StatusCode;
use chrono::Utc;
use serde::{Deserialize, Serialize};
use tracing::info;
use uuid::Uuid;
use vti_common::audit::{AuditEvent, AuditWriter, CommunityInstalledData};
use vti_common::auth::passkey::store::get_passkey_user_by_did;
use vti_common::error::AppError;
use crate::acl::admin::{AdminEntry, RegisteredPasskey, store_admin_entry};
use crate::community::{CommunityProfile, load_profile, store_profile};
use crate::error::TaskError;
use crate::install::InstallTokenSigner;
use crate::server::AppState;
#[derive(Deserialize, utoipa::ToSchema)]
#[schema(as = AdminBootstrapRequest)]
#[serde(rename_all = "camelCase")]
pub struct BootstrapRequest {
pub setup_session_token: String,
}
impl std::fmt::Debug for BootstrapRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BootstrapRequest")
.field("setup_session_token", &"<redacted>")
.finish()
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(utoipa::ToSchema)]
pub struct BootstrapResponse {
pub admin_did: String,
pub event_id: Uuid,
}
pub const BOOTSTRAP_ERR_INVALID_TOKEN: &str =
trust_tasks_rs::specs::vtc::admin::bootstrap::v0_1::error_codes::INVALID_TOKEN.code;
pub const BOOTSTRAP_ERR_ALREADY_BOOTSTRAPPED: &str =
trust_tasks_rs::specs::vtc::admin::bootstrap::v0_1::error_codes::ALREADY_BOOTSTRAPPED.code;
pub(crate) async fn bootstrap(
state: &AppState,
req: BootstrapRequest,
) -> Result<BootstrapResponse, TaskError> {
let signer = require_install_signer(state)?;
let audit_writer = require_audit_writer(state)?;
let claims = signer
.decode_session(&req.setup_session_token)
.map_err(|e| TaskError::declared(BOOTSTRAP_ERR_INVALID_TOKEN, e))?;
let admin_did = claims.sub;
let install_jti = claims.install_jti;
for entry in list_acl_entries(&state.acl_ks).await? {
if entry.admin.is_administrator() {
return Err(TaskError::declared(
BOOTSTRAP_ERR_ALREADY_BOOTSTRAPPED,
AppError::Conflict(
"an admin already exists; refusing to bootstrap a second one".into(),
),
));
}
}
let passkey_user = get_passkey_user_by_did(&state.passkey_ks, &admin_did).await?;
let pending_approver = if passkey_user.is_none() {
crate::step_up_approver::take_pending_install(state, &install_jti, &admin_did).await?
} else {
None
};
if passkey_user.is_none() && pending_approver.is_none() {
return Err(AppError::Unauthorized(
"no passkey or step-up approver registered for the candidate admin DID — run the \
install claim first"
.into(),
)
.into());
}
let first_cred_hex = match passkey_user.as_ref() {
Some(user) => {
let first_cred = user.credentials.first().ok_or_else(|| {
AppError::Internal("admin passkey user has no credentials persisted".into())
})?;
Some(hex::encode(<_ as AsRef<[u8]>>::as_ref(
first_cred.cred_id(),
)))
}
None => None,
};
if let Some(p) = pending_approver.as_ref() {
crate::acl::approver::check_bindable(
&state.step_up_approvers_ks,
&p.approver_did,
&admin_did,
None,
)
.await
.map_err(|e| {
AppError::Conflict(format!(
"the claimed step-up approver cannot be bound: {e:?}"
))
})?;
}
let now = Utc::now();
if crate::acl::admin::get_admin_entry(&state.passkey_ks, &admin_did)
.await?
.is_none()
{
let passkeys = first_cred_hex
.map(|credential_id| RegisteredPasskey {
credential_id,
label: "install".into(),
transports: Vec::new(),
registered_at: now,
last_used_at: None,
})
.into_iter()
.collect();
let admin_entry = AdminEntry {
did: admin_did.clone(),
passkeys,
extensions: serde_json::Value::Null,
created_at: now,
};
store_admin_entry(&state.passkey_ks, &admin_entry).await?;
}
let acl_entry = VtcAclEntry {
did: admin_did.clone(),
role: VtcRole::Admin,
label: Some("first admin (install bootstrap)".into()),
admin: crate::acl::AdminAuthority::community_admin(),
delegated_by: None,
created_at: now_unix(),
created_by: "did:key:vtc-install".into(),
updated_at: None,
updated_by: None,
expires_at: None,
resource_grants: Vec::new(),
label_set_by_subject: false,
suspension: None,
};
store_acl_entry(&state.acl_ks, &acl_entry).await?;
if let Some(p) = pending_approver.as_ref() {
crate::step_up_approver::bind_installed(state, &install_jti, p).await?;
}
let co_admin = match state.install_store.take_co_admin().await? {
Some(did) if did != admin_did => {
let entry = VtcAclEntry {
did: did.clone(),
role: VtcRole::Admin,
label: Some("co-admin (install bootstrap)".into()),
admin: crate::acl::AdminAuthority::community_admin(),
delegated_by: None,
created_at: now_unix(),
created_by: "did:key:vtc-install".into(),
updated_at: None,
updated_by: None,
expires_at: None,
resource_grants: Vec::new(),
label_set_by_subject: false,
suspension: None,
};
store_acl_entry(&state.acl_ks, &entry).await?;
if crate::acl::admin::get_admin_entry(&state.passkey_ks, &did)
.await?
.is_none()
{
store_admin_entry(
&state.passkey_ks,
&AdminEntry {
did: did.clone(),
passkeys: Vec::new(),
extensions: serde_json::Value::Null,
created_at: now,
},
)
.await?;
}
Some(entry)
}
_ => None,
};
let vtc_did = state.config.read().await.vtc_did.clone();
if let Some(did) = vtc_did.as_deref()
&& load_profile(&state.community_ks).await?.is_none()
{
let profile = CommunityProfile::new(did, "");
store_profile(&state.community_ks, &profile).await?;
}
let community_did = vtc_did.unwrap_or_else(|| "did:key:vtc-uninitialised".to_string());
let envelope = audit_writer
.write(
&admin_did,
None,
AuditEvent::CommunityInstalled(CommunityInstalledData {
community_did,
install_token_jti: install_jti,
}),
)
.await?;
info!(%admin_did, event_id = %envelope.event_id, "community installed");
if let Some(entry) = co_admin {
audit_writer
.write(
"did:key:vtc-install",
Some(&entry.did),
AuditEvent::AclGranted(vti_common::audit::AclChangeData {
did: entry.did.clone(),
role: "community-admin".into(),
contexts: entry.capability_list(),
expires_at: None,
}),
)
.await?;
info!(co_admin = %entry.did, "co-admin installed beside the first admin");
}
Ok(BootstrapResponse {
admin_did,
event_id: envelope.event_id,
})
}
fn require_install_signer(state: &AppState) -> Result<&Arc<InstallTokenSigner>, AppError> {
state
.install_signer
.as_ref()
.ok_or_else(|| AppError::ServiceError {
status: StatusCode::SERVICE_UNAVAILABLE,
message: "install signer not configured (run setup first)".into(),
})
}
fn require_audit_writer(state: &AppState) -> Result<&AuditWriter, AppError> {
state
.audit_writer
.as_ref()
.ok_or_else(|| AppError::ServiceError {
status: StatusCode::SERVICE_UNAVAILABLE,
message: "audit writer not configured (run setup first)".into(),
})
}
fn now_unix() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}