392 lines
12 KiB
Rust
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());
|
|
}
|
|
}
|