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 require_token: bool,
30 pub public_status: bool,
32 pub verifier: Option<Arc<dyn CommandTokenVerifier>>,
34}
35
36impl Default for HttpCommandAuth {
37 fn default() -> Self {
38 Self {
39 require_token: true,
40 public_status: false,
41 verifier: None,
42 }
43 }
44}
45
46impl HttpCommandAuth {
47 pub fn insecure_local_for_testing() -> Self {
51 Self {
52 require_token: false,
53 public_status: false,
54 verifier: None,
55 }
56 }
57}
58
59pub trait CommandTokenVerifier: Send + Sync {
61 fn verify_command_token(
63 &self,
64 token: &str,
65 command_name: &str,
66 ) -> Result<(), CommandTokenError>;
67 fn verify_query_token(&self, token: &str, query_name: &str) -> Result<(), CommandTokenError>;
69
70 fn verify_command_token_with_request(
72 &self,
73 token: &str,
74 command_name: &str,
75 _details: Option<&RequestValidationDetails>,
76 ) -> Result<(), CommandTokenError> {
77 self.verify_command_token(token, command_name)
78 }
79
80 fn verify_query_token_with_request(
82 &self,
83 token: &str,
84 query_name: &str,
85 _details: Option<&RequestValidationDetails>,
86 ) -> Result<(), CommandTokenError> {
87 self.verify_query_token(token, query_name)
88 }
89
90 fn verify_command_token_with_borrowed_request(
96 &self,
97 token: &str,
98 command_name: &str,
99 details: Option<&RequestValidationDetailsRef<'_>>,
100 ) -> Result<(), CommandTokenError> {
101 let owned = owned_request_details(details)?;
102 self.verify_command_token_with_request(token, command_name, owned.as_ref())
103 }
104
105 fn verify_query_token_with_borrowed_request(
111 &self,
112 token: &str,
113 query_name: &str,
114 details: Option<&RequestValidationDetailsRef<'_>>,
115 ) -> Result<(), CommandTokenError> {
116 let owned = owned_request_details(details)?;
117 self.verify_query_token_with_request(token, query_name, owned.as_ref())
118 }
119}
120
121pub(crate) fn authorize_command(
122 auth: &HttpCommandAuth,
123 headers: &HeaderMap,
124 request: &CommandRequest,
125) -> Option<(StatusCode, Json<CommandResponse>)> {
126 if !auth.require_token {
127 return None;
128 }
129 let token = match extract_bearer_token(headers) {
130 Some(token) => token,
131 None => return Some(command_unauthorized("missing bearer token")),
132 };
133 let Some(verifier) = &auth.verifier else {
134 return Some(command_unauthorized("token verifier not configured"));
135 };
136 let details = RequestValidationDetailsRef {
137 purpose: "command",
138 name: &request.command_name,
139 id: &request.command_id,
140 idempotency_key: request.idempotency_key.as_deref(),
141 payload: RequestPayloadRef::Text(&request.payload),
142 subject: None,
143 audience: None,
144 };
145 match verifier.verify_command_token_with_borrowed_request(
146 token,
147 &request.command_name,
148 Some(&details),
149 ) {
150 Ok(()) => None,
151 Err(CommandTokenError::Forbidden) => {
152 Some(command_forbidden("command not allowed for token"))
153 }
154 Err(CommandTokenError::InvalidFormat | CommandTokenError::Unauthorized) => {
155 Some(command_unauthorized("invalid bearer token"))
156 }
157 }
158}
159
160pub(crate) fn authorize_query(
161 auth: &HttpCommandAuth,
162 headers: &HeaderMap,
163 request: &QueryRequest,
164) -> Option<(StatusCode, Json<QueryResponse>)> {
165 if !auth.require_token {
166 return None;
167 }
168 let token = match extract_bearer_token(headers) {
169 Some(token) => token,
170 None => {
171 return Some((
172 StatusCode::UNAUTHORIZED,
173 Json(QueryResponse::rejected("missing bearer token")),
174 ))
175 }
176 };
177 let Some(verifier) = &auth.verifier else {
178 return Some((
179 StatusCode::UNAUTHORIZED,
180 Json(QueryResponse::rejected("token verifier not configured")),
181 ));
182 };
183 let details = RequestValidationDetailsRef {
184 purpose: "query",
185 name: &request.query_name,
186 id: &request.query_id,
187 idempotency_key: None,
188 payload: RequestPayloadRef::Json(&request.payload),
189 subject: None,
190 audience: None,
191 };
192 match verifier.verify_query_token_with_borrowed_request(
193 token,
194 &request.query_name,
195 Some(&details),
196 ) {
197 Ok(()) => None,
198 Err(CommandTokenError::Forbidden) => Some((
199 StatusCode::FORBIDDEN,
200 Json(QueryResponse::rejected("query not allowed for token")),
201 )),
202 Err(_) => Some((
203 StatusCode::UNAUTHORIZED,
204 Json(QueryResponse::rejected("invalid bearer token")),
205 )),
206 }
207}
208
209fn owned_request_details(
210 details: Option<&RequestValidationDetailsRef<'_>>,
211) -> Result<Option<RequestValidationDetails>, CommandTokenError> {
212 details
213 .copied()
214 .map(RequestValidationDetailsRef::to_owned)
215 .transpose()
216 .map_err(|_| CommandTokenError::InvalidFormat)
217}
218
219pub(crate) fn authorize_status(
220 auth: &HttpCommandAuth,
221 headers: &HeaderMap,
222) -> Result<bool, StatusCode> {
223 let token_opt = extract_bearer_token(headers);
224 if !auth.public_status {
225 let Some(token) = token_opt else {
226 return Err(StatusCode::UNAUTHORIZED);
227 };
228 let Some(verifier) = &auth.verifier else {
229 return Err(StatusCode::UNAUTHORIZED);
230 };
231 match verifier.verify_query_token_with_request(token, "runtime.status", None) {
232 Ok(()) => Ok(true),
233 Err(CommandTokenError::Forbidden) => Err(StatusCode::FORBIDDEN),
234 Err(_) => Err(StatusCode::UNAUTHORIZED),
235 }
236 } else {
237 let Some(token) = token_opt else {
238 return Ok(false);
239 };
240 let Some(verifier) = &auth.verifier else {
241 return Ok(false);
242 };
243 match verifier.verify_query_token_with_request(token, "runtime.status", None) {
244 Ok(()) => Ok(true),
245 _ => Ok(false),
246 }
247 }
248}
249
250pub(crate) fn authorize_private_status(
251 auth: &HttpCommandAuth,
252 headers: &HeaderMap,
253 query_name: &str,
254) -> Result<(), StatusCode> {
255 let Some(token) = extract_bearer_token(headers) else {
256 return Err(StatusCode::UNAUTHORIZED);
257 };
258 let Some(verifier) = &auth.verifier else {
259 return Err(StatusCode::UNAUTHORIZED);
260 };
261 match verifier.verify_query_token_with_request(token, query_name, None) {
262 Ok(()) => Ok(()),
263 Err(CommandTokenError::Forbidden) => Err(StatusCode::FORBIDDEN),
264 Err(_) => Err(StatusCode::UNAUTHORIZED),
265 }
266}
267
268fn extract_bearer_token(headers: &HeaderMap) -> Option<&str> {
269 if headers.get_all("authorization").iter().count() != 1 {
270 return None;
271 }
272 let auth = headers.get("authorization")?;
273 let auth = auth.to_str().ok()?;
274 let token = auth.strip_prefix("Bearer ")?;
275 if token.is_empty() {
276 return None;
277 }
278 Some(token)
279}