1use 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)]
24pub struct HttpCommandAuth {
26 pub require_token: bool,
28 pub public_status: bool,
30 pub verifier: Option<Arc<dyn CommandTokenVerifier>>,
32}
33
34impl Default for HttpCommandAuth {
35 fn default() -> Self {
36 Self {
37 require_token: true,
38 public_status: false,
39 verifier: None,
40 }
41 }
42}
43
44impl HttpCommandAuth {
45 pub fn insecure_local_for_testing() -> Self {
49 Self {
50 require_token: false,
51 public_status: false,
52 verifier: None,
53 }
54 }
55}
56
57pub trait CommandTokenVerifier: Send + Sync {
59 fn verify_command_token(
61 &self,
62 token: &str,
63 command_name: &str,
64 ) -> Result<(), CommandTokenError>;
65 fn verify_query_token(&self, token: &str, query_name: &str) -> Result<(), CommandTokenError>;
67
68 fn verify_command_token_with_request(
70 &self,
71 token: &str,
72 command_name: &str,
73 _details: Option<&RequestValidationDetails>,
74 ) -> Result<(), CommandTokenError> {
75 self.verify_command_token(token, command_name)
76 }
77
78 fn verify_query_token_with_request(
80 &self,
81 token: &str,
82 query_name: &str,
83 _details: Option<&RequestValidationDetails>,
84 ) -> Result<(), CommandTokenError> {
85 self.verify_query_token(token, query_name)
86 }
87}
88
89pub(crate) fn authorize_command(
90 auth: &HttpCommandAuth,
91 headers: &HeaderMap,
92 request: &CommandRequest,
93) -> Option<(StatusCode, Json<CommandResponse>)> {
94 if !auth.require_token {
95 return None;
96 }
97 let token = match extract_bearer_token(headers) {
98 Some(token) => token,
99 None => return Some(command_unauthorized("missing bearer token")),
100 };
101 let Some(verifier) = &auth.verifier else {
102 return Some(command_unauthorized("token verifier not configured"));
103 };
104 let details = RequestValidationDetails {
105 purpose: "command".to_string(),
106 name: request.command_name.clone(),
107 id: request.command_id.clone(),
108 idempotency_key: request.idempotency_key.clone(),
109 payload: request.payload.clone(),
110 subject: None,
111 audience: None,
112 };
113 match verifier.verify_command_token_with_request(token, &request.command_name, Some(&details)) {
114 Ok(()) => None,
115 Err(CommandTokenError::Forbidden) => {
116 Some(command_forbidden("command not allowed for token"))
117 }
118 Err(CommandTokenError::InvalidFormat | CommandTokenError::Unauthorized) => {
119 Some(command_unauthorized("invalid bearer token"))
120 }
121 }
122}
123
124pub(crate) fn authorize_query(
125 auth: &HttpCommandAuth,
126 headers: &HeaderMap,
127 request: &QueryRequest,
128) -> Option<(StatusCode, Json<QueryResponse>)> {
129 if !auth.require_token {
130 return None;
131 }
132 let token = match extract_bearer_token(headers) {
133 Some(token) => token,
134 None => {
135 return Some((
136 StatusCode::UNAUTHORIZED,
137 Json(QueryResponse::rejected("missing bearer token")),
138 ))
139 }
140 };
141 let Some(verifier) = &auth.verifier else {
142 return Some((
143 StatusCode::UNAUTHORIZED,
144 Json(QueryResponse::rejected("token verifier not configured")),
145 ));
146 };
147 let details = RequestValidationDetails {
148 purpose: "query".to_string(),
149 name: request.query_name.clone(),
150 id: request.query_id.clone(),
151 idempotency_key: None,
152 payload: serde_json::to_string(&request.payload).unwrap_or_default(),
153 subject: None,
154 audience: None,
155 };
156 match verifier.verify_query_token_with_request(token, &request.query_name, Some(&details)) {
157 Ok(()) => None,
158 Err(CommandTokenError::Forbidden) => Some((
159 StatusCode::FORBIDDEN,
160 Json(QueryResponse::rejected("query not allowed for token")),
161 )),
162 Err(_) => Some((
163 StatusCode::UNAUTHORIZED,
164 Json(QueryResponse::rejected("invalid bearer token")),
165 )),
166 }
167}
168
169pub(crate) fn authorize_status(
170 auth: &HttpCommandAuth,
171 headers: &HeaderMap,
172) -> Result<bool, StatusCode> {
173 let token_opt = extract_bearer_token(headers);
174 if !auth.public_status {
175 let Some(token) = token_opt else {
176 return Err(StatusCode::UNAUTHORIZED);
177 };
178 let Some(verifier) = &auth.verifier else {
179 return Err(StatusCode::UNAUTHORIZED);
180 };
181 match verifier.verify_query_token_with_request(token, "runtime.status", None) {
182 Ok(()) => Ok(true),
183 Err(CommandTokenError::Forbidden) => Err(StatusCode::FORBIDDEN),
184 Err(_) => Err(StatusCode::UNAUTHORIZED),
185 }
186 } else {
187 let Some(token) = token_opt else {
188 return Ok(false);
189 };
190 let Some(verifier) = &auth.verifier else {
191 return Ok(false);
192 };
193 match verifier.verify_query_token_with_request(token, "runtime.status", None) {
194 Ok(()) => Ok(true),
195 _ => Ok(false),
196 }
197 }
198}
199
200pub(crate) fn authorize_private_status(
201 auth: &HttpCommandAuth,
202 headers: &HeaderMap,
203 query_name: &str,
204) -> Result<(), StatusCode> {
205 let Some(token) = extract_bearer_token(headers) else {
206 return Err(StatusCode::UNAUTHORIZED);
207 };
208 let Some(verifier) = &auth.verifier else {
209 return Err(StatusCode::UNAUTHORIZED);
210 };
211 match verifier.verify_query_token_with_request(token, query_name, None) {
212 Ok(()) => Ok(()),
213 Err(CommandTokenError::Forbidden) => Err(StatusCode::FORBIDDEN),
214 Err(_) => Err(StatusCode::UNAUTHORIZED),
215 }
216}
217
218fn extract_bearer_token(headers: &HeaderMap) -> Option<&str> {
219 if headers.get_all("authorization").iter().count() != 1 {
220 return None;
221 }
222 let auth = headers.get("authorization")?;
223 let auth = auth.to_str().ok()?;
224 let token = auth.strip_prefix("Bearer ")?;
225 if token.is_empty() {
226 return None;
227 }
228 Some(token)
229}