//! Role storage //! //! Stores and retrieves roles and their permissions. use std::sync::Arc; use iam_types::{builtin_roles, Error, IamError, Result, Role, Scope}; use crate::backend::{Backend, CasResult, JsonStore, StorageBackend}; /// Key prefixes for role storage mod keys { /// Primary key: by name /// Format: iam/roles/{name} pub const ROLES: &str = "iam/roles/"; /// Secondary index: by scope /// Format: iam/roles/by-scope/{scope}/{name} pub const BY_SCOPE: &str = "iam/roles/by-scope/"; /// Builtin roles marker /// Format: iam/roles/builtin/{name} pub const BUILTIN: &str = "iam/roles/builtin/"; } /// Store for roles pub struct RoleStore { backend: Arc, } impl JsonStore for RoleStore { fn backend(&self) -> &Backend { &self.backend } } impl RoleStore { /// Create a new role store pub fn new(backend: Arc) -> Self { Self { backend } } /// Initialize builtin roles pub async fn init_builtin_roles(&self) -> Result<()> { for role in builtin_roles::all() { // Check if already exists let key = format!("{}{}", keys::ROLES, role.name); if self.backend.get(key.as_bytes()).await?.is_none() { // Create builtin role self.create_internal(&role).await?; } } Ok(()) } /// Create a new role pub async fn create(&self, role: &Role) -> Result { if role.builtin { return Err(Error::Iam(IamError::CannotModifyBuiltinRole( role.name.clone(), ))); } self.create_internal(role).await } /// Create a role (internal use, bypasses builtin check) /// Used by init_builtin_roles and tests pub async fn create_internal(&self, role: &Role) -> Result { let key = format!("{}{}", keys::ROLES, role.name); let bytes = serde_json::to_vec(role).map_err(|e| Error::Serialization(e.to_string()))?; match self.backend.cas(key.as_bytes(), 0, &bytes).await? { CasResult::Success(version) => { // Create secondary indexes self.create_indexes(role).await?; Ok(version) } CasResult::Conflict { .. } => { Err(Error::Iam(IamError::RoleAlreadyExists(role.name.clone()))) } CasResult::NotFound => Err(Error::Internal("Unexpected CAS result".into())), } } /// Get a role by name pub async fn get(&self, name: &str) -> Result> { let key = format!("{}{}", keys::ROLES, name); match self.get_json::(key.as_bytes()).await? { Some((role, _)) => Ok(Some(role)), None => Ok(None), } } /// Get a role by reference (e.g., "roles/ProjectAdmin") pub async fn get_by_ref(&self, role_ref: &str) -> Result> { let name = role_ref.strip_prefix("roles/").unwrap_or(role_ref); self.get(name).await } /// Get a role with version pub async fn get_with_version(&self, name: &str) -> Result> { let key = format!("{}{}", keys::ROLES, name); self.get_json::(key.as_bytes()).await } /// Update a role pub async fn update(&self, role: &Role, expected_version: u64) -> Result { // Check if trying to modify builtin role let existing = self.get(&role.name).await?; if let Some(existing_role) = existing.as_ref() { if existing_role.builtin { return Err(Error::Iam(IamError::CannotModifyBuiltinRole( role.name.clone(), ))); } } let key = format!("{}{}", keys::ROLES, role.name); let bytes = serde_json::to_vec(role).map_err(|e| Error::Serialization(e.to_string()))?; match self .backend .cas(key.as_bytes(), expected_version, &bytes) .await? { CasResult::Success(version) => { // Update indexes if let Some(existing_role) = existing.as_ref() { self.delete_indexes(existing_role).await?; } self.create_indexes(role).await?; Ok(version) } CasResult::Conflict { expected, actual } => { Err(Error::Storage(iam_types::StorageError::CasConflict { expected, actual, })) } CasResult::NotFound => Err(Error::Iam(IamError::RoleNotFound(role.name.clone()))), } } /// Delete a role pub async fn delete(&self, name: &str) -> Result { // Check if builtin if let Some(role) = self.get(name).await? { if role.builtin { return Err(Error::Iam(IamError::CannotModifyBuiltinRole(name.into()))); } let key = format!("{}{}", keys::ROLES, name); let deleted = self.backend.delete(key.as_bytes()).await?; if deleted { self.delete_indexes(&role).await?; } Ok(deleted) } else { Ok(false) } } /// List all roles pub async fn list(&self) -> Result> { let pairs = self .backend .scan_prefix(keys::ROLES.as_bytes(), 10000) .await?; let mut roles = Vec::new(); for pair in pairs { // Skip index keys let key_str = String::from_utf8_lossy(&pair.key); if key_str.contains("/by-scope/") || key_str.contains("/builtin/") { continue; } let role: Role = serde_json::from_slice(&pair.value) .map_err(|e| Error::Serialization(e.to_string()))?; roles.push(role); } Ok(roles) } /// List roles by scope pub async fn list_by_scope(&self, scope: &Scope) -> Result> { let prefix = format!("{}{}/", keys::BY_SCOPE, scope.to_key()); let pairs = self.backend.scan_prefix(prefix.as_bytes(), 10000).await?; let mut roles = Vec::new(); for pair in pairs { // Index stores role name let name = String::from_utf8_lossy(&pair.value); if let Some(role) = self.get(&name).await? { roles.push(role); } } Ok(roles) } /// List builtin roles pub async fn list_builtin(&self) -> Result> { let pairs = self .backend .scan_prefix(keys::BUILTIN.as_bytes(), 100) .await?; let mut roles = Vec::new(); for pair in pairs { let name = String::from_utf8_lossy(&pair.value); if let Some(role) = self.get(&name).await? { roles.push(role); } } Ok(roles) } /// List custom (non-builtin) roles pub async fn list_custom(&self) -> Result> { let all_roles = self.list().await?; Ok(all_roles.into_iter().filter(|r| !r.builtin).collect()) } /// Check if a role exists pub async fn exists(&self, name: &str) -> Result { let key = format!("{}{}", keys::ROLES, name); Ok(self.backend.get(key.as_bytes()).await?.is_some()) } // Helper methods async fn create_indexes(&self, role: &Role) -> Result<()> { // Scope index let scope_key = format!("{}{}/{}", keys::BY_SCOPE, role.scope.to_key(), role.name); self.backend .put(scope_key.as_bytes(), role.name.as_bytes()) .await?; // Builtin marker if role.builtin { let builtin_key = format!("{}{}", keys::BUILTIN, role.name); self.backend .put(builtin_key.as_bytes(), role.name.as_bytes()) .await?; } Ok(()) } async fn delete_indexes(&self, role: &Role) -> Result<()> { // Scope index let scope_key = format!("{}{}/{}", keys::BY_SCOPE, role.scope.to_key(), role.name); self.backend.delete(scope_key.as_bytes()).await?; // Builtin marker (shouldn't delete builtin roles, but just in case) if role.builtin { let builtin_key = format!("{}{}", keys::BUILTIN, role.name); self.backend.delete(builtin_key.as_bytes()).await?; } Ok(()) } } #[cfg(test)] mod tests { use super::*; use iam_types::Permission; fn test_backend() -> Arc { Arc::new(Backend::memory()) } #[tokio::test] async fn test_role_crud() { let store = RoleStore::new(test_backend()); let role = Role::new( "CustomViewer", Scope::project("*", "*"), vec![Permission::new("*:read", "project/${project}/*")], ) .with_display_name("Custom Viewer") .with_description("Read-only access"); // Create let version = store.create(&role).await.unwrap(); assert!(version > 0); // Get let fetched = store.get("CustomViewer").await.unwrap(); assert!(fetched.is_some()); assert_eq!(fetched.unwrap().display_name, "Custom Viewer"); // Get by ref let fetched = store.get_by_ref("roles/CustomViewer").await.unwrap(); assert!(fetched.is_some()); // Delete let deleted = store.delete("CustomViewer").await.unwrap(); assert!(deleted); // Verify deleted let fetched = store.get("CustomViewer").await.unwrap(); assert!(fetched.is_none()); } #[tokio::test] async fn test_builtin_roles() { let store = RoleStore::new(test_backend()); // Initialize builtin roles store.init_builtin_roles().await.unwrap(); // Verify they exist let admin = store.get("SystemAdmin").await.unwrap(); assert!(admin.is_some()); assert!(admin.unwrap().builtin); // Try to delete builtin role let result = store.delete("SystemAdmin").await; assert!(result.is_err()); // Try to create role marked as builtin let fake_builtin = Role::builtin("FakeBuiltin", Scope::System, vec![Permission::wildcard()]); let result = store.create(&fake_builtin).await; assert!(result.is_err()); } #[tokio::test] async fn test_list_roles() { let store = RoleStore::new(test_backend()); // Init builtin store.init_builtin_roles().await.unwrap(); // Create custom role let custom = Role::new( "MyRole", Scope::project("*", "*"), vec![Permission::new("compute:*", "*")], ); store.create(&custom).await.unwrap(); // List all let all = store.list().await.unwrap(); assert!(all.len() > 1); // List custom only let custom_roles = store.list_custom().await.unwrap(); assert_eq!(custom_roles.len(), 1); assert_eq!(custom_roles[0].name, "MyRole"); // List builtin only let builtin_roles = store.list_builtin().await.unwrap(); assert!(!builtin_roles.is_empty()); assert!(builtin_roles.iter().all(|r| r.builtin)); } #[tokio::test] async fn test_list_by_scope() { let store = RoleStore::new(test_backend()); store.init_builtin_roles().await.unwrap(); // List system scope roles let system_roles = store.list_by_scope(&Scope::System).await.unwrap(); assert!(!system_roles.is_empty()); // SystemAdmin should be in system scope assert!(system_roles.iter().any(|r| r.name == "SystemAdmin")); } #[tokio::test] async fn test_duplicate_role() { let store = RoleStore::new(test_backend()); let role = Role::new("TestRole", Scope::System, vec![]); store.create(&role).await.unwrap(); // Try to create again let result = store.create(&role).await; assert!(result.is_err()); } }