Skip to main content

appcore_api/http/
auth.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: auth.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/06/02 13:08:16 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/06/04 11:51:27 by dnettoRaw
8//      ###########      S: 0.6.0
9// =============================================================================
10
11//! Bearer-token authorization for HTTP command and query ingress.
12
13use crate::command_contract::{CommandRequest, CommandResponse};
14use crate::query_contract::{QueryRequest, QueryResponse};
15use appcore_security::CommandTokenError;
16pub use appcore_security::RequestValidationDetails;
17use axum::http::{HeaderMap, StatusCode};
18use axum::Json;
19use std::sync::Arc;
20
21use super::response::{command_forbidden, command_unauthorized};
22
23#[derive(Clone, Default)]
24/// Bearer-token requirements and verifier for HTTP Runtime routes.
25pub struct HttpCommandAuth {
26    /// Whether command and query endpoints require a bearer token.
27    pub require_token: bool,
28    /// Whether unauthenticated callers may read the reduced status response.
29    pub public_status: bool,
30    /// Token verifier used when authentication is required.
31    pub verifier: Option<Arc<dyn CommandTokenVerifier>>,
32}
33
34/// Verifies command- and query-scoped bearer tokens.
35pub trait CommandTokenVerifier: Send + Sync {
36    /// Verifies a token for a command name.
37    fn verify_command_token(
38        &self,
39        token: &str,
40        command_name: &str,
41    ) -> Result<(), CommandTokenError>;
42    /// Verifies a token for a query name.
43    fn verify_query_token(&self, token: &str, query_name: &str) -> Result<(), CommandTokenError>;
44
45    /// Verifies a command token with optional request-bound validation details.
46    fn verify_command_token_with_request(
47        &self,
48        token: &str,
49        command_name: &str,
50        _details: Option<&RequestValidationDetails>,
51    ) -> Result<(), CommandTokenError> {
52        self.verify_command_token(token, command_name)
53    }
54
55    /// Verifies a query token with optional request-bound validation details.
56    fn verify_query_token_with_request(
57        &self,
58        token: &str,
59        query_name: &str,
60        _details: Option<&RequestValidationDetails>,
61    ) -> Result<(), CommandTokenError> {
62        self.verify_query_token(token, query_name)
63    }
64}
65
66pub(crate) fn authorize_command(
67    auth: &HttpCommandAuth,
68    headers: &HeaderMap,
69    request: &CommandRequest,
70) -> Option<(StatusCode, Json<CommandResponse>)> {
71    if !auth.require_token {
72        return None;
73    }
74    let token = match extract_bearer_token(headers) {
75        Some(token) => token,
76        None => return Some(command_unauthorized("missing bearer token")),
77    };
78    let Some(verifier) = &auth.verifier else {
79        return Some(command_unauthorized("token verifier not configured"));
80    };
81    let details = RequestValidationDetails {
82        purpose: "command".to_string(),
83        name: request.command_name.clone(),
84        id: request.command_id.clone(),
85        idempotency_key: request.idempotency_key.clone(),
86        payload: request.payload.clone(),
87        subject: None,
88        audience: None,
89    };
90    match verifier.verify_command_token_with_request(token, &request.command_name, Some(&details)) {
91        Ok(()) => None,
92        Err(CommandTokenError::Forbidden) => {
93            Some(command_forbidden("command not allowed for token"))
94        }
95        Err(CommandTokenError::InvalidFormat | CommandTokenError::Unauthorized) => {
96            Some(command_unauthorized("invalid bearer token"))
97        }
98    }
99}
100
101pub(crate) fn authorize_query(
102    auth: &HttpCommandAuth,
103    headers: &HeaderMap,
104    request: &QueryRequest,
105) -> Option<(StatusCode, Json<QueryResponse>)> {
106    if !auth.require_token {
107        return None;
108    }
109    let token = match extract_bearer_token(headers) {
110        Some(token) => token,
111        None => {
112            return Some((
113                StatusCode::UNAUTHORIZED,
114                Json(QueryResponse::rejected("missing bearer token")),
115            ))
116        }
117    };
118    let Some(verifier) = &auth.verifier else {
119        return Some((
120            StatusCode::UNAUTHORIZED,
121            Json(QueryResponse::rejected("token verifier not configured")),
122        ));
123    };
124    let details = RequestValidationDetails {
125        purpose: "query".to_string(),
126        name: request.query_name.clone(),
127        id: request.query_id.clone(),
128        idempotency_key: None,
129        payload: serde_json::to_string(&request.payload).unwrap_or_default(),
130        subject: None,
131        audience: None,
132    };
133    match verifier.verify_query_token_with_request(token, &request.query_name, Some(&details)) {
134        Ok(()) => None,
135        Err(CommandTokenError::Forbidden) => Some((
136            StatusCode::FORBIDDEN,
137            Json(QueryResponse::rejected("query not allowed for token")),
138        )),
139        Err(_) => Some((
140            StatusCode::UNAUTHORIZED,
141            Json(QueryResponse::rejected("invalid bearer token")),
142        )),
143    }
144}
145
146pub(crate) fn authorize_status(
147    auth: &HttpCommandAuth,
148    headers: &HeaderMap,
149) -> Result<bool, StatusCode> {
150    let token_opt = extract_bearer_token(headers);
151    if !auth.public_status {
152        let Some(token) = token_opt else {
153            return Err(StatusCode::UNAUTHORIZED);
154        };
155        let Some(verifier) = &auth.verifier else {
156            return Err(StatusCode::UNAUTHORIZED);
157        };
158        match verifier.verify_query_token_with_request(token, "runtime.status", None) {
159            Ok(()) => Ok(true),
160            Err(CommandTokenError::Forbidden) => Err(StatusCode::FORBIDDEN),
161            Err(_) => Err(StatusCode::UNAUTHORIZED),
162        }
163    } else {
164        let Some(token) = token_opt else {
165            return Ok(false);
166        };
167        let Some(verifier) = &auth.verifier else {
168            return Ok(false);
169        };
170        match verifier.verify_query_token_with_request(token, "runtime.status", None) {
171            Ok(()) => Ok(true),
172            _ => Ok(false),
173        }
174    }
175}
176
177pub(crate) fn authorize_private_status(
178    auth: &HttpCommandAuth,
179    headers: &HeaderMap,
180    query_name: &str,
181) -> Result<(), StatusCode> {
182    let Some(token) = extract_bearer_token(headers) else {
183        return Err(StatusCode::UNAUTHORIZED);
184    };
185    let Some(verifier) = &auth.verifier else {
186        return Err(StatusCode::UNAUTHORIZED);
187    };
188    match verifier.verify_query_token_with_request(token, query_name, None) {
189        Ok(()) => Ok(()),
190        Err(CommandTokenError::Forbidden) => Err(StatusCode::FORBIDDEN),
191        Err(_) => Err(StatusCode::UNAUTHORIZED),
192    }
193}
194
195fn extract_bearer_token(headers: &HeaderMap) -> Option<&str> {
196    if headers.get_all("authorization").iter().count() != 1 {
197        return None;
198    }
199    let auth = headers.get("authorization")?;
200    let auth = auth.to_str().ok()?;
201    let token = auth.strip_prefix("Bearer ")?;
202    if token.is_empty() {
203        return None;
204    }
205    Some(token)
206}