// 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::KeystoneClient; use rustfs_policy::policy::Policy; use std::collections::HashMap; use std::sync::Arc; use tracing::{debug, info}; /// Maps Keystone identities to RustFS concepts pub struct KeystoneIdentityMapper { #[allow(dead_code)] client: Arc, role_policy_map: HashMap, enable_tenant_prefix: bool, } impl KeystoneIdentityMapper { /// Create new identity mapper pub fn new(client: Arc, enable_tenant_prefix: bool) -> Self { let mut role_policy_map = HashMap::new(); // Default Keystone role mappings role_policy_map.insert("admin".to_string(), "AdminPolicy".to_string()); role_policy_map.insert("Admin".to_string(), "AdminPolicy".to_string()); role_policy_map.insert("Member".to_string(), "ReadWritePolicy".to_string()); role_policy_map.insert("_member_".to_string(), "ReadOnlyPolicy".to_string()); role_policy_map.insert("ResellerAdmin".to_string(), "AdminPolicy".to_string()); role_policy_map.insert("SwiftOperator".to_string(), "ReadWritePolicy".to_string()); role_policy_map.insert("objectstore:admin".to_string(), "AdminPolicy".to_string()); role_policy_map.insert("objectstore:creator".to_string(), "ReadWritePolicy".to_string()); Self { client, role_policy_map, enable_tenant_prefix, } } /// Add custom role-to-policy mapping pub fn add_role_mapping(&mut self, keystone_role: String, rustfs_policy: String) { info!("Adding role mapping: {} -> {}", keystone_role, rustfs_policy); self.role_policy_map.insert(keystone_role, rustfs_policy); } /// Add multiple role mappings pub fn add_role_mappings(&mut self, mappings: Vec<(String, String)>) { for (role, policy) in mappings { self.add_role_mapping(role, policy); } } /// Map Keystone roles to RustFS policy names pub fn map_roles_to_policies(&self, roles: &[String]) -> Vec { let policies: Vec = roles .iter() .filter_map(|role| self.role_policy_map.get(role).cloned()) .collect(); debug!("Mapped roles {:?} to policies {:?}", roles, policies); policies } /// Generate tenant-prefixed bucket name /// Format: : pub fn apply_tenant_prefix(&self, bucket: &str, project_id: Option<&str>) -> String { if !self.enable_tenant_prefix { return bucket.to_string(); } if let Some(proj_id) = project_id { let prefixed = format!("{}:{}", proj_id, bucket); debug!("Applied tenant prefix: {} -> {}", bucket, prefixed); prefixed } else { bucket.to_string() } } /// Remove tenant prefix from bucket name pub fn remove_tenant_prefix(&self, prefixed_bucket: &str, project_id: Option<&str>) -> String { if !self.enable_tenant_prefix { return prefixed_bucket.to_string(); } if let Some(proj_id) = project_id { let prefix = format!("{}:", proj_id); if prefixed_bucket.starts_with(&prefix) { let unprefixed = prefixed_bucket[prefix.len()..].to_string(); debug!("Removed tenant prefix: {} -> {}", prefixed_bucket, unprefixed); return unprefixed; } } prefixed_bucket.to_string() } /// Check if bucket belongs to project pub fn is_project_bucket(&self, bucket: &str, project_id: Option<&str>) -> bool { if !self.enable_tenant_prefix { return true; // No multi-tenancy, all buckets accessible } if let Some(proj_id) = project_id { let prefix = format!("{}:", proj_id); bucket.starts_with(&prefix) } else { !bucket.contains(':') // No project ID, only unprefixed buckets } } /// Extract project ID from prefixed bucket name pub fn extract_project_id(&self, bucket: &str) -> Option { if !self.enable_tenant_prefix { return None; } bucket.find(':').map(|pos| bucket[..pos].to_string()) } /// Create default policies for Keystone roles pub fn create_default_policies(&self) -> HashMap { let mut policies = HashMap::new(); // Admin policy - full access let admin_json = r#"{ "Version": "2012-10-17", "ID": "AdminPolicy", "Statement": [{ "Sid": "AdminFullAccess", "Effect": "Allow", "Action": ["s3:*"], "Resource": ["arn:aws:s3:::*"] }] }"#; if let Ok(policy) = serde_json::from_str::(admin_json) { policies.insert("AdminPolicy".to_string(), policy); } // ReadWrite policy - read/write access let readwrite_json = r#"{ "Version": "2012-10-17", "ID": "ReadWritePolicy", "Statement": [{ "Sid": "ReadWriteAccess", "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket", "s3:GetBucketLocation", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts", "s3:AbortMultipartUpload" ], "Resource": ["arn:aws:s3:::*"] }] }"#; if let Ok(policy) = serde_json::from_str::(readwrite_json) { policies.insert("ReadWritePolicy".to_string(), policy); } // ReadOnly policy - read-only access let readonly_json = r#"{ "Version": "2012-10-17", "ID": "ReadOnlyPolicy", "Statement": [{ "Sid": "ReadOnlyAccess", "Effect": "Allow", "Action": [ "s3:GetObject", "s3:ListBucket", "s3:GetBucketLocation" ], "Resource": ["arn:aws:s3:::*"] }] }"#; if let Ok(policy) = serde_json::from_str::(readonly_json) { policies.insert("ReadOnlyPolicy".to_string(), policy); } policies } /// Check if user has permission based on Keystone roles pub fn has_permission(&self, roles: &[String], action: &str, _resource: &str) -> bool { // Admin always has access if roles.iter().any(|r| r.eq_ignore_ascii_case("admin") || r == "ResellerAdmin") { return true; } // Check role-based permissions for role in roles { if let Some(policy_name) = self.role_policy_map.get(role) { match policy_name.as_str() { "AdminPolicy" => return true, "ReadWritePolicy" => { if action.starts_with("s3:Get") || action.starts_with("s3:Put") || action.starts_with("s3:Delete") || action.starts_with("s3:List") { return true; } } "ReadOnlyPolicy" => { if action.starts_with("s3:Get") || action.starts_with("s3:List") { return true; } } _ => continue, } } } false } /// Check if tenant prefixing is enabled pub fn is_tenant_prefix_enabled(&self) -> bool { self.enable_tenant_prefix } } #[cfg(test)] mod tests { use super::*; use crate::KeystoneVersion; fn create_mapper() -> KeystoneIdentityMapper { let client = KeystoneClient::new( "http://localhost:5000".to_string(), KeystoneVersion::V3, None, None, None, "Default".to_string(), true, ); KeystoneIdentityMapper::new(Arc::new(client), true) } #[test] fn test_tenant_prefix() { let mapper = create_mapper(); let prefixed = mapper.apply_tenant_prefix("mybucket", Some("proj123")); assert_eq!(prefixed, "proj123:mybucket"); let unprefixed = mapper.remove_tenant_prefix("proj123:mybucket", Some("proj123")); assert_eq!(unprefixed, "mybucket"); // No project ID let no_prefix = mapper.apply_tenant_prefix("mybucket", None); assert_eq!(no_prefix, "mybucket"); } #[test] fn test_is_project_bucket() { let mapper = create_mapper(); assert!(mapper.is_project_bucket("proj123:mybucket", Some("proj123"))); assert!(!mapper.is_project_bucket("proj456:mybucket", Some("proj123"))); assert!(!mapper.is_project_bucket("mybucket", Some("proj123"))); } #[test] fn test_extract_project_id() { let mapper = create_mapper(); assert_eq!(mapper.extract_project_id("proj123:mybucket"), Some("proj123".to_string())); assert_eq!(mapper.extract_project_id("mybucket"), None); } #[test] fn test_role_mapping() { let mapper = create_mapper(); let roles = vec!["Member".to_string(), "admin".to_string()]; let policies = mapper.map_roles_to_policies(&roles); assert!(policies.contains(&"ReadWritePolicy".to_string())); assert!(policies.contains(&"AdminPolicy".to_string())); } #[test] fn test_has_permission() { let mapper = create_mapper(); // Admin has all permissions assert!(mapper.has_permission(&["admin".to_string()], "s3:DeleteBucket", "")); // Member has read/write permissions assert!(mapper.has_permission(&["Member".to_string()], "s3:PutObject", "")); assert!(mapper.has_permission(&["Member".to_string()], "s3:GetObject", "")); // _member_ has read-only permissions assert!(mapper.has_permission(&["_member_".to_string()], "s3:GetObject", "")); assert!(!mapper.has_permission(&["_member_".to_string()], "s3:PutObject", "")); } #[test] fn test_add_role_mapping() { let mut mapper = create_mapper(); mapper.add_role_mapping("CustomRole".to_string(), "CustomPolicy".to_string()); let policies = mapper.map_roles_to_policies(&["CustomRole".to_string()]); assert_eq!(policies, vec!["CustomPolicy".to_string()]); } }