// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use crate::{EC2Credential, KeystoneClient, KeystoneError, KeystoneToken, Result, TokenCache}; use rustfs_credentials::Credentials; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use tracing::{debug, info}; /// Keystone authentication provider /// /// This provider validates credentials against OpenStack Keystone /// and maps Keystone identities to RustFS credentials. pub struct KeystoneAuthProvider { client: Arc, token_cache: TokenCache, ec2_cache: TokenCache, enable_cache: bool, } impl KeystoneAuthProvider { /// Create new authentication provider pub fn new(client: KeystoneClient, cache_size: u64, cache_ttl: Duration, enable_cache: bool) -> Self { Self { client: Arc::new(client), token_cache: TokenCache::new(cache_size, cache_ttl), ec2_cache: TokenCache::new(cache_size, cache_ttl), enable_cache, } } /// Disable caching (for testing) pub fn without_cache(mut self) -> Self { self.enable_cache = false; self } /// Authenticate using Keystone token (X-Auth-Token header) pub async fn authenticate_with_token(&self, token: &str) -> Result { // Check cache first if self.enable_cache && let Some(cached_token) = self.token_cache.get(token).await && !cached_token.is_expired() { debug!("Token cache hit: user_id={}", cached_token.user_id); return Ok(self.keystone_token_to_credentials(&cached_token)); } if self.enable_cache { debug!("Cached token expired or not found, re-validating"); } // Validate token with Keystone let keystone_token = self.client.validate_token(token).await?; // Check expiration if keystone_token.is_expired() { return Err(KeystoneError::TokenExpired); } // Cache token if self.enable_cache { self.token_cache .insert(token.to_string(), Arc::new(keystone_token.clone())) .await; } info!( "Keystone authentication successful: user={}, project={:?}", keystone_token.username, keystone_token.project_name ); Ok(self.keystone_token_to_credentials(&keystone_token)) } /// Authenticate using EC2 credentials (S3 API with AWS SigV4) pub async fn authenticate_with_ec2(&self, access_key: &str, signature: &str, string_to_sign: &str) -> Result { // Check cache let cache_key = format!("{}:{}", access_key, signature); if self.enable_cache && let Some(cached) = self.ec2_cache.get(&cache_key).await && !cached.is_expired() { debug!("EC2 credential cache hit: access_key={}", access_key); return Ok(self.keystone_token_to_credentials(&cached)); } // Validate EC2 credentials with Keystone let ec2_cred = self .client .validate_ec2_credentials(access_key, signature, string_to_sign) .await?; // Convert to Keystone token (need to get full token info) let keystone_token = self.ec2_to_keystone_token(&ec2_cred).await?; // Cache if self.enable_cache { self.ec2_cache.insert(cache_key, Arc::new(keystone_token.clone())).await; } info!( "EC2 credential authentication successful: user={}, access_key={}", ec2_cred.user_id, access_key ); Ok(self.keystone_token_to_credentials(&keystone_token)) } /// Convert EC2 credential to Keystone token async fn ec2_to_keystone_token(&self, ec2_cred: &EC2Credential) -> Result { // In a real implementation, you'd need to: // 1. Use admin credentials to get user/project details // 2. Or maintain a mapping table // For simplicity, construct a minimal token Ok(KeystoneToken { token: String::new(), user_id: ec2_cred.user_id.clone(), username: ec2_cred.user_id.clone(), // Use user_id as username project_id: ec2_cred.project_id.clone(), project_name: ec2_cred.project_id.clone(), domain_id: None, domain_name: None, roles: vec!["Member".to_string()], // Default role expires_at: time::OffsetDateTime::now_utc() + time::Duration::hours(24), issued_at: time::OffsetDateTime::now_utc(), }) } /// Convert Keystone token to RustFS credentials fn keystone_token_to_credentials(&self, token: &KeystoneToken) -> Credentials { use serde_json::json; // Map Keystone roles to RustFS groups let groups = Some(token.roles.clone()); // Add Keystone-specific claims let mut claims = HashMap::new(); claims.insert("keystone_user_id".to_string(), json!(token.user_id)); claims.insert("keystone_username".to_string(), json!(token.username)); if let Some(ref proj_id) = token.project_id { claims.insert("keystone_project_id".to_string(), json!(proj_id)); } if let Some(ref proj_name) = token.project_name { claims.insert("keystone_project_name".to_string(), json!(proj_name)); } if let Some(ref dom_id) = token.domain_id { claims.insert("keystone_domain_id".to_string(), json!(dom_id)); } if let Some(ref dom_name) = token.domain_name { claims.insert("keystone_domain_name".to_string(), json!(dom_name)); } claims.insert("keystone_roles".to_string(), json!(token.roles)); claims.insert("auth_source".to_string(), json!("keystone")); Credentials { access_key: format!("keystone:{}", token.user_id), secret_key: String::new(), // Not used for token auth session_token: token.token.clone(), expiration: Some(token.expires_at), status: "active".to_string(), parent_user: token.username.clone(), groups, claims: Some(claims), name: Some(token.username.clone()), description: Some(format!("Keystone user: {}", token.username)), } } /// Invalidate cached token pub async fn invalidate_token(&self, token: &str) { self.token_cache.invalidate(token).await; } /// Clear all caches pub async fn clear_caches(&self) { self.token_cache.clear().await; self.ec2_cache.clear().await; } /// Check if user has admin privileges pub fn is_admin(&self, cred: &Credentials) -> bool { cred.groups .as_ref() .map(|groups| { groups .iter() .any(|g| g.eq_ignore_ascii_case("admin") || g.eq_ignore_ascii_case("reseller_admin")) }) .unwrap_or(false) } /// Extract project ID from credentials pub fn get_project_id(&self, cred: &Credentials) -> Option { cred.claims .as_ref() .and_then(|claims| claims.get("keystone_project_id")) .and_then(|v| v.as_str()) .map(String::from) } /// Extract user ID from credentials pub fn get_user_id(&self, cred: &Credentials) -> Option { cred.claims .as_ref() .and_then(|claims| claims.get("keystone_user_id")) .and_then(|v| v.as_str()) .map(String::from) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_keystone_token_to_credentials() { let client = KeystoneClient::new( "http://localhost:5000".to_string(), crate::KeystoneVersion::V3, None, None, None, "Default".to_string(), true, ); let provider = KeystoneAuthProvider::new(client, 100, Duration::from_secs(60), true); let token = KeystoneToken { token: "test-token".to_string(), user_id: "user123".to_string(), username: "testuser".to_string(), project_id: Some("proj456".to_string()), project_name: Some("testproject".to_string()), domain_id: Some("default".to_string()), domain_name: Some("Default".to_string()), roles: vec!["Member".to_string(), "admin".to_string()], expires_at: time::OffsetDateTime::now_utc() + time::Duration::hours(1), issued_at: time::OffsetDateTime::now_utc(), }; let cred = provider.keystone_token_to_credentials(&token); assert_eq!(cred.access_key, "keystone:user123"); assert_eq!(cred.parent_user, "testuser"); assert_eq!(cred.groups, Some(vec!["Member".to_string(), "admin".to_string()])); assert!(cred.claims.is_some()); let claims = cred.claims.unwrap(); assert_eq!(claims.get("keystone_user_id").unwrap().as_str().unwrap(), "user123"); assert_eq!(claims.get("keystone_project_id").unwrap().as_str().unwrap(), "proj456"); } #[test] fn test_is_admin() { let client = KeystoneClient::new( "http://localhost:5000".to_string(), crate::KeystoneVersion::V3, None, None, None, "Default".to_string(), true, ); let provider = KeystoneAuthProvider::new(client, 100, Duration::from_secs(60), true); let mut cred = Credentials { groups: Some(vec!["Member".to_string()]), ..Default::default() }; assert!(!provider.is_admin(&cred)); cred.groups = Some(vec!["admin".to_string()]); assert!(provider.is_admin(&cred)); cred.groups = Some(vec!["Admin".to_string()]); assert!(provider.is_admin(&cred)); } }