use super::manager::RebacManager;
use crate::error::{Error, Result};
use crate::multitenancy::Permission;
use std::sync::{Arc, RwLock};
fn validate_component(value: &str, field: &str) -> Result<()> {
if value.is_empty() {
return Err(Error::InvalidInput(format!("{} must not be empty", field)));
}
if value.contains('/') || value.contains(':') || value.contains('#') {
return Err(Error::InvalidInput(format!(
"Invalid {} '{}': must not contain '/', ':' or '#'",
field, value
)));
}
Ok(())
}
fn user_subject(tenant_id: &str, user_id: &str) -> Result<String> {
validate_component(tenant_id, "tenant id")?;
validate_component(user_id, "user id")?;
Ok(format!("{}/user:{}", tenant_id, user_id))
}
fn tenant_object(tenant_id: &str) -> Result<String> {
validate_component(tenant_id, "tenant id")?;
Ok(format!("{}/tenant:{}", tenant_id, tenant_id))
}
fn resource_object(tenant_id: &str, resource_type: &str, resource_id: &str) -> Result<String> {
validate_component(tenant_id, "tenant id")?;
validate_component(resource_type, "resource type")?;
validate_component(resource_id, "resource id")?;
Ok(format!("{}/{}:{}", tenant_id, resource_type, resource_id))
}
#[derive(Debug)]
pub struct RbacCompatLayer {
rebac: Arc<RwLock<RebacManager>>,
}
impl RbacCompatLayer {
pub fn new(rebac: Arc<RwLock<RebacManager>>) -> Self {
RbacCompatLayer { rebac }
}
pub fn assign_role(&self, user_id: &str, role: &str, tenant_id: &str) -> Result<()> {
let rebac = self
.rebac
.write()
.map_err(|_| Error::InvalidOperation("Failed to acquire write lock".to_string()))?;
let user_subject = user_subject(tenant_id, user_id)?;
let tenant_object = tenant_object(tenant_id)?;
match role.to_lowercase().as_str() {
"admin" => {
rebac.grant(&user_subject, "admin", &tenant_object)?;
rebac.grant(&user_subject, "owner", &tenant_object)?;
rebac.grant(&user_subject, "manager", &tenant_object)?;
rebac.grant(&user_subject, "editor", &tenant_object)?;
rebac.grant(&user_subject, "viewer", &tenant_object)?;
}
"manager" => {
rebac.grant(&user_subject, "manager", &tenant_object)?;
rebac.grant(&user_subject, "editor", &tenant_object)?;
rebac.grant(&user_subject, "viewer", &tenant_object)?;
}
"analyst" | "editor" => {
rebac.grant(&user_subject, "editor", &tenant_object)?;
rebac.grant(&user_subject, "viewer", &tenant_object)?;
}
"viewer" => {
rebac.grant(&user_subject, "viewer", &tenant_object)?;
}
_ => {
return Err(Error::InvalidInput(format!("Unknown role: {}", role)));
}
}
Ok(())
}
pub fn revoke_role(&self, user_id: &str, role: &str, tenant_id: &str) -> Result<()> {
let rebac = self
.rebac
.write()
.map_err(|_| Error::InvalidOperation("Failed to acquire write lock".to_string()))?;
let user_subject = user_subject(tenant_id, user_id)?;
let tenant_object = tenant_object(tenant_id)?;
match role.to_lowercase().as_str() {
"admin" => {
rebac.revoke(&user_subject, "admin", &tenant_object)?;
rebac.revoke(&user_subject, "owner", &tenant_object)?;
rebac.revoke(&user_subject, "manager", &tenant_object)?;
rebac.revoke(&user_subject, "editor", &tenant_object)?;
rebac.revoke(&user_subject, "viewer", &tenant_object)?;
}
"manager" => {
rebac.revoke(&user_subject, "manager", &tenant_object)?;
rebac.revoke(&user_subject, "editor", &tenant_object)?;
rebac.revoke(&user_subject, "viewer", &tenant_object)?;
}
"analyst" | "editor" => {
rebac.revoke(&user_subject, "editor", &tenant_object)?;
rebac.revoke(&user_subject, "viewer", &tenant_object)?;
}
"viewer" => {
rebac.revoke(&user_subject, "viewer", &tenant_object)?;
}
_ => {
return Err(Error::InvalidInput(format!("Unknown role: {}", role)));
}
}
Ok(())
}
pub fn check_permission(
&self,
user_id: &str,
permission: &Permission,
resource_type: &str,
resource_id: &str,
tenant_id: &str,
) -> Result<bool> {
let rebac = self
.rebac
.read()
.map_err(|_| Error::InvalidOperation("Failed to acquire read lock".to_string()))?;
let user_subject = user_subject(tenant_id, user_id)?;
let resource_object = resource_object(tenant_id, resource_type, resource_id)?;
let relation = self.map_permission_to_relation(permission);
let has_direct = rebac.check_access(&user_subject, relation, &resource_object)?;
if has_direct {
return Ok(true);
}
let tenant_object = tenant_object(tenant_id)?;
let has_tenant = rebac.check_access(&user_subject, relation, &tenant_object)?;
Ok(has_tenant)
}
pub fn check_any_permission(
&self,
user_id: &str,
permissions: &[Permission],
resource_type: &str,
resource_id: &str,
tenant_id: &str,
) -> Result<bool> {
for permission in permissions {
if self.check_permission(user_id, permission, resource_type, resource_id, tenant_id)? {
return Ok(true);
}
}
Ok(false)
}
pub fn check_all_permissions(
&self,
user_id: &str,
permissions: &[Permission],
resource_type: &str,
resource_id: &str,
tenant_id: &str,
) -> Result<bool> {
if permissions.is_empty() {
return Ok(false);
}
for permission in permissions {
if !self.check_permission(user_id, permission, resource_type, resource_id, tenant_id)? {
return Ok(false);
}
}
Ok(true)
}
pub fn list_accessible_resources(
&self,
user_id: &str,
permission: &Permission,
resource_type: &str,
tenant_id: &str,
) -> Result<Vec<String>> {
let rebac = self
.rebac
.read()
.map_err(|_| Error::InvalidOperation("Failed to acquire read lock".to_string()))?;
let user_subject = user_subject(tenant_id, user_id)?;
let relation = self.map_permission_to_relation(permission);
let namespaced_type = format!("{}/{}", tenant_id, resource_type);
let prefix = format!("{}:", namespaced_type);
let objects = rebac.list_accessible(&user_subject, relation, &namespaced_type)?;
Ok(objects
.into_iter()
.map(|o| o.strip_prefix(&prefix).map(str::to_string).unwrap_or(o))
.collect())
}
pub fn get_users_with_role(&self, role: &str, tenant_id: &str) -> Result<Vec<String>> {
let rebac = self
.rebac
.read()
.map_err(|_| Error::InvalidOperation("Failed to acquire read lock".to_string()))?;
let tenant_object = tenant_object(tenant_id)?;
let relation = match role.to_lowercase().as_str() {
"admin" => "admin",
"manager" => "manager",
"analyst" | "editor" => "editor",
"viewer" => "viewer",
_ => return Err(Error::InvalidInput(format!("Unknown role: {}", role))),
};
let subjects = rebac.expand(relation, &tenant_object)?;
let prefix = format!("{}/user:", tenant_id);
let user_ids: Vec<String> = subjects
.into_iter()
.filter_map(|s| s.strip_prefix(&prefix).map(str::to_string))
.collect();
Ok(user_ids)
}
fn map_permission_to_relation(&self, permission: &Permission) -> &str {
match permission {
Permission::Read => "viewer",
Permission::Write => "editor",
Permission::Delete => "owner",
Permission::Create => "editor",
Permission::Share => "owner",
Permission::Admin => "admin",
}
}
pub fn grant_resource_permission(
&self,
user_id: &str,
permission: &Permission,
resource_type: &str,
resource_id: &str,
tenant_id: &str,
) -> Result<()> {
let rebac = self
.rebac
.write()
.map_err(|_| Error::InvalidOperation("Failed to acquire write lock".to_string()))?;
let user_subject = user_subject(tenant_id, user_id)?;
let resource_object = resource_object(tenant_id, resource_type, resource_id)?;
let relation = self.map_permission_to_relation(permission);
rebac.grant(&user_subject, relation, &resource_object)
}
pub fn revoke_resource_permission(
&self,
user_id: &str,
permission: &Permission,
resource_type: &str,
resource_id: &str,
tenant_id: &str,
) -> Result<()> {
let rebac = self
.rebac
.write()
.map_err(|_| Error::InvalidOperation("Failed to acquire write lock".to_string()))?;
let user_subject = user_subject(tenant_id, user_id)?;
let resource_object = resource_object(tenant_id, resource_type, resource_id)?;
let relation = self.map_permission_to_relation(permission);
rebac.revoke(&user_subject, relation, &resource_object)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_compat() -> RbacCompatLayer {
let rebac = Arc::new(RwLock::new(RebacManager::new()));
RbacCompatLayer::new(rebac)
}
#[test]
fn test_assign_role() {
let compat = create_test_compat();
compat
.assign_role("alice", "admin", "tenant_a")
.expect("assign should succeed");
let has_admin = compat
.check_permission(
"alice",
&Permission::Admin,
"tenant",
"tenant_a",
"tenant_a",
)
.expect("check should succeed");
assert!(has_admin);
}
#[test]
fn test_revoke_role() {
let compat = create_test_compat();
compat
.assign_role("alice", "admin", "tenant_a")
.expect("assign should succeed");
compat
.revoke_role("alice", "admin", "tenant_a")
.expect("revoke should succeed");
let has_admin = compat
.check_permission(
"alice",
&Permission::Admin,
"tenant",
"tenant_a",
"tenant_a",
)
.expect("check should succeed");
assert!(!has_admin);
}
#[test]
fn test_check_permission() {
let compat = create_test_compat();
compat
.assign_role("alice", "viewer", "tenant_a")
.expect("assign should succeed");
let can_read = compat
.check_permission("alice", &Permission::Read, "document", "123", "tenant_a")
.expect("check should succeed");
assert!(can_read);
let can_write = compat
.check_permission("alice", &Permission::Write, "document", "123", "tenant_a")
.expect("check should succeed");
assert!(!can_write);
}
#[test]
fn test_resource_level_permission() {
let compat = create_test_compat();
compat
.grant_resource_permission("alice", &Permission::Write, "document", "123", "tenant_a")
.expect("grant should succeed");
let rebac = compat.rebac.read().expect("lock should succeed");
let has_access = rebac
.check_access("tenant_a/user:alice", "editor", "tenant_a/document:123")
.expect("check should succeed");
assert!(has_access);
let cross_tenant = rebac
.check_access("tenant_b/user:alice", "editor", "tenant_b/document:123")
.expect("check should succeed");
assert!(!cross_tenant, "cross-tenant access must be denied");
}
#[test]
fn test_check_any_permission() {
let compat = create_test_compat();
compat
.assign_role("alice", "editor", "tenant_a")
.expect("assign should succeed");
let has_any = compat
.check_any_permission(
"alice",
&[Permission::Write, Permission::Delete],
"document",
"123",
"tenant_a",
)
.expect("check should succeed");
assert!(has_any);
}
#[test]
fn test_check_all_permissions() {
let compat = create_test_compat();
compat
.assign_role("alice", "admin", "tenant_a")
.expect("assign should succeed");
let has_all = compat
.check_all_permissions(
"alice",
&[Permission::Read, Permission::Write],
"document",
"123",
"tenant_a",
)
.expect("check should succeed");
assert!(has_all);
}
#[test]
fn test_get_users_with_role() {
let compat = create_test_compat();
compat
.assign_role("alice", "admin", "tenant_a")
.expect("assign should succeed");
compat
.assign_role("bob", "admin", "tenant_a")
.expect("assign should succeed");
let admins = compat
.get_users_with_role("admin", "tenant_a")
.expect("get should succeed");
assert_eq!(admins.len(), 2);
assert!(admins.contains(&"alice".to_string()));
assert!(admins.contains(&"bob".to_string()));
}
#[test]
fn test_role_hierarchy() {
let compat = create_test_compat();
compat
.assign_role("alice", "manager", "tenant_a")
.expect("assign should succeed");
let can_write = compat
.check_permission("alice", &Permission::Write, "document", "123", "tenant_a")
.expect("check should succeed");
assert!(can_write);
let is_admin = compat
.check_permission("alice", &Permission::Admin, "document", "123", "tenant_a")
.expect("check should succeed");
assert!(!is_admin);
}
}