use std::collections::HashMap;
use std::sync::Arc;
use axum::extract::{Query, State};
use axum::http::StatusCode;
use axum::response::Response;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine;
use recall_wire::audit::{
AuditCheckpoint, AuditConsistencyResponse, AuditEntriesResponse, MAX_PAGE, MAX_PAGE_BYTES,
};
use super::auth::{Caller, SignedRequestInfo};
use super::respond::{error, internal, json};
use super::AppState;
use crate::audit::leaf;
use crate::store::ConsistencyError;
pub(super) fn actor_for(caller: Option<&Caller>) -> leaf::Actor<'_> {
match caller {
Some(Caller::Device {
id, name, agent, ..
}) => leaf::Actor::Device { id, name, agent },
Some(Caller::Owner { credential_id }) => leaf::Actor::Session { credential_id },
Some(Caller::Operator) | None => leaf::Actor::Operator,
}
}
pub(super) fn signed_request_for<'a>(
signed: Option<&'a SignedRequestInfo>,
body: Option<&'a str>,
) -> Option<leaf::SignedRequest<'a>> {
signed.map(|s| leaf::SignedRequest {
body_sha256: &s.body_sha256,
signature_base: &s.signature_base,
signature: &s.signature,
body,
})
}
pub(super) fn base64_hash(h: &crate::audit::merkle::Hash) -> String {
BASE64_STANDARD.encode(h)
}
pub(super) async fn handle_checkpoint(State(state): State<Arc<AppState>>) -> Response {
let (tree_size, root) = state.store.audit_checkpoint();
json(
StatusCode::OK,
&AuditCheckpoint {
tree_size,
root_hash: base64_hash(&root),
},
)
}
pub(super) async fn handle_entries(
State(state): State<Arc<AppState>>,
Query(params): Query<HashMap<String, String>>,
) -> Response {
const BAD_RANGE: &str =
"start and end are required, end must be at least start, at most 1000 apart, \
and at most the log's size";
let parse = |k: &str| params.get(k).and_then(|v| v.parse::<u64>().ok());
let (Some(start), Some(end)) = (parse("start"), parse("end")) else {
return error(StatusCode::BAD_REQUEST, BAD_RANGE);
};
let (tree_size, _) = state.store.audit_checkpoint();
if end < start || end - start > u64::from(MAX_PAGE) || end > tree_size {
return error(StatusCode::BAD_REQUEST, BAD_RANGE);
}
let entries = match state.store.audit_entries(start, end, MAX_PAGE_BYTES) {
Ok(entries) => entries,
Err(e) => return internal(e),
};
let mut leaves = Vec::with_capacity(entries.len());
for e in entries {
match String::from_utf8(e.leaf) {
Ok(leaf) => leaves.push(leaf),
Err(_) => return internal(anyhow::anyhow!("audit leaf {} is not valid UTF-8", e.seq)),
}
}
json(
StatusCode::OK,
&AuditEntriesResponse {
start,
end: start + leaves.len() as u64,
tree_size,
entries: leaves,
},
)
}
pub(super) async fn handle_consistency(
State(state): State<Arc<AppState>>,
Query(params): Query<HashMap<String, String>>,
) -> Response {
let parse = |k: &str| params.get(k).and_then(|v| v.parse::<u64>().ok());
let (Some(first), Some(second)) = (parse("first"), parse("second")) else {
return error(
StatusCode::BAD_REQUEST,
"first and second query params are required",
);
};
match state.store.audit_consistency(first, second) {
Ok(Ok(proof)) => json(
StatusCode::OK,
&AuditConsistencyResponse {
first,
second,
proof: proof.iter().map(base64_hash).collect(),
},
),
Ok(Err(ConsistencyError::BadRange)) => error(
StatusCode::BAD_REQUEST,
"first must be at least 1 and at most second",
),
Ok(Err(ConsistencyError::SecondBeyondTreeSize)) => {
error(StatusCode::BAD_REQUEST, "second is past the end of the log")
}
Err(e) => internal(e),
}
}