use std::error::Error as _;
use std::sync::Arc;
use axum::Router;
use axum::extract::{DefaultBodyLimit, Request, State};
use axum::http::HeaderMap;
use axum::middleware::{Next, from_fn_with_state};
use axum::response::Response;
use axum::routing::MethodRouter;
use tracing::warn;
use super::auth::{
AdminAction, AdminAuthenticator, AdminAuthorizer, AdminGrant, AdminIdentity, AdminPresented,
};
use super::error::AdminError;
use super::handlers;
use super::protocol::{ADMIN_PREFIX, MutationPreconditions};
use super::resources::{
AliasRequest, CatalogRequest, CredentialRequest, ModelRequest, PolicyRequest, ProjectRequest,
ProviderRequest, TenantRequest,
};
use super::service::AdminService;
use crate::availability::AvailabilityReader;
use crate::convergence::{RevisionReport, RevisionStatus};
use crate::desired_state::{ResourceScope, Surface};
pub struct AdminApi {
pub service: Arc<AdminService>,
pub authenticator: Arc<dyn AdminAuthenticator>,
pub authorizer: Arc<dyn AdminAuthorizer>,
pub convergence: Option<Arc<RevisionStatus>>,
pub availability: Option<Arc<dyn AvailabilityReader>>,
}
impl AdminApi {
pub fn new(
service: Arc<AdminService>,
authenticator: Arc<dyn AdminAuthenticator>,
authorizer: Arc<dyn AdminAuthorizer>,
) -> Self {
Self {
service,
authenticator,
authorizer,
convergence: None,
availability: None,
}
}
#[must_use]
pub fn with_availability(mut self, availability: Arc<dyn AvailabilityReader>) -> Self {
self.availability = Some(availability);
self
}
#[must_use]
pub fn with_convergence(mut self, status: Arc<RevisionStatus>) -> Self {
self.convergence = Some(status);
self
}
pub fn convergence_report(&self) -> Option<RevisionReport> {
self.convergence.as_ref().map(|status| status.report())
}
pub async fn authenticate(&self, headers: &HeaderMap) -> Result<AdminIdentity, AdminError> {
let presented = AdminPresented::from_headers(headers)?;
Ok(self.authenticator.authenticate(&presented).await?)
}
pub async fn authorize(
&self,
identity: &AdminIdentity,
action: AdminAction,
surface: Surface,
scope: &ResourceScope,
) -> Result<AdminGrant, AdminError> {
match self.authorizer.authorize(identity, action, scope) {
Ok(grant) => Ok(grant),
Err(refusal) => {
let error = AdminError::from(refusal);
self.service
.record_denial(identity, action, surface, scope, &error)
.await;
Err(error)
}
}
}
pub fn holds_deployment_authority(
&self,
identity: &AdminIdentity,
action: AdminAction,
) -> bool {
self.authorizer
.authorize(identity, action, &ResourceScope::Deployment)
.is_ok()
}
}
pub struct AdminRouteSpec {
pub path: &'static str,
pub action: AdminAction,
pub router: fn() -> MethodRouter<Arc<AdminApi>>,
}
pub fn admin_route_specs() -> Vec<AdminRouteSpec> {
vec![
AdminRouteSpec {
path: "/state",
action: AdminAction::ReadState,
router: handlers::state_route,
},
AdminRouteSpec {
path: "/catalogue",
action: AdminAction::ReadState,
router: handlers::catalogue_route,
},
AdminRouteSpec {
path: "/history",
action: AdminAction::ReadHistory,
router: handlers::history_route,
},
AdminRouteSpec {
path: "/audit/{revision}",
action: AdminAction::ReadAudit,
router: handlers::audit_route,
},
AdminRouteSpec {
path: "/convergence",
action: AdminAction::ReadConvergence,
router: handlers::convergence_route,
},
AdminRouteSpec {
path: "/availability",
action: AdminAction::ReadAvailability,
router: handlers::availability_route,
},
AdminRouteSpec {
path: "/tenants",
action: AdminAction::Publish,
router: handlers::publish_route::<TenantRequest>,
},
AdminRouteSpec {
path: "/projects",
action: AdminAction::Publish,
router: handlers::publish_route::<ProjectRequest>,
},
AdminRouteSpec {
path: "/providers",
action: AdminAction::Publish,
router: handlers::publish_route::<ProviderRequest>,
},
AdminRouteSpec {
path: "/credentials",
action: AdminAction::Publish,
router: handlers::publish_route::<CredentialRequest>,
},
AdminRouteSpec {
path: "/catalogs",
action: AdminAction::Publish,
router: handlers::publish_route::<CatalogRequest>,
},
AdminRouteSpec {
path: "/models",
action: AdminAction::Publish,
router: handlers::publish_route::<ModelRequest>,
},
AdminRouteSpec {
path: "/aliases",
action: AdminAction::Publish,
router: handlers::publish_route::<AliasRequest>,
},
AdminRouteSpec {
path: "/policies",
action: AdminAction::Publish,
router: handlers::publish_route::<PolicyRequest>,
},
AdminRouteSpec {
path: "/secrets",
action: AdminAction::WriteSecrets,
router: handlers::stage_secret_route,
},
AdminRouteSpec {
path: "/secrets/rotate",
action: AdminAction::WriteSecrets,
router: handlers::rotate_secret_route,
},
AdminRouteSpec {
path: "/secrets/lifecycle",
action: AdminAction::WriteSecrets,
router: handlers::secret_lifecycle_route,
},
AdminRouteSpec {
path: "/secrets/{secret}",
action: AdminAction::ReadSecrets,
router: handlers::secret_versions_route,
},
AdminRouteSpec {
path: "/rollback",
action: AdminAction::Rollback,
router: handlers::rollback_route,
},
]
}
#[cfg(test)]
pub(super) fn concrete_path(spec: &AdminRouteSpec) -> String {
use crate::desired_state::fixtures;
spec.path
.replace("{revision}", &fixtures::revision_id(1).to_string())
.replace("{secret}", &fixtures::secret_id(1).to_string())
}
pub const ADMIN_MAX_REQUEST_BYTES: usize = 1024 * 1024;
pub(crate) fn mount(api: Arc<AdminApi>, specs: Vec<AdminRouteSpec>) -> Router {
let inner = specs
.into_iter()
.fold(Router::new(), |router, spec| {
let route = (spec.router)()
.layer(DefaultBodyLimit::max(ADMIN_MAX_REQUEST_BYTES))
.layer(from_fn_with_state(
(api.clone(), spec.action),
admin_authenticate,
));
router.route(spec.path, route)
})
.fallback(unknown_route)
.method_not_allowed_fallback(wrong_method)
.with_state(api);
Router::new().nest(ADMIN_PREFIX, inner)
}
pub fn router(api: Arc<AdminApi>) -> Router {
mount(api, admin_route_specs())
}
pub fn refusing_router() -> Router {
Router::new().nest(
ADMIN_PREFIX,
Router::new()
.fallback(stateful_mode_required)
.method_not_allowed_fallback(stateful_mode_required),
)
}
async fn stateful_mode_required() -> AdminError {
AdminError::StatefulModeRequired
}
async fn unknown_route() -> AdminError {
AdminError::RouteNotFound
}
async fn wrong_method() -> AdminError {
AdminError::MethodNotAllowed
}
async fn admin_authenticate(
State((api, action)): State<(Arc<AdminApi>, AdminAction)>,
headers: HeaderMap,
mut request: Request,
next: Next,
) -> Result<Response, AdminError> {
let identity = match api.authenticate(&headers).await {
Ok(identity) => identity,
Err(error) => {
warn!(
code = error.code(),
cause = error.source().map(ToString::to_string).as_deref(),
"administrative authentication failed"
);
return Err(error);
}
};
if action.mutates() {
let preconditions = MutationPreconditions::from_headers(&headers)?;
request.extensions_mut().insert(preconditions);
}
request.extensions_mut().insert(identity);
request.extensions_mut().insert(action);
Ok(next.run(request).await)
}