1use crate::extract::RequestId;
7use crate::extract::{Auth, IdTag};
8use crate::prelude::*;
9use axum::{
10 body::Body,
11 extract::State,
12 http::{Request, header, response::Response},
13 middleware::Next,
14};
15use cloudillo_types::auth_adapter::AuthCtx;
16use cloudillo_types::types::TokenScope;
17use std::pin::Pin;
18
19const TENANT_API_KEY_PREFIX: &str = "cl_";
21
22const IDP_API_KEY_PREFIX: &str = "idp_";
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27enum ApiKeyType {
28 Tenant,
30 Idp,
32}
33
34fn get_api_key_type(token: &str) -> Option<ApiKeyType> {
36 if token.starts_with(TENANT_API_KEY_PREFIX) {
37 Some(ApiKeyType::Tenant)
38 } else if token.starts_with(IDP_API_KEY_PREFIX) {
39 Some(ApiKeyType::Idp)
40 } else {
41 None
42 }
43}
44
45pub type PermissionCheckInput =
47 (State<App>, Auth, axum::extract::Path<String>, Request<Body>, Next);
48pub type PermissionCheckOutput =
49 Pin<Box<dyn Future<Output = Result<axum::response::Response, Error>> + Send>>;
50
51#[derive(Clone)]
56pub struct PermissionCheckFactory<F>
57where
58 F: Fn(
59 State<App>,
60 Auth,
61 axum::extract::Path<String>,
62 Request<Body>,
63 Next,
64 ) -> PermissionCheckOutput
65 + Clone
66 + Send
67 + Sync,
68{
69 handler: F,
70}
71
72impl<F> PermissionCheckFactory<F>
73where
74 F: Fn(
75 State<App>,
76 Auth,
77 axum::extract::Path<String>,
78 Request<Body>,
79 Next,
80 ) -> PermissionCheckOutput
81 + Clone
82 + Send
83 + Sync,
84{
85 pub fn new(handler: F) -> Self {
86 Self { handler }
87 }
88
89 pub fn call(
90 &self,
91 state: State<App>,
92 auth: Auth,
93 path: axum::extract::Path<String>,
94 req: Request<Body>,
95 next: Next,
96 ) -> PermissionCheckOutput {
97 (self.handler)(state, auth, path, req, next)
98 }
99}
100
101fn extract_token_from_query(query: &str) -> Option<String> {
103 for param in query.split('&') {
104 if param.starts_with("token=") {
105 let token = param.strip_prefix("token=")?;
106 if !token.is_empty() {
107 return Some(token.to_string());
110 }
111 }
112 }
113 None
114}
115
116pub async fn require_leader(
126 Auth(auth_ctx): Auth,
127 req: Request<Body>,
128 next: Next,
129) -> ClResult<Response<Body>> {
130 if auth_ctx.scope.as_deref().and_then(TokenScope::parse).is_some() {
131 warn!(
132 subject = %auth_ctx.id_tag,
133 scope = ?auth_ctx.scope,
134 "Owner/leader permission denied - delegated token"
135 );
136 return Err(Error::PermissionDenied);
137 }
138 if !crate::roles::is_leader(&auth_ctx.roles) {
139 warn!(
140 subject = %auth_ctx.id_tag,
141 roles = ?auth_ctx.roles,
142 "Owner/leader permission denied"
143 );
144 return Err(Error::PermissionDenied);
145 }
146 Ok(next.run(req).await)
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151enum ScopeGate {
152 Enforce,
154 Skip,
156}
157
158async fn authenticate(
170 state: App,
171 mut req: Request<Body>,
172 next: Next,
173 gate: ScopeGate,
174) -> ClResult<Response<Body>> {
175 let id_tag = req
177 .extensions()
178 .get::<IdTag>()
179 .ok_or_else(|| {
180 warn!("IdTag not found in request extensions");
181 Error::PermissionDenied
182 })?
183 .clone();
184
185 let tn_id = state.auth_adapter.read_tn_id(&id_tag.0).await.map_err(|_| {
187 warn!("Failed to resolve tenant ID for id_tag: {}", id_tag.0);
188 Error::PermissionDenied
189 })?;
190
191 let token = if let Some(auth_header) =
193 req.headers().get("Authorization").and_then(|h| h.to_str().ok())
194 {
195 if let Some(token) = auth_header.strip_prefix("Bearer ") {
196 token.trim().to_string()
197 } else {
198 warn!("Authorization header present but doesn't start with 'Bearer ': {}", auth_header);
199 return Err(Error::PermissionDenied);
200 }
201 } else {
202 let query_token = extract_token_from_query(req.uri().query().unwrap_or(""));
204 if query_token.is_none() {
205 warn!("No Authorization header and no token query parameter found");
206 }
207 query_token.ok_or(Error::PermissionDenied)?
208 };
209
210 let claims = match get_api_key_type(&token) {
212 Some(ApiKeyType::Tenant) => {
213 let validation = state.auth_adapter.validate_api_key(&token).await.map_err(|e| {
215 warn!("Tenant API key validation failed: {:?}", e);
216 Error::PermissionDenied
217 })?;
218
219 if validation.tn_id != tn_id {
221 warn!(
222 "API key tenant mismatch: key belongs to {:?} but request is for {:?}",
223 validation.tn_id, tn_id
224 );
225 return Err(Error::PermissionDenied);
226 }
227
228 AuthCtx {
229 tn_id: validation.tn_id,
230 id_tag: validation.id_tag,
231 roles: validation.roles.map(|r| crate::roles::parse_roles(&r)).unwrap_or_default(),
232 scope: validation.scopes,
233 anonymous: false,
234 }
235 }
236 Some(ApiKeyType::Idp) => {
237 let idp_adapter = state.idp_adapter.as_ref().ok_or_else(|| {
239 warn!("IDP API key used but Identity Provider not available");
240 Error::ServiceUnavailable("Identity Provider not available".to_string())
241 })?;
242
243 let auth_id_tag = idp_adapter
244 .verify_api_key(&token)
245 .await
246 .map_err(|e| {
247 warn!("IDP API key validation error: {:?}", e);
248 Error::PermissionDenied
249 })?
250 .ok_or_else(|| {
251 warn!("IDP API key validation failed: key not found or expired");
252 Error::PermissionDenied
253 })?;
254
255 AuthCtx {
256 tn_id, id_tag: auth_id_tag.into(),
258 roles: Box::new([]), scope: None,
260 anonymous: false,
261 }
262 }
263 None => {
264 state.auth_adapter.validate_access_token(tn_id, &id_tag.0, &token).await?
266 }
267 };
268
269 if gate == ScopeGate::Enforce
272 && !crate::scope::scope_permits(claims.scope.as_deref(), req.method(), req.uri().path())
273 {
274 warn!(
275 scope = ?claims.scope,
276 path = %req.uri().path(),
277 "Scoped token denied access to non-matching endpoint"
278 );
279 return Err(Error::PermissionDenied);
280 }
281
282 req.extensions_mut().insert(Auth(claims));
283
284 Ok(next.run(req).await)
285}
286
287pub async fn require_auth(
288 State(state): State<App>,
289 req: Request<Body>,
290 next: Next,
291) -> ClResult<Response<Body>> {
292 authenticate(state, req, next, ScopeGate::Enforce).await
293}
294
295pub async fn require_auth_public_data(
333 State(state): State<App>,
334 req: Request<Body>,
335 next: Next,
336) -> ClResult<Response<Body>> {
337 authenticate(state, req, next, ScopeGate::Skip).await
338}
339
340pub async fn optional_auth(
341 State(state): State<App>,
342 mut req: Request<Body>,
343 next: Next,
344) -> ClResult<Response<Body>> {
345 let id_tag = req.extensions().get::<IdTag>().cloned();
347
348 let token = if let Some(auth_header) =
350 req.headers().get(header::AUTHORIZATION).and_then(|h| h.to_str().ok())
351 {
352 auth_header.strip_prefix("Bearer ").map(|token| token.trim().to_string())
353 } else if req.uri().path().starts_with("/ws/") || req.uri().path().starts_with("/api/files/") {
354 let query = req.uri().query().unwrap_or("");
356 extract_token_from_query(query)
357 } else {
358 None
359 };
360
361 if let (Some(id_tag), Some(ref token)) = (id_tag, token) {
363 match state.auth_adapter.read_tn_id(&id_tag.0).await {
365 Ok(tn_id) => {
366 let claims_result: Result<Result<AuthCtx, Error>, Error> =
368 match get_api_key_type(token) {
369 Some(ApiKeyType::Tenant) => {
370 state.auth_adapter.validate_api_key(token).await.map(|validation| {
372 if validation.tn_id != tn_id {
374 return Err(Error::PermissionDenied);
375 }
376 Ok(AuthCtx {
377 tn_id: validation.tn_id,
378 id_tag: validation.id_tag,
379 roles: validation
380 .roles
381 .map(|r| crate::roles::parse_roles(&r))
382 .unwrap_or_default(),
383 scope: validation.scopes,
384 anonymous: false,
385 })
386 })
387 }
388 Some(ApiKeyType::Idp) => {
389 if let Some(idp_adapter) = state.idp_adapter.as_ref() {
391 match idp_adapter.verify_api_key(token).await {
392 Ok(Some(auth_id_tag)) => Ok(Ok(AuthCtx {
393 tn_id,
394 id_tag: auth_id_tag.into(),
395 roles: Box::new([]),
396 scope: None,
397 anonymous: false,
398 })),
399 Ok(None) => {
400 warn!(
401 "IDP API key validation failed: key not found or expired"
402 );
403 Err(Error::PermissionDenied)
404 }
405 Err(e) => {
406 warn!("IDP API key validation error: {:?}", e);
407 Err(Error::PermissionDenied)
408 }
409 }
410 } else {
411 warn!("IDP API key used but Identity Provider not available");
412 Err(Error::ServiceUnavailable(
413 "Identity Provider not available".to_string(),
414 ))
415 }
416 }
417 None => {
418 state
420 .auth_adapter
421 .validate_access_token(tn_id, &id_tag.0, token)
422 .await
423 .map(Ok)
424 }
425 };
426
427 match claims_result {
428 Ok(Ok(claims)) => {
429 let allowed = crate::scope::scope_permits(
432 claims.scope.as_deref(),
433 req.method(),
434 req.uri().path(),
435 );
436 if allowed {
437 req.extensions_mut().insert(Auth(claims));
438 } else {
439 warn!(
440 scope = ?claims.scope,
441 path = %req.uri().path(),
442 "Scoped token denied access in optional_auth, treating as unauthenticated"
443 );
444 }
445 }
446 Ok(Err(e)) => {
447 warn!("Token validation failed (tenant mismatch): {:?}", e);
448 }
449 Err(e) => {
450 warn!("Token validation failed: {:?}", e);
451 }
452 }
453 }
454 Err(e) => {
455 warn!("Failed to resolve tenant ID: {:?}", e);
456 }
457 }
458 }
459
460 Ok(next.run(req).await)
461}
462
463pub async fn request_id_middleware(mut req: Request<Body>, next: Next) -> Response<Body> {
472 let span = RequestId::install(&mut req);
473 let request_id = req.extensions().get::<RequestId>().map(|r| r.0.clone()).unwrap_or_default();
474
475 let mut response = {
476 use tracing::Instrument;
477 next.run(req).instrument(span).await
478 };
479
480 if let Ok(header_value) = request_id.parse() {
481 response.headers_mut().insert("X-Request-ID", header_value);
482 }
483 response
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use axum::{Router, http::StatusCode, middleware, routing::get};
490 use tower::ServiceExt;
491
492 fn auth_ctx(roles: &[&str], scope: Option<&str>) -> AuthCtx {
493 AuthCtx {
494 tn_id: TnId(1),
495 id_tag: "alice.example.com".into(),
496 roles: roles.iter().map(|r| Box::from(*r)).collect(),
497 scope: scope.map(Box::from),
498 anonymous: false,
499 }
500 }
501
502 async fn run_require_leader(auth: Option<AuthCtx>) -> StatusCode {
505 let app: Router = Router::new()
506 .route("/x", get(|| async { "ok" }))
507 .layer(middleware::from_fn(require_leader));
508
509 let mut req = Request::builder().uri("/x").body(Body::empty()).expect("build request");
510 if let Some(ctx) = auth {
511 req.extensions_mut().insert(Auth(ctx));
512 }
513
514 app.oneshot(req).await.expect("router responds").status()
515 }
516
517 #[tokio::test]
518 async fn require_leader_allows_leader() {
519 assert_eq!(run_require_leader(Some(auth_ctx(&["leader"], None))).await, StatusCode::OK);
520 }
521
522 #[tokio::test]
523 async fn require_leader_denies_non_leader_roles() {
524 assert_eq!(
525 run_require_leader(Some(auth_ctx(&["contributor"], None))).await,
526 StatusCode::FORBIDDEN
527 );
528 }
529
530 #[tokio::test]
531 async fn require_leader_denies_role_less_principal() {
532 assert_eq!(run_require_leader(Some(auth_ctx(&[], None))).await, StatusCode::FORBIDDEN);
534 }
535
536 #[tokio::test]
537 async fn require_leader_denies_delegated_token_with_leader_roles() {
538 assert_eq!(
541 run_require_leader(Some(auth_ctx(&["leader"], Some("file:f1~abc:W")))).await,
542 StatusCode::FORBIDDEN
543 );
544 }
545
546 #[tokio::test]
547 async fn require_leader_allows_capability_token_with_leader_roles() {
548 assert_eq!(
551 run_require_leader(Some(auth_ctx(&["leader"], Some("carddav:read")))).await,
552 StatusCode::OK
553 );
554 }
555
556 #[tokio::test]
557 async fn require_leader_denies_missing_auth() {
558 assert_eq!(run_require_leader(None).await, StatusCode::FORBIDDEN);
560 }
561
562 #[test]
566 fn file_scope_needs_the_permissive_tier_for_the_batch_route() {
567 use crate::scope::scope_permits;
568 use axum::http::Method;
569
570 let s = Some("file:f1~abc:R");
571
572 assert!(!scope_permits(s, &Method::GET, "/api/profiles/batch"));
575
576 assert!(!scope_permits(s, &Method::GET, "/api/profiles/alice.example.com"));
579 assert!(!scope_permits(s, &Method::PATCH, "/api/profiles/alice.example.com"));
580 assert!(!scope_permits(s, &Method::POST, "/api/profiles/alice.example.com/refresh"));
581 assert!(!scope_permits(s, &Method::PUT, "/api/profiles/alice.example.com"));
582 assert!(!scope_permits(s, &Method::GET, "/api/profiles"));
583 }
584}
585
586