diff --git a/tests/api/src/auth/project.rs b/tests/api/src/auth/project.rs index 006ba7459..d24f7422c 100644 --- a/tests/api/src/auth/project.rs +++ b/tests/api/src/auth/project.rs @@ -57,3 +57,85 @@ async fn test_list_user_projects() -> Result<()> { assert!(!projects.is_empty()); Ok(()) } + + +#[tokio::test] +async fn test_auth_projects_returns_empty_for_user_without_roles() -> Result<()> { + // Regression test for issue #515: + // When a user has no role assignments, /auth/projects should return + // an empty list, not all projects in the system. + + let mut admin_client = TestClient::default()?; + admin_client.auth_admin().await?; + + // Create a domain for the test user + let domain_response = admin_client + .client + .post(admin_client.base_url.join("v3/domains")?) + .json(&serde_json::json!({ + "domain": { + "name": format!("test-domain-{}", uuid::Uuid::new_v4()), + "enabled": true + } + })) + .send() + .await?; + + let domain: serde_json::Value = domain_response.json().await?; + let domain_id = domain["domain"]["id"].as_str().unwrap(); + + // Create a user with no role assignments + let user_password = "TestPassword123!"; + let user_response = admin_client + .client + .post(admin_client.base_url.join("v3/users")?) + .json(&serde_json::json!({ + "user": { + "name": format!("test-user-{}", uuid::Uuid::new_v4()), + "domain_id": domain_id, + "password": user_password, + "enabled": true + } + })) + .send() + .await?; + + let user: serde_json::Value = user_response.json().await?; + let user_id = user["user"]["id"].as_str().unwrap(); + + // Authenticate as the user with no roles + let mut user_client = TestClient::default()?; + user_client + .auth_password( + PasswordAuthBuilder::default() + .user( + UserBuilder::default() + .id(user_id) + .domain( + DomainBuilder::default() + .id(domain_id) + .build()? + ) + .build()? + ) + .password(user_password) + .build()?, + None, // No scope - unscoped auth + ) + .await?; + + // Call /auth/projects - should return empty list + let projects = list_auth_projects(&Arc::new( + AsyncOpenStack::new(&CloudConfig::from_env()?).await? + )) + .await?; + + // The user has no role assignments, so projects list should be empty + assert!( + projects.is_empty(), + "User with no role assignments should see no projects. \ + This may indicate the bug from issue #515 is present (returning all projects instead of empty list)" + ); + + Ok(()) +} \ No newline at end of file diff --git a/tests/integration/src/assignment/grant/list.rs b/tests/integration/src/assignment/grant/list.rs index 35fa6bccb..f94c0e015 100644 --- a/tests/integration/src/assignment/grant/list.rs +++ b/tests/integration/src/assignment/grant/list.rs @@ -200,3 +200,140 @@ async fn test_list_user_roles() -> Result<()> { ); Ok(()) } + +// Add these two tests at the BOTTOM of: +// tests/integration/src/assignment/grant/list.rs +// (paste before the last closing brace if any, or just at the end of the file) + +#[traced_test] +#[tokio::test] +async fn test_list_role_assignments_by_user_same_role_multiple_scopes() -> Result<()> { + // Regression test for issue #513: + // Assigning the same role to a user on multiple projects + system was + // mixing up assignments due to HashMap by role_id. This test verifies + // all assignments are returned correctly when listing by user_id only. + let (state, _) = get_state().await?; + + let domain = create_domain!(state)?; + let project_1 = create_project!(state, domain.id.clone())?; + let project_2 = create_project!(state, domain.id.clone())?; + let user = create_user!(state, domain.id.clone())?; + let role = create_role!(state)?; + + // Assign the SAME role to the user on two different projects AND system + for assignment in [ + AssignmentCreate::user_project(&user.id, &project_1.id, &role.id, false), + AssignmentCreate::user_project(&user.id, &project_2.id, &role.id, false), + AssignmentCreate::user_system(&user.id, "all", &role.id, false), + ] { + state + .provider + .get_assignment_provider() + .create_grant(&state, assignment) + .await?; + } + + // List assignments filtering by user_id ONLY (no target filter) + let assignments = state + .provider + .get_assignment_provider() + .list_role_assignments( + &state, + &RoleAssignmentListParametersBuilder::default() + .user_id(user.id.clone()) + .build()?, + ) + .await?; + + // Should return exactly 3 assignments - one per scope + // (Before the fix, HashMap deduplicated by role_id and returned only 1) + assert_eq!( + assignments.len(), + 3, + "Expected 3 role assignments (same role on project_1 + project_2 + system), got {}. \ + This may indicate the HashMap bug from issue #513 is present.", + assignments.len() + ); + + // Verify both project targets are present + let target_ids: BTreeSet = assignments.iter().map(|a| a.target_id.clone()).collect(); + assert!( + target_ids.contains(&project_1.id), + "Assignment on project_1 should be present" + ); + assert!( + target_ids.contains(&project_2.id), + "Assignment on project_2 should be present" + ); + + Ok(()) +} + +#[traced_test] +#[tokio::test] +async fn test_list_role_assignments_by_role_id_same_role_multiple_scopes() -> Result<()> { + // Regression test for issue #513: + // Listing by role_id should return all assignments for that role + // across different targets (project_1, project_2, system). + let (state, _) = get_state().await?; + + let domain = create_domain!(state)?; + let project_1 = create_project!(state, domain.id.clone())?; + let project_2 = create_project!(state, domain.id.clone())?; + let user = create_user!(state, domain.id.clone())?; + let role = create_role!(state)?; + + // Assign the SAME role on two projects and system + for assignment in [ + AssignmentCreate::user_project(&user.id, &project_1.id, &role.id, false), + AssignmentCreate::user_project(&user.id, &project_2.id, &role.id, false), + AssignmentCreate::user_system(&user.id, "all", &role.id, false), + ] { + state + .provider + .get_assignment_provider() + .create_grant(&state, assignment) + .await?; + } + + // List assignments filtering by role_id ONLY + let assignments = state + .provider + .get_assignment_provider() + .list_role_assignments( + &state, + &RoleAssignmentListParametersBuilder::default() + .role_id(role.id.clone()) + .build()?, + ) + .await?; + + // Filter to only our test user's assignments + let user_assignments: Vec<_> = assignments + .iter() + .filter(|a| a.actor_id == user.id) + .collect(); + + // Should have all 3 assignments for this user+role combination + assert_eq!( + user_assignments.len(), + 3, + "Expected 3 assignments when filtering by role_id, got {}. \ + This may indicate the HashMap bug from issue #513 is present.", + user_assignments.len() + ); + + // Verify the correct targets are present + let target_ids: BTreeSet = + user_assignments.iter().map(|a| a.target_id.clone()).collect(); + assert!( + target_ids.contains(&project_1.id), + "Assignment on project_1 should be present when filtering by role_id" + ); + assert!( + target_ids.contains(&project_2.id), + "Assignment on project_2 should be present when filtering by role_id" + ); + + Ok(()) +} \ No newline at end of file