use std::sync::Arc;
use axum::http::{header, HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::Json;
use crate::audit::{AuditEntry, Outcome};
use crate::auth::Principal;
use crate::server::{Section, Server};
use super::names::is_name;
pub(super) struct Admitted<'a> {
pub(super) principal: Principal,
pub(super) section: &'a Arc<Section>,
pub(super) endpoint: &'static str,
}
pub(super) 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,
})
}
pub(super) 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),
});
}
pub(super) 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(),
}
}
pub(super) 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()
}
pub(super) 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()
}
pub(super) 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()
}
pub(super) fn unauthenticated() -> Response {
(
StatusCode::UNAUTHORIZED,
[(header::WWW_AUTHENTICATE, "Bearer")],
Json(serde_json::json!({ "error": "unauthenticated" })),
)
.into_response()
}
pub(super) fn not_found() -> Response {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "not_found" })),
)
.into_response()
}