photoncloud-monorepo/iam/crates/iam-api/tests/tenant_path_integration.rs
centra 8f94aee1fa Fix R8: Convert submodule gitlinks to regular directories
- Remove gitlinks (160000 mode) for chainfire, flaredb, iam
- Add workspace contents as regular tracked files
- Update flake.nix to use simple paths instead of builtins.fetchGit

This resolves the nix build failure where submodule directories
appeared empty in the nix store.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 16:51:20 +09:00

778 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");
}