use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use axum::extract::{Path, State};
use axum::http::{header, HeaderMap, StatusCode};
use axum::response::sse::{Event, KeepAlive, Sse};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::{Json, Router};
use dynamic_config::telemetry::Exposition;
use dynamic_config::Changes;
use futures_core::Stream;
use serde::Serialize;
use crate::audit::{AuditEntry, Outcome};
use crate::auth::Principal;
use crate::document::Document;
use crate::server::{Section, Server, StreamPermit};
pub fn router(server: Arc<Server>) -> Router {
Router::new()
.route("/healthz", get(healthz))
.route("/readyz", get(readyz))
.route("/metrics", get(metrics))
.route("/{application}/{profile}", get(document))
.route("/{application}/{profile}/paths", get(paths))
.route("/{application}/{profile}/check", get(check))
.route("/{application}/{profile}/status", get(status))
.route("/{application}/{profile}/stream", get(stream))
.route("/{application}/{profile}/explain/{path}", get(explain))
.fallback(|| async { not_found() })
.with_state(server)
}
async fn healthz() -> Response {
(StatusCode::OK, Json(serde_json::json!({ "status": "ok" }))).into_response()
}
async fn readyz(State(server): State<Arc<Server>>) -> Response {
let ready = server.is_ready();
let code = if ready {
StatusCode::OK
} else {
StatusCode::SERVICE_UNAVAILABLE
};
(code, Json(serde_json::json!({ "ready": ready }))).into_response()
}
const EXPOSITION: &str = "text/plain; version=0.0.4; charset=utf-8";
async fn metrics(State(server): State<Arc<Server>>, headers: HeaderMap) -> Response {
let authorization = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok());
let Some(principal) = server.authenticate(authorization) else {
return refuse(&server, "metrics", Outcome::Unauthenticated, None, None);
};
let mut exposition = Exposition::new();
for section in server.sections() {
if !principal.may_read(section.application()) {
continue;
}
exposition.add_with(
&[
("application", section.application()),
("profile", section.profile()),
],
§ion.status(),
);
}
server.record(&AuditEntry {
caller: Some(principal.name().to_owned()),
application: None,
profile: None,
endpoint: "metrics",
outcome: Outcome::Served,
generation: None,
});
([(header::CONTENT_TYPE, EXPOSITION)], exposition.render()).into_response()
}
#[derive(Serialize)]
struct DocumentBody<'a> {
application: &'a str,
profile: &'a str,
generation: u64,
config: &'a serde_json::Value,
}
async fn document(
State(server): State<Arc<Server>>,
headers: HeaderMap,
Path((application, profile)): Path<(String, String)>,
) -> Response {
let admitted = match admit(&server, &headers, &application, &profile, "document") {
Ok(admitted) => admitted,
Err(response) => return *response,
};
let Some((generation, document)) = admitted.section.installed() else {
return unready(&server, &admitted);
};
served(&server, &admitted, generation);
Json(DocumentBody {
application: admitted.section.application(),
profile: admitted.section.profile(),
generation,
config: document.as_json(),
})
.into_response()
}
#[derive(Serialize)]
struct PathsBody<'a> {
application: &'a str,
profile: &'a str,
generation: u64,
paths: Vec<String>,
}
async fn paths(
State(server): State<Arc<Server>>,
headers: HeaderMap,
Path((application, profile)): Path<(String, String)>,
) -> Response {
let admitted = match admit(&server, &headers, &application, &profile, "paths") {
Ok(admitted) => admitted,
Err(response) => return *response,
};
let Some((generation, document)) = admitted.section.installed() else {
return unready(&server, &admitted);
};
served(&server, &admitted, generation);
Json(PathsBody {
application: admitted.section.application(),
profile: admitted.section.profile(),
generation,
paths: document.leaf_paths(),
})
.into_response()
}
#[derive(Serialize)]
struct StatusBody<'a> {
application: &'a str,
profile: &'a str,
generation: u64,
ready: bool,
healthy: bool,
consecutive_failures: u32,
stale_for_seconds: Option<f64>,
last_reason: Option<&'static str>,
last_failure: Option<FailureBody>,
}
#[derive(Serialize)]
struct FailureBody {
kind: &'static str,
path: String,
seconds_ago: f64,
}
async fn status(
State(server): State<Arc<Server>>,
headers: HeaderMap,
Path((application, profile)): Path<(String, String)>,
) -> Response {
let admitted = match admit(&server, &headers, &application, &profile, "status") {
Ok(admitted) => admitted,
Err(response) => return *response,
};
let status = admitted.section.status();
let generation = status.generation;
served(&server, &admitted, generation);
Json(StatusBody {
application: admitted.section.application(),
profile: admitted.section.profile(),
generation,
ready: admitted.section.is_ready(),
healthy: status.is_healthy(),
consecutive_failures: status.consecutive_failures,
stale_for_seconds: status.stale_for().map(|elapsed| elapsed.as_secs_f64()),
last_reason: status
.last_reason
.as_ref()
.map(dynamic_config::ReloadReason::as_str),
last_failure: status.last_failure.as_ref().map(|failure| FailureBody {
kind: failure.kind.as_str(),
path: failure.path.clone(),
seconds_ago: failure.at.elapsed().as_secs_f64(),
}),
})
.into_response()
}
const KEEP_ALIVE: Duration = Duration::from_secs(15);
async fn stream(
State(server): State<Arc<Server>>,
headers: HeaderMap,
Path((application, profile)): Path<(String, String)>,
) -> Response {
let admitted = match admit(&server, &headers, &application, &profile, "stream") {
Ok(admitted) => admitted,
Err(response) => return *response,
};
if !server.streams_enabled() {
return refuse(
&server,
"stream",
Outcome::NotFound,
Some(admitted.principal.name().to_owned()),
Some((
admitted.section.application().to_owned(),
admitted.section.profile().to_owned(),
)),
);
}
let Some(permit) = server.open_stream() else {
return at_capacity(&server, &admitted);
};
let section = Arc::clone(admitted.section);
let generation = section.generation();
served(&server, &admitted, generation);
let stream = Generations {
application: section.application().to_owned(),
profile: section.profile().to_owned(),
changes: section.changes(),
resume: last_event_id(&headers),
sent: None,
section,
_permit: permit,
};
Sse::new(stream)
.keep_alive(KeepAlive::new().interval(KEEP_ALIVE))
.into_response()
}
fn last_event_id(headers: &HeaderMap) -> Option<u64> {
headers
.get("last-event-id")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.trim().parse::<u64>().ok())
}
struct Generations {
application: String,
profile: String,
section: Arc<Section>,
changes: Changes<Document>,
sent: Option<u64>,
resume: Option<u64>,
_permit: StreamPermit,
}
impl Generations {
fn is_news(&self, generation: u64) -> bool {
if generation == 0 {
return false;
}
match self.sent {
Some(sent) => generation > sent,
None => match self.resume {
Some(resumed) => generation != resumed,
None => true,
},
}
}
fn event(&self, generation: u64) -> Event {
let data = serde_json::json!({
"application": self.application,
"profile": self.profile,
"generation": generation,
})
.to_string();
Event::default()
.id(generation.to_string())
.event("generation")
.data(data)
}
}
impl Stream for Generations {
type Item = Result<Event, std::convert::Infallible>;
fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
let generation = this.section.generation();
if this.is_news(generation) {
this.sent = Some(generation);
return Poll::Ready(Some(Ok(this.event(generation))));
}
let changed = this.changes.changed();
match std::pin::pin!(changed).poll(context) {
Poll::Ready(_) => continue,
Poll::Pending => return Poll::Pending,
}
}
}
}
#[derive(Serialize)]
struct CheckBody<'a> {
application: &'a str,
profile: &'a str,
clean: bool,
resolved: Vec<ResolvedRow>,
unknown: Vec<UnknownRow>,
failure: Option<String>,
}
#[derive(Serialize)]
struct ResolvedRow {
path: String,
origin: String,
}
#[derive(Serialize)]
struct UnknownRow {
path: String,
suggestion: Option<String>,
}
async fn check(
State(server): State<Arc<Server>>,
headers: HeaderMap,
Path((application, profile)): Path<(String, String)>,
) -> Response {
let admitted = match admit(&server, &headers, &application, &profile, "check") {
Ok(admitted) => admitted,
Err(response) => return *response,
};
let sources = admitted.section.sources();
let report = match dynamic_config::off_thread(move || sources.check()).await {
Ok(report) => report,
Err(error) => return unavailable(&server, &admitted, &error),
};
let generation = admitted.section.generation();
served(&server, &admitted, generation);
Json(CheckBody {
application: admitted.section.application(),
profile: admitted.section.profile(),
clean: report.is_clean(),
resolved: report
.resolved
.into_iter()
.map(|resolved| ResolvedRow {
path: resolved.path,
origin: resolved.origin.to_string(),
})
.collect(),
unknown: report
.unknown
.into_iter()
.map(|unknown| UnknownRow {
path: unknown.path,
suggestion: unknown.suggestion,
})
.collect(),
failure: report.failure,
})
.into_response()
}
#[derive(Serialize)]
struct ExplainBody<'a> {
application: &'a str,
profile: &'a str,
path: String,
winner: Option<&'static str>,
rows: Vec<ExplainRow>,
}
#[derive(Serialize)]
struct ExplainRow {
layer: &'static str,
origin: Option<String>,
value: Option<String>,
}
async fn explain(
State(server): State<Arc<Server>>,
headers: HeaderMap,
Path((application, profile, path)): Path<(String, String, String)>,
) -> Response {
let admitted = match admit(&server, &headers, &application, &profile, "explain") {
Ok(admitted) => admitted,
Err(response) => return *response,
};
if !is_key_path(&path) {
return refuse(
&server,
"explain",
Outcome::Malformed,
Some(admitted.principal.name().to_owned()),
Some((
admitted.section.application().to_owned(),
admitted.section.profile().to_owned(),
)),
);
}
let sources = admitted.section.sources();
let asked = path.clone();
let explanation = match dynamic_config::off_thread(move || sources.explain(&asked)).await {
Ok(explanation) => explanation.redacted(),
Err(error) => return unavailable(&server, &admitted, &error),
};
let generation = admitted.section.generation();
served(&server, &admitted, generation);
Json(ExplainBody {
application: admitted.section.application(),
profile: admitted.section.profile(),
path,
winner: explanation.winner().map(|row| row.layer),
rows: explanation
.rows()
.iter()
.map(|row| ExplainRow {
layer: row.layer,
origin: row.origin.as_ref().map(ToString::to_string),
value: row.value.clone(),
})
.collect(),
})
.into_response()
}
struct Admitted<'a> {
principal: Principal,
section: &'a Arc<Section>,
endpoint: &'static str,
}
fn admit<'a>(
server: &'a Server,
headers: &HeaderMap,
application: &str,
profile: &str,
endpoint: &'static str,
) -> Result<Admitted<'a>, Box<Response>> {
let authorization = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok());
let Some(principal) = server.authenticate(authorization) else {
return Err(Box::new(refuse(
server,
endpoint,
Outcome::Unauthenticated,
None,
None,
)));
};
if !is_name(application) || !is_name(profile) {
return Err(Box::new(refuse(
server,
endpoint,
Outcome::Malformed,
Some(principal.name().to_owned()),
None,
)));
}
let caller = Some(principal.name().to_owned());
let where_ = Some((application.to_owned(), profile.to_owned()));
if !principal.may_read(application) {
return Err(Box::new(refuse(
server,
endpoint,
Outcome::NotFound,
caller,
where_,
)));
}
let Some(section) = server.section(application, profile) else {
return Err(Box::new(refuse(
server,
endpoint,
Outcome::NotFound,
caller,
where_,
)));
};
Ok(Admitted {
principal,
section,
endpoint,
})
}
fn served(server: &Server, admitted: &Admitted<'_>, generation: u64) {
server.record(&AuditEntry {
caller: Some(admitted.principal.name().to_owned()),
application: Some(admitted.section.application().to_owned()),
profile: Some(admitted.section.profile().to_owned()),
endpoint: admitted.endpoint,
outcome: Outcome::Served,
generation: Some(generation),
});
}
fn refuse(
server: &Server,
endpoint: &'static str,
outcome: Outcome,
caller: Option<String>,
where_: Option<(String, String)>,
) -> Response {
let (application, profile) = match where_ {
Some((application, profile)) => (Some(application), Some(profile)),
None => (None, None),
};
server.record(&AuditEntry {
caller,
application,
profile,
endpoint,
outcome,
generation: None,
});
match outcome {
Outcome::Unauthenticated => unauthenticated(),
_ => not_found(),
}
}
fn unready(server: &Server, admitted: &Admitted<'_>) -> Response {
server.record(&AuditEntry {
caller: Some(admitted.principal.name().to_owned()),
application: Some(admitted.section.application().to_owned()),
profile: Some(admitted.section.profile().to_owned()),
endpoint: admitted.endpoint,
outcome: Outcome::Unavailable,
generation: None,
});
(
StatusCode::SERVICE_UNAVAILABLE,
Json(serde_json::json!({ "error": "unavailable" })),
)
.into_response()
}
fn at_capacity(server: &Server, admitted: &Admitted<'_>) -> Response {
server.record(&AuditEntry {
caller: Some(admitted.principal.name().to_owned()),
application: Some(admitted.section.application().to_owned()),
profile: Some(admitted.section.profile().to_owned()),
endpoint: admitted.endpoint,
outcome: Outcome::Unavailable,
generation: None,
});
(
StatusCode::SERVICE_UNAVAILABLE,
[(header::RETRY_AFTER, "5")],
Json(serde_json::json!({ "error": "unavailable" })),
)
.into_response()
}
fn unavailable(
server: &Server,
admitted: &Admitted<'_>,
error: &dynamic_config::Error,
) -> Response {
server.record(&AuditEntry {
caller: Some(admitted.principal.name().to_owned()),
application: Some(admitted.section.application().to_owned()),
profile: Some(admitted.section.profile().to_owned()),
endpoint: admitted.endpoint,
outcome: Outcome::Unavailable,
generation: None,
});
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "unavailable",
"kind": error.kind().as_str(),
"path": error.path(),
})),
)
.into_response()
}
fn unauthenticated() -> Response {
(
StatusCode::UNAUTHORIZED,
[(header::WWW_AUTHENTICATE, "Bearer")],
Json(serde_json::json!({ "error": "unauthenticated" })),
)
.into_response()
}
fn not_found() -> Response {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "not_found" })),
)
.into_response()
}
pub(crate) fn is_name(value: &str) -> bool {
let mut characters = value.chars();
characters
.next()
.is_some_and(|first| first.is_ascii_alphanumeric())
&& value.len() <= 64
&& characters.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
}
fn is_key_path(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 256
&& value.split('.').all(|segment| {
!segment.is_empty()
&& segment
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-'))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_name_is_narrow_and_rejects_traversal_and_newlines() {
assert!(is_name("billing"));
assert!(is_name("billing-api"));
assert!(is_name("billing.api_2"));
assert!(!is_name(""), "the empty segment");
assert!(!is_name(".."), "traversal");
assert!(!is_name(".hidden"), "a leading dot");
assert!(!is_name("bill ing"), "a space");
assert!(!is_name("billing\nadmin"), "a forged audit line");
assert!(!is_name("bill/ing"), "a separator");
assert!(!is_name(&"a".repeat(65)), "unbounded length");
}
#[test]
fn a_key_path_is_dotted_and_has_no_empty_segments() {
assert!(is_key_path("port"));
assert!(is_key_path("pool.max_size"));
assert!(is_key_path("a-b.c-d"));
assert!(!is_key_path(""));
assert!(!is_key_path("."));
assert!(!is_key_path("pool."));
assert!(!is_key_path(".pool"));
assert!(!is_key_path("pool..max"));
assert!(!is_key_path("pool max"));
assert!(!is_key_path(&"a".repeat(257)));
}
}