1use crate::command_contract::{CommandRequest, CommandResponse};
14use crate::query_contract::{QueryRequest, QueryResponse};
15use appcore_security::CommandTokenError;
16pub use appcore_security::{
17 RequestPayloadRef, RequestValidationDetails, RequestValidationDetailsRef,
18};
19use axum::http::{HeaderMap, StatusCode};
20use axum::Json;
21use std::sync::Arc;
22
23use super::response::{command_forbidden, command_unauthorized};
24
25#[derive(Clone)]
26pub struct HttpCommandAuth {
28 pub(crate) require_token: bool,
29 pub(crate) public_status: bool,
30 pub(crate) verifier: Option<Arc<dyn CommandTokenVerifier>>,
31}
32
33impl Default for HttpCommandAuth {
34 fn default() -> Self {
35 Self {
36 require_token: true,
37 public_status: false,
38 verifier: None,
39 }
40 }
41}
42
43impl HttpCommandAuth {
44 pub fn required(verifier: Arc<dyn CommandTokenVerifier>) -> Self {
46 Self {
47 require_token: true,
48 public_status: false,
49 verifier: Some(verifier),
50 }
51 }
52
53 pub fn with_public_status(mut self, public_status: bool) -> Self {
55 self.public_status = public_status;
56 self
57 }
58
59 pub fn requires_token(&self) -> bool {
61 self.require_token
62 }
63
64 pub fn allows_public_status(&self) -> bool {
66 self.public_status
67 }
68
69 #[cfg(any(test, feature = "insecure-testing"))]
75 pub fn insecure_local_for_testing() -> Self {
76 Self {
77 require_token: false,
78 public_status: false,
79 verifier: None,
80 }
81 }
82}
83
84pub trait CommandTokenVerifier: Send + Sync {
86 fn verify_command_token(
88 &self,
89 token: &str,
90 command_name: &str,
91 ) -> Result<(), CommandTokenError>;
92 fn verify_query_token(&self, token: &str, query_name: &str) -> Result<(), CommandTokenError>;
94
95 fn verify_command_token_with_request(
97 &self,
98 token: &str,
99 command_name: &str,
100 _details: Option<&RequestValidationDetails>,
101 ) -> Result<(), CommandTokenError> {
102 self.verify_command_token(token, command_name)
103 }
104
105 fn verify_query_token_with_request(
107 &self,
108 token: &str,
109 query_name: &str,
110 _details: Option<&RequestValidationDetails>,
111 ) -> Result<(), CommandTokenError> {
112 self.verify_query_token(token, query_name)
113 }
114
115 fn verify_command_token_with_borrowed_request(
121 &self,
122 token: &str,
123 command_name: &str,
124 details: Option<&RequestValidationDetailsRef<'_>>,
125 ) -> Result<(), CommandTokenError> {
126 let owned = owned_request_details(details)?;
127 self.verify_command_token_with_request(token, command_name, owned.as_ref())
128 }
129
130 fn verify_query_token_with_borrowed_request(
136 &self,
137 token: &str,
138 query_name: &str,
139 details: Option<&RequestValidationDetailsRef<'_>>,
140 ) -> Result<(), CommandTokenError> {
141 let owned = owned_request_details(details)?;
142 self.verify_query_token_with_request(token, query_name, owned.as_ref())
143 }
144}
145
146pub(crate) fn authorize_command(
147 auth: &HttpCommandAuth,
148 headers: &HeaderMap,
149 request: &CommandRequest,
150) -> Option<(StatusCode, Json<CommandResponse>)> {
151 if !auth.require_token {
152 return None;
153 }
154 let token = match extract_bearer_token(headers) {
155 Some(token) => token,
156 None => return Some(command_unauthorized("missing bearer token")),
157 };
158 let Some(verifier) = &auth.verifier else {
159 return Some(command_unauthorized("token verifier not configured"));
160 };
161 let details = RequestValidationDetailsRef {
162 purpose: "command",
163 name: &request.command_name,
164 id: &request.command_id,
165 idempotency_key: request.idempotency_key.as_deref(),
166 payload: RequestPayloadRef::Text(&request.payload),
167 subject: None,
168 audience: None,
169 };
170 match verifier.verify_command_token_with_borrowed_request(
171 token,
172 &request.command_name,
173 Some(&details),
174 ) {
175 Ok(()) => None,
176 Err(CommandTokenError::Forbidden) => {
177 Some(command_forbidden("command not allowed for token"))
178 }
179 Err(CommandTokenError::InvalidFormat | CommandTokenError::Unauthorized) => {
180 Some(command_unauthorized("invalid bearer token"))
181 }
182 }
183}
184
185pub(crate) fn authorize_query(
186 auth: &HttpCommandAuth,
187 headers: &HeaderMap,
188 request: &QueryRequest,
189) -> Option<(StatusCode, Json<QueryResponse>)> {
190 if !auth.require_token {
191 return None;
192 }
193 let token = match extract_bearer_token(headers) {
194 Some(token) => token,
195 None => {
196 return Some((
197 StatusCode::UNAUTHORIZED,
198 Json(QueryResponse::rejected("missing bearer token")),
199 ))
200 }
201 };
202 let Some(verifier) = &auth.verifier else {
203 return Some((
204 StatusCode::UNAUTHORIZED,
205 Json(QueryResponse::rejected("token verifier not configured")),
206 ));
207 };
208 let details = RequestValidationDetailsRef {
209 purpose: "query",
210 name: &request.query_name,
211 id: &request.query_id,
212 idempotency_key: None,
213 payload: RequestPayloadRef::Json(&request.payload),
214 subject: None,
215 audience: None,
216 };
217 match verifier.verify_query_token_with_borrowed_request(
218 token,
219 &request.query_name,
220 Some(&details),
221 ) {
222 Ok(()) => None,
223 Err(CommandTokenError::Forbidden) => Some((
224 StatusCode::FORBIDDEN,
225 Json(QueryResponse::rejected("query not allowed for token")),
226 )),
227 Err(_) => Some((
228 StatusCode::UNAUTHORIZED,
229 Json(QueryResponse::rejected("invalid bearer token")),
230 )),
231 }
232}
233
234fn owned_request_details(
235 details: Option<&RequestValidationDetailsRef<'_>>,
236) -> Result<Option<RequestValidationDetails>, CommandTokenError> {
237 details
238 .copied()
239 .map(RequestValidationDetailsRef::to_owned)
240 .transpose()
241 .map_err(|_| CommandTokenError::InvalidFormat)
242}
243
244pub(crate) fn authorize_status(
245 auth: &HttpCommandAuth,
246 headers: &HeaderMap,
247) -> Result<bool, StatusCode> {
248 let token_opt = extract_bearer_token(headers);
249 if !auth.public_status {
250 let Some(token) = token_opt else {
251 return Err(StatusCode::UNAUTHORIZED);
252 };
253 let Some(verifier) = &auth.verifier else {
254 return Err(StatusCode::UNAUTHORIZED);
255 };
256 match verifier.verify_query_token_with_request(token, "runtime.status", None) {
257 Ok(()) => Ok(true),
258 Err(CommandTokenError::Forbidden) => Err(StatusCode::FORBIDDEN),
259 Err(_) => Err(StatusCode::UNAUTHORIZED),
260 }
261 } else {
262 let Some(token) = token_opt else {
263 return Ok(false);
264 };
265 let Some(verifier) = &auth.verifier else {
266 return Ok(false);
267 };
268 match verifier.verify_query_token_with_request(token, "runtime.status", None) {
269 Ok(()) => Ok(true),
270 _ => Ok(false),
271 }
272 }
273}
274
275pub(crate) fn authorize_private_status(
276 auth: &HttpCommandAuth,
277 headers: &HeaderMap,
278 query_name: &str,
279) -> Result<(), StatusCode> {
280 let Some(token) = extract_bearer_token(headers) else {
281 return Err(StatusCode::UNAUTHORIZED);
282 };
283 let Some(verifier) = &auth.verifier else {
284 return Err(StatusCode::UNAUTHORIZED);
285 };
286 match verifier.verify_query_token_with_request(token, query_name, None) {
287 Ok(()) => Ok(()),
288 Err(CommandTokenError::Forbidden) => Err(StatusCode::FORBIDDEN),
289 Err(_) => Err(StatusCode::UNAUTHORIZED),
290 }
291}
292
293fn extract_bearer_token(headers: &HeaderMap) -> Option<&str> {
294 if headers.get_all("authorization").iter().count() != 1 {
295 return None;
296 }
297 let auth = headers.get("authorization")?;
298 let auth = auth.to_str().ok()?;
299 let token = auth.strip_prefix("Bearer ")?;
300 if token.is_empty() {
301 return None;
302 }
303 Some(token)
304}