/// Auth middleware helpers for HTTP handlers /// /// Provides utilities to: /// 1. Validate JWT tokens from requests /// 2. Extract claims /// 3. Check permissions /// 4. Return standardized auth errors use actix_web::{HttpRequest, HttpResponse}; use serde_json::json; use crate::auth::provider::{AuthProvider, AuthError}; use crate::auth::guard::{AuthGuard, PermissionGuard, Role}; /// Result type for auth operations pub type AuthResult = Result; /// Extract and validate bearer token from request pub async fn validate_request_token( req: &HttpRequest, auth_provider: &dyn AuthProvider, ) -> AuthResult { // Extract Authorization header let auth_header = req .headers() .get("Authorization") .and_then(|h| h.to_str().ok()) .ok_or(AuthError::MissingToken)?; // Extract token from "Bearer " let token = AuthGuard::extract_token(auth_header)?; // Validate token with provider auth_provider.validate_token(&token).await } /// Check if user has required role for resource pub fn check_resource_role( claims: &crate::auth::provider::Claims, resource_type: &str, resource_id: &str, required_role: Role, ) -> bool { let user_role = PermissionGuard::get_resource_role(claims, resource_type, resource_id) .unwrap_or(Role::User); user_role.satisfies(required_role) } /// Check if user belongs to required group pub fn check_group_membership( claims: &crate::auth::provider::Claims, required_group: &str, ) -> bool { PermissionGuard::check_group(claims, required_group) } /// Convert auth error to HTTP response pub fn auth_error_response(error: &AuthError) -> HttpResponse { let (status, message) = match error { AuthError::MissingToken => ("Unauthorized", "Missing or invalid Authorization header"), AuthError::InvalidSignature => ("Unauthorized", "Invalid token signature"), AuthError::ExpiredToken => ("Unauthorized", "Token has expired"), AuthError::InvalidIssuer => ("Unauthorized", "Invalid token issuer"), AuthError::AccessDenied => ("Forbidden", "Access denied for this resource"), AuthError::InvalidClaims => ("Unauthorized", "Invalid or missing required claims"), }; HttpResponse::build(match status { "Unauthorized" => actix_web::http::StatusCode::UNAUTHORIZED, "Forbidden" => actix_web::http::StatusCode::FORBIDDEN, _ => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, }) .json(json!({ "error": status, "message": message })) } #[cfg(test)] mod tests { use super::*; #[test] fn test_check_resource_role() { let claims = crate::auth::provider::Claims { sub: "user-1".to_string(), groups: vec![], attributes: serde_json::Map::new(), exp: 999999999, iat: 0, }; // User with no resource role defaults to Role::User assert!(check_resource_role(&claims, "memory", "proj-1", Role::User)); assert!(!check_resource_role(&claims, "memory", "proj-1", Role::Viewer)); } #[test] fn test_check_group_membership() { let claims = crate::auth::provider::Claims { sub: "user-1".to_string(), groups: vec!["admins".to_string(), "developers".to_string()], attributes: serde_json::Map::new(), exp: 999999999, iat: 0, }; assert!(check_group_membership(&claims, "admins")); assert!(check_group_membership(&claims, "developers")); assert!(!check_group_membership(&claims, "managers")); } #[test] fn test_auth_error_response() { let err = AuthError::MissingToken; let response = auth_error_response(&err); assert_eq!(response.status(), 401); } }