Includes all pending changes needed for nixos-anywhere: - fiberlb: L7 policy, rule, certificate types - deployer: New service for cluster management - nix-nos: Generic network modules - Various service updates and fixes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
756 lines
26 KiB
Rust
756 lines
26 KiB
Rust
//! Integration tests for Tenant Path (User → Org → Project) with RBAC enforcement
|
|
//!
|
|
//! This test suite validates the E2E flow of IAM tenant setup and authorization:
|
|
//! 1. User creation and organization assignment
|
|
//! 2. Project creation scoped to organizations
|
|
//! 3. Role-based access control (RBAC) enforcement
|
|
//! 4. Cross-tenant isolation (users can't access other tenants' resources)
|
|
//! 5. Hierarchical permission evaluation
|
|
|
|
use std::sync::Arc;
|
|
|
|
use iam_api::iam_service::{IamAdminService, IamAuthzService};
|
|
use iam_authz::{AuthzDecision, AuthzRequest, PolicyCache, PolicyEvaluator};
|
|
use iam_store::{Backend, BindingStore, PrincipalStore, RoleStore};
|
|
use iam_types::{Permission, PolicyBinding, Principal, PrincipalRef, Resource, Role, Scope};
|
|
|
|
/// Test helper: Create all required stores and services
|
|
fn setup_services() -> (
|
|
IamAdminService,
|
|
IamAuthzService,
|
|
Arc<PrincipalStore>,
|
|
Arc<RoleStore>,
|
|
Arc<BindingStore>,
|
|
Arc<PolicyEvaluator>,
|
|
) {
|
|
let backend = Arc::new(Backend::memory());
|
|
let principal_store = Arc::new(PrincipalStore::new(backend.clone()));
|
|
let role_store = Arc::new(RoleStore::new(backend.clone()));
|
|
let binding_store = Arc::new(BindingStore::new(backend));
|
|
|
|
let admin_service = IamAdminService::new(
|
|
principal_store.clone(),
|
|
role_store.clone(),
|
|
binding_store.clone(),
|
|
);
|
|
|
|
let cache = Arc::new(PolicyCache::default_config());
|
|
let evaluator = Arc::new(
|
|
PolicyEvaluator::new(binding_store.clone(), role_store.clone(), cache).with_config(
|
|
iam_authz::PolicyEvaluatorConfig {
|
|
use_cache: false,
|
|
max_bindings: 1000,
|
|
debug: false,
|
|
},
|
|
),
|
|
);
|
|
|
|
let authz_service = IamAuthzService::new(evaluator.clone(), principal_store.clone());
|
|
|
|
(
|
|
admin_service,
|
|
authz_service,
|
|
principal_store,
|
|
role_store,
|
|
binding_store,
|
|
evaluator,
|
|
)
|
|
}
|
|
|
|
/// Test Scenario 1: Complete tenant setup flow
|
|
///
|
|
/// Validates:
|
|
/// - User creation
|
|
/// - Organization scope assignment
|
|
/// - Project creation within organization
|
|
/// - Role binding at org and project levels
|
|
/// - Authorization checks for created resources
|
|
#[tokio::test]
|
|
async fn test_tenant_setup_flow() {
|
|
let (_admin_service, _authz_service, principal_store, role_store, binding_store, evaluator) =
|
|
setup_services();
|
|
|
|
// Step 1: Create User Alice
|
|
let mut alice = Principal::new_user("alice", "Alice Smith");
|
|
alice.email = Some("alice@example.com".to_string());
|
|
alice.org_id = Some("acme-corp".to_string());
|
|
principal_store.create(&alice).await.unwrap();
|
|
|
|
// Step 2: Create a custom OrgAdmin role with proper resource patterns
|
|
// Resource path format: org/{org_id}/project/{project_id}/{kind}/{id}
|
|
let org_admin_role = Role::new(
|
|
"OrgAdmin",
|
|
Scope::org("*"),
|
|
vec![Permission::new("*", "org/acme-corp/*")],
|
|
)
|
|
.with_display_name("Organization Administrator")
|
|
.with_description("Full access to organization resources");
|
|
role_store.create(&org_admin_role).await.unwrap();
|
|
|
|
// Step 3: Create OrgAdmin role binding for Alice at Org scope
|
|
let org_scope = Scope::org("acme-corp");
|
|
let alice_org_binding = PolicyBinding::new(
|
|
"binding-alice-org-admin",
|
|
PrincipalRef::user("alice"),
|
|
"roles/OrgAdmin",
|
|
org_scope.clone(),
|
|
);
|
|
binding_store.create(&alice_org_binding).await.unwrap();
|
|
|
|
// Step 4: Verify Alice can access org-level resources
|
|
let org_resource = Resource::new("organization", "acme-corp", "acme-corp", "acme-corp");
|
|
|
|
let request = AuthzRequest::new(alice.clone(), "org:manage", org_resource);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"Alice should be allowed to manage org resources as OrgAdmin"
|
|
);
|
|
|
|
// Step 5: Verify Alice can access project resources (OrgAdmin includes projects)
|
|
let project_resource = Resource::new("project", "project-alpha", "acme-corp", "project-alpha");
|
|
|
|
let request = AuthzRequest::new(alice.clone(), "project:read", project_resource);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"Alice (OrgAdmin) should be able to read projects in her org"
|
|
);
|
|
|
|
// Step 6: Create a compute instance in the project
|
|
let instance = Resource::new("instance", "vm-001", "acme-corp", "project-alpha");
|
|
|
|
let request = AuthzRequest::new(alice.clone(), "compute:instances:create", instance);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"Alice (OrgAdmin) should be able to create instances in org projects"
|
|
);
|
|
}
|
|
|
|
/// Test Scenario 2: Cross-tenant isolation
|
|
///
|
|
/// Validates:
|
|
/// - Two users in different organizations
|
|
/// - Each user has full access to their own org
|
|
/// - Users cannot access resources in other organizations
|
|
/// - Proper denial reasons are returned
|
|
#[tokio::test]
|
|
async fn test_cross_tenant_denial() {
|
|
let (_admin_service, _authz_service, principal_store, role_store, binding_store, evaluator) =
|
|
setup_services();
|
|
|
|
// Create custom org admin roles with proper patterns for each org
|
|
let org1_admin_role = Role::new(
|
|
"Org1Admin",
|
|
Scope::org("org-1"),
|
|
vec![Permission::new("*", "org/org-1/*")],
|
|
);
|
|
role_store.create(&org1_admin_role).await.unwrap();
|
|
|
|
let org2_admin_role = Role::new(
|
|
"Org2Admin",
|
|
Scope::org("org-2"),
|
|
vec![Permission::new("*", "org/org-2/*")],
|
|
);
|
|
role_store.create(&org2_admin_role).await.unwrap();
|
|
|
|
// Setup User A (Alice) with Org1
|
|
let mut alice = Principal::new_user("alice", "Alice");
|
|
alice.org_id = Some("org-1".to_string());
|
|
principal_store.create(&alice).await.unwrap();
|
|
|
|
let alice_binding = PolicyBinding::new(
|
|
"alice-org1-admin",
|
|
PrincipalRef::user("alice"),
|
|
"roles/Org1Admin",
|
|
Scope::org("org-1"),
|
|
);
|
|
binding_store.create(&alice_binding).await.unwrap();
|
|
|
|
// Setup User B (Bob) with Org2
|
|
let mut bob = Principal::new_user("bob", "Bob");
|
|
bob.org_id = Some("org-2".to_string());
|
|
principal_store.create(&bob).await.unwrap();
|
|
|
|
let bob_binding = PolicyBinding::new(
|
|
"bob-org2-admin",
|
|
PrincipalRef::user("bob"),
|
|
"roles/Org2Admin",
|
|
Scope::org("org-2"),
|
|
);
|
|
binding_store.create(&bob_binding).await.unwrap();
|
|
|
|
// Create resources in Org1 / Project1
|
|
let org1_project1_instance = Resource::new("instance", "vm-alice-1", "org-1", "project-1");
|
|
|
|
// Test 1: Alice CAN access Org1 resources
|
|
let request = AuthzRequest::new(
|
|
alice.clone(),
|
|
"compute:instances:create",
|
|
org1_project1_instance.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"Alice should access her own org resources"
|
|
);
|
|
|
|
// Test 2: Bob CANNOT access Org1 resources (cross-tenant denial)
|
|
let request = AuthzRequest::new(
|
|
bob.clone(),
|
|
"compute:instances:create",
|
|
org1_project1_instance.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_denied(),
|
|
"Bob should NOT access Alice's org resources"
|
|
);
|
|
|
|
// Verify denial reason mentions no matching policy
|
|
if let AuthzDecision::Deny { reason } = decision {
|
|
assert!(
|
|
reason.contains("No") || reason.contains("not found") || reason.contains("binding"),
|
|
"Denial reason should indicate lack of permissions: {}",
|
|
reason
|
|
);
|
|
}
|
|
|
|
// Create resources in Org2 / Project2
|
|
let org2_project2_instance = Resource::new("instance", "vm-bob-1", "org-2", "project-2");
|
|
|
|
// Test 3: Bob CAN access Org2 resources
|
|
let request = AuthzRequest::new(
|
|
bob.clone(),
|
|
"compute:instances:create",
|
|
org2_project2_instance.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"Bob should access his own org resources"
|
|
);
|
|
|
|
// Test 4: Alice CANNOT access Org2 resources (cross-tenant denial)
|
|
let request = AuthzRequest::new(alice, "compute:instances:create", org2_project2_instance);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_denied(),
|
|
"Alice should NOT access Bob's org resources"
|
|
);
|
|
}
|
|
|
|
/// Test Scenario 3: RBAC enforcement at project level
|
|
///
|
|
/// Validates:
|
|
/// - ProjectAdmin has full access within project
|
|
/// - ProjectMember has limited access (own resources + read-only)
|
|
/// - Users without roles are denied access
|
|
/// - Role inheritance and permission evaluation
|
|
#[tokio::test]
|
|
async fn test_rbac_project_scope() {
|
|
let (_admin_service, _authz_service, principal_store, role_store, binding_store, evaluator) =
|
|
setup_services();
|
|
|
|
let org_id = "acme-corp";
|
|
let project_id = "project-delta";
|
|
let project_scope = Scope::project(project_id, org_id);
|
|
|
|
// Create custom roles with proper patterns
|
|
// ProjectAdmin - full access to project resources
|
|
let project_admin_role = Role::new(
|
|
"ProjectAdmin",
|
|
Scope::project("*", "*"),
|
|
vec![Permission::new(
|
|
"*",
|
|
format!("org/{}/project/{}/*", org_id, project_id),
|
|
)],
|
|
);
|
|
role_store.create(&project_admin_role).await.unwrap();
|
|
|
|
// ProjectMember - read access + manage own resources
|
|
let project_member_role = Role::new(
|
|
"ProjectMember",
|
|
Scope::project("*", "*"),
|
|
vec![
|
|
// Full access to own resources (with owner condition)
|
|
Permission::new(
|
|
"compute:instances:*",
|
|
format!("org/{}/project/{}/instance/*", org_id, project_id),
|
|
)
|
|
.with_condition(iam_types::Condition::string_equals(
|
|
"resource.owner",
|
|
"${principal.id}",
|
|
)),
|
|
// Read access to all project resources
|
|
Permission::new(
|
|
"*:*:read",
|
|
format!("org/{}/project/{}/*", org_id, project_id),
|
|
),
|
|
Permission::new(
|
|
"*:*:list",
|
|
format!("org/{}/project/{}/*", org_id, project_id),
|
|
),
|
|
],
|
|
);
|
|
role_store.create(&project_member_role).await.unwrap();
|
|
|
|
// Create three users in the same org/project
|
|
let mut admin_user = Principal::new_user("admin-user", "Project Admin");
|
|
admin_user.org_id = Some(org_id.to_string());
|
|
admin_user.project_id = Some(project_id.to_string());
|
|
principal_store.create(&admin_user).await.unwrap();
|
|
|
|
let mut member_user = Principal::new_user("member-user", "Project Member");
|
|
member_user.org_id = Some(org_id.to_string());
|
|
member_user.project_id = Some(project_id.to_string());
|
|
principal_store.create(&member_user).await.unwrap();
|
|
|
|
let mut guest_user = Principal::new_user("guest-user", "Guest User");
|
|
guest_user.org_id = Some(org_id.to_string());
|
|
principal_store.create(&guest_user).await.unwrap();
|
|
|
|
// Assign ProjectAdmin role to admin_user
|
|
let admin_binding = PolicyBinding::new(
|
|
"admin-project-admin",
|
|
PrincipalRef::user("admin-user"),
|
|
"roles/ProjectAdmin",
|
|
project_scope.clone(),
|
|
);
|
|
binding_store.create(&admin_binding).await.unwrap();
|
|
|
|
// Assign ProjectMember role to member_user
|
|
let member_binding = PolicyBinding::new(
|
|
"member-project-member",
|
|
PrincipalRef::user("member-user"),
|
|
"roles/ProjectMember",
|
|
project_scope.clone(),
|
|
);
|
|
binding_store.create(&member_binding).await.unwrap();
|
|
|
|
// Note: guest_user has no role binding (should be denied)
|
|
|
|
// Create test resources
|
|
let admin_instance =
|
|
Resource::new("instance", "vm-admin-1", org_id, project_id).with_owner("admin-user");
|
|
let member_instance =
|
|
Resource::new("instance", "vm-member-1", org_id, project_id).with_owner("member-user");
|
|
let shared_volume = Resource::new("volume", "vol-shared", org_id, project_id);
|
|
|
|
// Test 1: ProjectAdmin can create instances
|
|
let request = AuthzRequest::new(
|
|
admin_user.clone(),
|
|
"compute:instances:create",
|
|
admin_instance.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"ProjectAdmin should create instances"
|
|
);
|
|
|
|
// Test 2: ProjectAdmin can delete any instance in project
|
|
let request = AuthzRequest::new(
|
|
admin_user.clone(),
|
|
"compute:instances:delete",
|
|
member_instance.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"ProjectAdmin should delete any project instance"
|
|
);
|
|
|
|
// Test 3: ProjectMember can read instances (builtin permission)
|
|
let request = AuthzRequest::new(
|
|
member_user.clone(),
|
|
"compute:instances:read",
|
|
admin_instance.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"ProjectMember should read project instances"
|
|
);
|
|
|
|
// Test 4: ProjectMember can list instances (builtin permission)
|
|
let request = AuthzRequest::new(
|
|
member_user.clone(),
|
|
"compute:instances:list",
|
|
shared_volume.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"ProjectMember should list project resources"
|
|
);
|
|
|
|
// Test 5: ProjectMember can manage their own instances (owner condition)
|
|
let request = AuthzRequest::new(
|
|
member_user.clone(),
|
|
"compute:instances:create",
|
|
member_instance.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"ProjectMember should create own instances"
|
|
);
|
|
|
|
// Test 6: ProjectMember CANNOT delete others' instances (owner condition fails)
|
|
let request = AuthzRequest::new(
|
|
member_user.clone(),
|
|
"compute:instances:delete",
|
|
admin_instance.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_denied(),
|
|
"ProjectMember should NOT delete others' instances"
|
|
);
|
|
|
|
// Test 7: Guest user (no role) is denied all access
|
|
let request = AuthzRequest::new(
|
|
guest_user.clone(),
|
|
"compute:instances:read",
|
|
shared_volume.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(decision.is_denied(), "Guest should be denied without roles");
|
|
|
|
let request = AuthzRequest::new(
|
|
guest_user.clone(),
|
|
"compute:instances:create",
|
|
Resource::new("instance", "vm-guest", org_id, project_id).with_owner("guest-user"),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_denied(),
|
|
"Guest should NOT create instances without role"
|
|
);
|
|
}
|
|
|
|
/// Test Scenario 4: Hierarchical scope inheritance
|
|
///
|
|
/// Validates:
|
|
/// - System scope permissions apply to all orgs/projects
|
|
/// - Org scope permissions apply to all projects within org
|
|
/// - Project scope permissions are isolated to that project
|
|
#[tokio::test]
|
|
async fn test_hierarchical_scope_inheritance() {
|
|
let (_admin_service, _authz_service, principal_store, role_store, binding_store, evaluator) =
|
|
setup_services();
|
|
|
|
// Create custom roles
|
|
// SystemAdmin - full access to everything
|
|
let sys_admin_role = Role::new("SystemAdmin", Scope::System, vec![Permission::wildcard()]);
|
|
role_store.create(&sys_admin_role).await.unwrap();
|
|
|
|
// Org1Admin - full access to org-1 resources
|
|
let org1_admin_role = Role::new(
|
|
"Org1Admin",
|
|
Scope::org("org-1"),
|
|
vec![Permission::new("*", "org/org-1/*")],
|
|
);
|
|
role_store.create(&org1_admin_role).await.unwrap();
|
|
|
|
// Create a system admin
|
|
let sys_admin = Principal::new_user("sysadmin", "System Administrator");
|
|
principal_store.create(&sys_admin).await.unwrap();
|
|
|
|
let sys_admin_binding = PolicyBinding::new(
|
|
"sysadmin-system",
|
|
PrincipalRef::user("sysadmin"),
|
|
"roles/SystemAdmin",
|
|
Scope::System,
|
|
);
|
|
binding_store.create(&sys_admin_binding).await.unwrap();
|
|
|
|
// Create org admin for org-1 only
|
|
let org_admin = Principal::new_user("orgadmin", "Org Admin");
|
|
principal_store.create(&org_admin).await.unwrap();
|
|
|
|
let org_admin_binding = PolicyBinding::new(
|
|
"orgadmin-org1",
|
|
PrincipalRef::user("orgadmin"),
|
|
"roles/Org1Admin",
|
|
Scope::org("org-1"),
|
|
);
|
|
binding_store.create(&org_admin_binding).await.unwrap();
|
|
|
|
// Test resources in different orgs/projects
|
|
let org1_proj1_resource = Resource::new("instance", "vm-1", "org-1", "proj-1");
|
|
let org1_proj2_resource = Resource::new("instance", "vm-2", "org-1", "proj-2");
|
|
let org2_proj1_resource = Resource::new("instance", "vm-3", "org-2", "proj-1");
|
|
|
|
// Test 1: SystemAdmin can access resources in ANY org/project
|
|
for resource in [
|
|
&org1_proj1_resource,
|
|
&org1_proj2_resource,
|
|
&org2_proj1_resource,
|
|
] {
|
|
let request = AuthzRequest::new(
|
|
sys_admin.clone(),
|
|
"compute:instances:delete",
|
|
resource.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"SystemAdmin should access all resources"
|
|
);
|
|
}
|
|
|
|
// Test 2: OrgAdmin can access resources in org-1 projects
|
|
for resource in [&org1_proj1_resource, &org1_proj2_resource] {
|
|
let request = AuthzRequest::new(
|
|
org_admin.clone(),
|
|
"compute:instances:delete",
|
|
resource.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"OrgAdmin should access all projects in their org"
|
|
);
|
|
}
|
|
|
|
// Test 3: OrgAdmin CANNOT access resources in org-2
|
|
let request = AuthzRequest::new(
|
|
org_admin.clone(),
|
|
"compute:instances:delete",
|
|
org2_proj1_resource,
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_denied(),
|
|
"OrgAdmin should NOT access other orgs"
|
|
);
|
|
}
|
|
|
|
/// Test Scenario 5: Custom role with fine-grained permissions
|
|
///
|
|
/// Validates:
|
|
/// - Creation of custom roles with specific permissions
|
|
/// - Permission pattern matching (action and resource patterns)
|
|
/// - Custom role assignment and evaluation
|
|
#[tokio::test]
|
|
async fn test_custom_role_fine_grained_permissions() {
|
|
let (_admin_service, _authz_service, principal_store, role_store, binding_store, evaluator) =
|
|
setup_services();
|
|
|
|
let org_id = "tech-corp";
|
|
let project_id = "backend-services";
|
|
|
|
// Create a custom role: "StorageOperator" - can manage volumes but not instances
|
|
let storage_operator_role = Role::new(
|
|
"StorageOperator",
|
|
Scope::project("*", "*"),
|
|
vec![
|
|
Permission::new(
|
|
"storage:volumes:*",
|
|
format!("org/{}/project/{}/*", org_id, project_id),
|
|
),
|
|
Permission::new(
|
|
"storage:snapshots:*",
|
|
format!("org/{}/project/{}/*", org_id, project_id),
|
|
),
|
|
Permission::new(
|
|
"storage:*:read",
|
|
format!("org/{}/project/{}/*", org_id, project_id),
|
|
),
|
|
Permission::new(
|
|
"storage:*:list",
|
|
format!("org/{}/project/{}/*", org_id, project_id),
|
|
),
|
|
],
|
|
)
|
|
.with_display_name("Storage Operator")
|
|
.with_description("Can manage storage resources but not compute");
|
|
|
|
role_store.create(&storage_operator_role).await.unwrap();
|
|
|
|
// Create a user and assign the custom role
|
|
let storage_user = Principal::new_user("storage-ops", "Storage Operator User");
|
|
principal_store.create(&storage_user).await.unwrap();
|
|
|
|
let storage_binding = PolicyBinding::new(
|
|
"storage-ops-binding",
|
|
PrincipalRef::user("storage-ops"),
|
|
"roles/StorageOperator",
|
|
Scope::project(project_id, org_id),
|
|
);
|
|
binding_store.create(&storage_binding).await.unwrap();
|
|
|
|
// Create test resources
|
|
let volume = Resource::new("volume", "vol-001", org_id, project_id);
|
|
let snapshot = Resource::new("snapshot", "snap-001", org_id, project_id);
|
|
let instance = Resource::new("instance", "vm-001", org_id, project_id);
|
|
|
|
// Test 1: Storage operator CAN manage volumes
|
|
let request = AuthzRequest::new(
|
|
storage_user.clone(),
|
|
"storage:volumes:create",
|
|
volume.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"StorageOperator should create volumes"
|
|
);
|
|
|
|
let request = AuthzRequest::new(storage_user.clone(), "storage:volumes:delete", volume);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"StorageOperator should delete volumes"
|
|
);
|
|
|
|
// Test 2: Storage operator CAN manage snapshots
|
|
let request = AuthzRequest::new(
|
|
storage_user.clone(),
|
|
"storage:snapshots:create",
|
|
snapshot.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"StorageOperator should create snapshots"
|
|
);
|
|
|
|
// Test 3: Storage operator CAN read instances (read permission granted)
|
|
let request = AuthzRequest::new(
|
|
storage_user.clone(),
|
|
"storage:instances:read",
|
|
instance.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_allowed(),
|
|
"StorageOperator should read instances"
|
|
);
|
|
|
|
// Test 4: Storage operator CANNOT create/delete instances (no permission)
|
|
let request = AuthzRequest::new(
|
|
storage_user.clone(),
|
|
"compute:instances:create",
|
|
instance.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_denied(),
|
|
"StorageOperator should NOT create instances"
|
|
);
|
|
|
|
let request = AuthzRequest::new(storage_user.clone(), "compute:instances:delete", instance);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(
|
|
decision.is_denied(),
|
|
"StorageOperator should NOT delete instances"
|
|
);
|
|
}
|
|
|
|
/// Test Scenario 6: Multiple role bindings (role aggregation)
|
|
///
|
|
/// Validates:
|
|
/// - A principal can have multiple role bindings
|
|
/// - Permissions from all roles are aggregated
|
|
/// - Most permissive role wins
|
|
#[tokio::test]
|
|
async fn test_multiple_role_bindings() {
|
|
let (_admin_service, _authz_service, principal_store, role_store, binding_store, evaluator) =
|
|
setup_services();
|
|
|
|
let org_id = "multi-role-org";
|
|
let project1 = "project-1";
|
|
let project2 = "project-2";
|
|
|
|
// Create custom roles
|
|
// ReadOnly for project-1 - read/list/get operations only
|
|
let readonly_role = Role::new(
|
|
"ReadOnly",
|
|
Scope::project("*", "*"),
|
|
vec![
|
|
Permission::new("*:*:read", format!("org/{}/project/{}/*", org_id, project1)),
|
|
Permission::new("*:*:list", format!("org/{}/project/{}/*", org_id, project1)),
|
|
Permission::new("*:*:get", format!("org/{}/project/{}/*", org_id, project1)),
|
|
],
|
|
);
|
|
role_store.create(&readonly_role).await.unwrap();
|
|
|
|
// ProjectAdmin for project-2
|
|
let project_admin_role = Role::new(
|
|
"ProjectAdmin",
|
|
Scope::project("*", "*"),
|
|
vec![Permission::new(
|
|
"*",
|
|
format!("org/{}/project/{}/*", org_id, project2),
|
|
)],
|
|
);
|
|
role_store.create(&project_admin_role).await.unwrap();
|
|
|
|
// Create a user
|
|
let user = Principal::new_user("multi-role-user", "Multi Role User");
|
|
principal_store.create(&user).await.unwrap();
|
|
|
|
// Assign ReadOnly role in project-1
|
|
let readonly_binding = PolicyBinding::new(
|
|
"readonly-proj1",
|
|
PrincipalRef::user("multi-role-user"),
|
|
"roles/ReadOnly",
|
|
Scope::project(project1, org_id),
|
|
);
|
|
binding_store.create(&readonly_binding).await.unwrap();
|
|
|
|
// Assign ProjectAdmin role in project-2
|
|
let admin_binding = PolicyBinding::new(
|
|
"admin-proj2",
|
|
PrincipalRef::user("multi-role-user"),
|
|
"roles/ProjectAdmin",
|
|
Scope::project(project2, org_id),
|
|
);
|
|
binding_store.create(&admin_binding).await.unwrap();
|
|
|
|
// Resources in different projects
|
|
let proj1_instance = Resource::new("instance", "vm-1", org_id, project1);
|
|
let proj2_instance = Resource::new("instance", "vm-2", org_id, project2);
|
|
|
|
// Test 1: User can only READ in project-1 (ReadOnly role)
|
|
let request = AuthzRequest::new(
|
|
user.clone(),
|
|
"compute:instances:read",
|
|
proj1_instance.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(decision.is_allowed(), "Should read in project-1");
|
|
|
|
let request = AuthzRequest::new(user.clone(), "compute:instances:delete", proj1_instance);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(decision.is_denied(), "Should NOT delete in project-1");
|
|
|
|
// Test 2: User can do ANYTHING in project-2 (ProjectAdmin role)
|
|
let request = AuthzRequest::new(
|
|
user.clone(),
|
|
"compute:instances:read",
|
|
proj2_instance.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(decision.is_allowed(), "Should read in project-2");
|
|
|
|
let request = AuthzRequest::new(
|
|
user.clone(),
|
|
"compute:instances:delete",
|
|
proj2_instance.clone(),
|
|
);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(decision.is_allowed(), "Should delete in project-2");
|
|
|
|
let request = AuthzRequest::new(user.clone(), "compute:instances:create", proj2_instance);
|
|
let decision = evaluator.evaluate(&request).await.unwrap();
|
|
assert!(decision.is_allowed(), "Should create in project-2");
|
|
}
|