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