use crate::command_contract::{CommandRequest, CommandResponse};
use crate::query_contract::{QueryRequest, QueryResponse};
use appcore_security::CommandTokenError;
pub use appcore_security::{
RequestPayloadRef, RequestValidationDetails, RequestValidationDetailsRef,
};
use axum::http::{HeaderMap, StatusCode};
use axum::Json;
use std::sync::Arc;
use super::response::{command_forbidden, command_unauthorized};
#[derive(Clone)]
pub struct HttpCommandAuth {
pub require_token: bool,
pub public_status: bool,
pub verifier: Option<Arc<dyn CommandTokenVerifier>>,
}
impl Default for HttpCommandAuth {
fn default() -> Self {
Self {
require_token: true,
public_status: false,
verifier: None,
}
}
}
impl HttpCommandAuth {
pub fn insecure_local_for_testing() -> Self {
Self {
require_token: false,
public_status: false,
verifier: None,
}
}
}
pub trait CommandTokenVerifier: Send + Sync {
fn verify_command_token(
&self,
token: &str,
command_name: &str,
) -> Result<(), CommandTokenError>;
fn verify_query_token(&self, token: &str, query_name: &str) -> Result<(), CommandTokenError>;
fn verify_command_token_with_request(
&self,
token: &str,
command_name: &str,
_details: Option<&RequestValidationDetails>,
) -> Result<(), CommandTokenError> {
self.verify_command_token(token, command_name)
}
fn verify_query_token_with_request(
&self,
token: &str,
query_name: &str,
_details: Option<&RequestValidationDetails>,
) -> Result<(), CommandTokenError> {
self.verify_query_token(token, query_name)
}
fn verify_command_token_with_borrowed_request(
&self,
token: &str,
command_name: &str,
details: Option<&RequestValidationDetailsRef<'_>>,
) -> Result<(), CommandTokenError> {
let owned = owned_request_details(details)?;
self.verify_command_token_with_request(token, command_name, owned.as_ref())
}
fn verify_query_token_with_borrowed_request(
&self,
token: &str,
query_name: &str,
details: Option<&RequestValidationDetailsRef<'_>>,
) -> Result<(), CommandTokenError> {
let owned = owned_request_details(details)?;
self.verify_query_token_with_request(token, query_name, owned.as_ref())
}
}
pub(crate) fn authorize_command(
auth: &HttpCommandAuth,
headers: &HeaderMap,
request: &CommandRequest,
) -> Option<(StatusCode, Json<CommandResponse>)> {
if !auth.require_token {
return None;
}
let token = match extract_bearer_token(headers) {
Some(token) => token,
None => return Some(command_unauthorized("missing bearer token")),
};
let Some(verifier) = &auth.verifier else {
return Some(command_unauthorized("token verifier not configured"));
};
let details = RequestValidationDetailsRef {
purpose: "command",
name: &request.command_name,
id: &request.command_id,
idempotency_key: request.idempotency_key.as_deref(),
payload: RequestPayloadRef::Text(&request.payload),
subject: None,
audience: None,
};
match verifier.verify_command_token_with_borrowed_request(
token,
&request.command_name,
Some(&details),
) {
Ok(()) => None,
Err(CommandTokenError::Forbidden) => {
Some(command_forbidden("command not allowed for token"))
}
Err(CommandTokenError::InvalidFormat | CommandTokenError::Unauthorized) => {
Some(command_unauthorized("invalid bearer token"))
}
}
}
pub(crate) fn authorize_query(
auth: &HttpCommandAuth,
headers: &HeaderMap,
request: &QueryRequest,
) -> Option<(StatusCode, Json<QueryResponse>)> {
if !auth.require_token {
return None;
}
let token = match extract_bearer_token(headers) {
Some(token) => token,
None => {
return Some((
StatusCode::UNAUTHORIZED,
Json(QueryResponse::rejected("missing bearer token")),
))
}
};
let Some(verifier) = &auth.verifier else {
return Some((
StatusCode::UNAUTHORIZED,
Json(QueryResponse::rejected("token verifier not configured")),
));
};
let details = RequestValidationDetailsRef {
purpose: "query",
name: &request.query_name,
id: &request.query_id,
idempotency_key: None,
payload: RequestPayloadRef::Json(&request.payload),
subject: None,
audience: None,
};
match verifier.verify_query_token_with_borrowed_request(
token,
&request.query_name,
Some(&details),
) {
Ok(()) => None,
Err(CommandTokenError::Forbidden) => Some((
StatusCode::FORBIDDEN,
Json(QueryResponse::rejected("query not allowed for token")),
)),
Err(_) => Some((
StatusCode::UNAUTHORIZED,
Json(QueryResponse::rejected("invalid bearer token")),
)),
}
}
fn owned_request_details(
details: Option<&RequestValidationDetailsRef<'_>>,
) -> Result<Option<RequestValidationDetails>, CommandTokenError> {
details
.copied()
.map(RequestValidationDetailsRef::to_owned)
.transpose()
.map_err(|_| CommandTokenError::InvalidFormat)
}
pub(crate) fn authorize_status(
auth: &HttpCommandAuth,
headers: &HeaderMap,
) -> Result<bool, StatusCode> {
let token_opt = extract_bearer_token(headers);
if !auth.public_status {
let Some(token) = token_opt else {
return Err(StatusCode::UNAUTHORIZED);
};
let Some(verifier) = &auth.verifier else {
return Err(StatusCode::UNAUTHORIZED);
};
match verifier.verify_query_token_with_request(token, "runtime.status", None) {
Ok(()) => Ok(true),
Err(CommandTokenError::Forbidden) => Err(StatusCode::FORBIDDEN),
Err(_) => Err(StatusCode::UNAUTHORIZED),
}
} else {
let Some(token) = token_opt else {
return Ok(false);
};
let Some(verifier) = &auth.verifier else {
return Ok(false);
};
match verifier.verify_query_token_with_request(token, "runtime.status", None) {
Ok(()) => Ok(true),
_ => Ok(false),
}
}
}
pub(crate) fn authorize_private_status(
auth: &HttpCommandAuth,
headers: &HeaderMap,
query_name: &str,
) -> Result<(), StatusCode> {
let Some(token) = extract_bearer_token(headers) else {
return Err(StatusCode::UNAUTHORIZED);
};
let Some(verifier) = &auth.verifier else {
return Err(StatusCode::UNAUTHORIZED);
};
match verifier.verify_query_token_with_request(token, query_name, None) {
Ok(()) => Ok(()),
Err(CommandTokenError::Forbidden) => Err(StatusCode::FORBIDDEN),
Err(_) => Err(StatusCode::UNAUTHORIZED),
}
}
fn extract_bearer_token(headers: &HeaderMap) -> Option<&str> {
if headers.get_all("authorization").iter().count() != 1 {
return None;
}
let auth = headers.get("authorization")?;
let auth = auth.to_str().ok()?;
let token = auth.strip_prefix("Bearer ")?;
if token.is_empty() {
return None;
}
Some(token)
}