photoncloud-monorepo/iam/crates/iam-store/src/role_store.rs

392 lines
12 KiB
Rust

//! 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<Backend>,
}
impl JsonStore for RoleStore {
fn backend(&self) -> &Backend {
&self.backend
}
}
impl RoleStore {
/// Create a new role store
pub fn new(backend: Arc<Backend>) -> 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<u64> {
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<u64> {
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<Option<Role>> {
let key = format!("{}{}", keys::ROLES, name);
match self.get_json::<Role>(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<Option<Role>> {
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<Option<(Role, u64)>> {
let key = format!("{}{}", keys::ROLES, name);
self.get_json::<Role>(key.as_bytes()).await
}
/// Update a role
pub async fn update(&self, role: &Role, expected_version: u64) -> Result<u64> {
// 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<bool> {
// 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<Vec<Role>> {
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<Vec<Role>> {
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<Vec<Role>> {
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<Vec<Role>> {
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<bool> {
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<Backend> {
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());
}
}