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