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, Default)]
24pub struct HttpCommandAuth {
26 pub require_token: bool,
28 pub public_status: bool,
30 pub verifier: Option<Arc<dyn CommandTokenVerifier>>,
32}
33
34pub trait CommandTokenVerifier: Send + Sync {
36 fn verify_command_token(
38 &self,
39 token: &str,
40 command_name: &str,
41 ) -> Result<(), CommandTokenError>;
42 fn verify_query_token(&self, token: &str, query_name: &str) -> Result<(), CommandTokenError>;
44
45 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 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}