1use crate::extract::RequestId;
4use crate::extract::{Auth, IdTag};
5use crate::prelude::*;
6use axum::{
7 body::Body,
8 extract::State,
9 http::{header, response::Response, Method, Request},
10 middleware::Next,
11};
12use cloudillo_types::auth_adapter::AuthCtx;
13use cloudillo_types::utils::random_id;
14use std::future::Future;
15use std::pin::Pin;
16
17const TENANT_API_KEY_PREFIX: &str = "cl_";
19
20const IDP_API_KEY_PREFIX: &str = "idp_";
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25enum ApiKeyType {
26 Tenant,
28 Idp,
30}
31
32fn get_api_key_type(token: &str) -> Option<ApiKeyType> {
34 if token.starts_with(TENANT_API_KEY_PREFIX) {
35 Some(ApiKeyType::Tenant)
36 } else if token.starts_with(IDP_API_KEY_PREFIX) {
37 Some(ApiKeyType::Idp)
38 } else {
39 None
40 }
41}
42
43pub type PermissionCheckInput =
45 (State<App>, Auth, axum::extract::Path<String>, Request<Body>, Next);
46pub type PermissionCheckOutput =
47 Pin<Box<dyn Future<Output = Result<axum::response::Response, Error>> + Send>>;
48
49#[derive(Clone)]
54pub struct PermissionCheckFactory<F>
55where
56 F: Fn(
57 State<App>,
58 Auth,
59 axum::extract::Path<String>,
60 Request<Body>,
61 Next,
62 ) -> PermissionCheckOutput
63 + Clone
64 + Send
65 + Sync,
66{
67 handler: F,
68}
69
70impl<F> PermissionCheckFactory<F>
71where
72 F: Fn(
73 State<App>,
74 Auth,
75 axum::extract::Path<String>,
76 Request<Body>,
77 Next,
78 ) -> PermissionCheckOutput
79 + Clone
80 + Send
81 + Sync,
82{
83 pub fn new(handler: F) -> Self {
84 Self { handler }
85 }
86
87 pub fn call(
88 &self,
89 state: State<App>,
90 auth: Auth,
91 path: axum::extract::Path<String>,
92 req: Request<Body>,
93 next: Next,
94 ) -> PermissionCheckOutput {
95 (self.handler)(state, auth, path, req, next)
96 }
97}
98
99fn extract_token_from_query(query: &str) -> Option<String> {
101 for param in query.split('&') {
102 if param.starts_with("token=") {
103 let token = param.strip_prefix("token=")?;
104 if !token.is_empty() {
105 return Some(token.to_string());
108 }
109 }
110 }
111 None
112}
113
114pub async fn require_auth(
115 State(state): State<App>,
116 mut req: Request<Body>,
117 next: Next,
118) -> ClResult<Response<Body>> {
119 let id_tag = req
121 .extensions()
122 .get::<IdTag>()
123 .ok_or_else(|| {
124 warn!("IdTag not found in request extensions");
125 Error::PermissionDenied
126 })?
127 .clone();
128
129 let tn_id = state.auth_adapter.read_tn_id(&id_tag.0).await.map_err(|_| {
131 warn!("Failed to resolve tenant ID for id_tag: {}", id_tag.0);
132 Error::PermissionDenied
133 })?;
134
135 let token = if let Some(auth_header) =
137 req.headers().get("Authorization").and_then(|h| h.to_str().ok())
138 {
139 if let Some(token) = auth_header.strip_prefix("Bearer ") {
140 token.trim().to_string()
141 } else {
142 warn!("Authorization header present but doesn't start with 'Bearer ': {}", auth_header);
143 return Err(Error::PermissionDenied);
144 }
145 } else {
146 let query_token = extract_token_from_query(req.uri().query().unwrap_or(""));
148 if query_token.is_none() {
149 warn!("No Authorization header and no token query parameter found");
150 }
151 query_token.ok_or(Error::PermissionDenied)?
152 };
153
154 let claims = match get_api_key_type(&token) {
156 Some(ApiKeyType::Tenant) => {
157 let validation = state.auth_adapter.validate_api_key(&token).await.map_err(|e| {
159 warn!("Tenant API key validation failed: {:?}", e);
160 Error::PermissionDenied
161 })?;
162
163 if validation.tn_id != tn_id {
165 warn!(
166 "API key tenant mismatch: key belongs to {:?} but request is for {:?}",
167 validation.tn_id, tn_id
168 );
169 return Err(Error::PermissionDenied);
170 }
171
172 AuthCtx {
173 tn_id: validation.tn_id,
174 id_tag: validation.id_tag,
175 roles: validation
176 .roles
177 .map(|r| r.split(',').map(Box::from).collect())
178 .unwrap_or_default(),
179 scope: validation.scopes,
180 }
181 }
182 Some(ApiKeyType::Idp) => {
183 let idp_adapter = state.idp_adapter.as_ref().ok_or_else(|| {
185 warn!("IDP API key used but Identity Provider not available");
186 Error::ServiceUnavailable("Identity Provider not available".to_string())
187 })?;
188
189 let auth_id_tag = idp_adapter
190 .verify_api_key(&token)
191 .await
192 .map_err(|e| {
193 warn!("IDP API key validation error: {:?}", e);
194 Error::PermissionDenied
195 })?
196 .ok_or_else(|| {
197 warn!("IDP API key validation failed: key not found or expired");
198 Error::PermissionDenied
199 })?;
200
201 AuthCtx {
202 tn_id, id_tag: auth_id_tag.into(),
204 roles: Box::new([]), scope: None,
206 }
207 }
208 None => {
209 state.auth_adapter.validate_access_token(tn_id, &token).await?
211 }
212 };
213
214 if let Some(ref scope) = claims.scope {
216 if let Some(token_scope) = cloudillo_types::types::TokenScope::parse(scope) {
217 let path = req.uri().path();
218 let method = req.method().clone();
219 let allowed = match token_scope {
220 cloudillo_types::types::TokenScope::File { .. } => {
221 path.starts_with("/api/files/")
222 || path == "/api/files"
223 || path.starts_with("/ws/rtdb/")
224 || path.starts_with("/ws/crdt/")
225 || path == "/api/auth/access-token"
226 }
227 cloudillo_types::types::TokenScope::ApkgPublish => {
228 path.starts_with("/api/files/apkg/")
229 || (path == "/api/actions" && method == Method::POST)
230 || path.starts_with("/api/apps")
231 }
232 };
233 if !allowed {
234 warn!(scope = %scope, path = %path, "Scoped token denied access to non-matching endpoint");
235 return Err(Error::PermissionDenied);
236 }
237 }
238 }
239
240 req.extensions_mut().insert(Auth(claims));
241
242 Ok(next.run(req).await)
243}
244
245pub async fn optional_auth(
246 State(state): State<App>,
247 mut req: Request<Body>,
248 next: Next,
249) -> ClResult<Response<Body>> {
250 let id_tag = req.extensions().get::<IdTag>().cloned();
252
253 let token = if let Some(auth_header) =
255 req.headers().get(header::AUTHORIZATION).and_then(|h| h.to_str().ok())
256 {
257 auth_header.strip_prefix("Bearer ").map(|token| token.trim().to_string())
258 } else if req.uri().path().starts_with("/ws/") || req.uri().path().starts_with("/api/files/") {
259 let query = req.uri().query().unwrap_or("");
261 extract_token_from_query(query)
262 } else {
263 None
264 };
265
266 if let (Some(id_tag), Some(ref token)) = (id_tag, token) {
268 match state.auth_adapter.read_tn_id(&id_tag.0).await {
270 Ok(tn_id) => {
271 let claims_result: Result<Result<AuthCtx, Error>, Error> =
273 match get_api_key_type(token) {
274 Some(ApiKeyType::Tenant) => {
275 state.auth_adapter.validate_api_key(token).await.map(|validation| {
277 if validation.tn_id != tn_id {
279 return Err(Error::PermissionDenied);
280 }
281 Ok(AuthCtx {
282 tn_id: validation.tn_id,
283 id_tag: validation.id_tag,
284 roles: validation
285 .roles
286 .map(|r| r.split(',').map(Box::from).collect())
287 .unwrap_or_default(),
288 scope: validation.scopes,
289 })
290 })
291 }
292 Some(ApiKeyType::Idp) => {
293 if let Some(idp_adapter) = state.idp_adapter.as_ref() {
295 match idp_adapter.verify_api_key(token).await {
296 Ok(Some(auth_id_tag)) => Ok(Ok(AuthCtx {
297 tn_id,
298 id_tag: auth_id_tag.into(),
299 roles: Box::new([]),
300 scope: None,
301 })),
302 Ok(None) => {
303 warn!("IDP API key validation failed: key not found or expired");
304 Err(Error::PermissionDenied)
305 }
306 Err(e) => {
307 warn!("IDP API key validation error: {:?}", e);
308 Err(Error::PermissionDenied)
309 }
310 }
311 } else {
312 warn!("IDP API key used but Identity Provider not available");
313 Err(Error::ServiceUnavailable(
314 "Identity Provider not available".to_string(),
315 ))
316 }
317 }
318 None => {
319 state.auth_adapter.validate_access_token(tn_id, token).await.map(Ok)
321 }
322 };
323
324 match claims_result {
325 Ok(Ok(claims)) => {
326 let scope_allowed = if let Some(ref scope) = claims.scope {
328 if let Some(token_scope) =
329 cloudillo_types::types::TokenScope::parse(scope)
330 {
331 let path = req.uri().path();
332 let method = req.method().clone();
333 match token_scope {
334 cloudillo_types::types::TokenScope::File { .. } => {
335 path.starts_with("/api/files/")
336 || path == "/api/files" || path.starts_with("/ws/rtdb/")
337 || path.starts_with("/ws/crdt/") || path
338 == "/api/auth/access-token"
339 }
340 cloudillo_types::types::TokenScope::ApkgPublish => {
344 path.starts_with("/api/files/apkg/")
345 || (path == "/api/actions" && method == Method::POST)
346 || path.starts_with("/api/apps")
347 }
348 }
349 } else {
350 false
351 }
352 } else {
353 true
354 };
355 if scope_allowed {
356 req.extensions_mut().insert(Auth(claims));
357 } else {
358 warn!("Scoped token denied access in optional_auth, treating as unauthenticated");
359 }
360 }
361 Ok(Err(e)) => {
362 warn!("Token validation failed (tenant mismatch): {:?}", e);
363 }
364 Err(e) => {
365 warn!("Token validation failed: {:?}", e);
366 }
367 }
368 }
369 Err(e) => {
370 warn!("Failed to resolve tenant ID: {:?}", e);
371 }
372 }
373 }
374
375 Ok(next.run(req).await)
376}
377
378pub async fn request_id_middleware(mut req: Request<Body>, next: Next) -> Response<Body> {
380 let request_id = req
382 .headers()
383 .get("X-Request-ID")
384 .and_then(|h| h.to_str().ok())
385 .map_or_else(|| format!("req_{}", random_id().unwrap_or_default()), ToString::to_string);
386
387 req.extensions_mut().insert(RequestId(request_id.clone()));
389
390 let mut response = next.run(req).await;
392
393 if let Ok(header_value) = request_id.parse() {
395 response.headers_mut().insert("X-Request-ID", header_value);
396 }
397
398 response
399}
400
401