//! 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, Arc, Arc, Arc, ) { 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"); }