diff --git a/crates/core/src/assignment/mock.rs b/crates/core/src/assignment/mock.rs index 05313ec4..ecd13edf 100644 --- a/crates/core/src/assignment/mock.rs +++ b/crates/core/src/assignment/mock.rs @@ -18,6 +18,7 @@ use crate::assignment::AssignmentApi; use crate::assignment::AssignmentProviderError; use crate::assignment::types::*; use crate::keystone::ServiceState; +use crate::role::types::*; mock! { pub AssignmentProvider {} @@ -41,5 +42,13 @@ mock! { state: &ServiceState, params: Assignment, ) -> Result<(), AssignmentProviderError>; + + // List user roles on project + async fn list_user_roles_on_project( + &self, + state: &ServiceState, + params: &RoleAssignmentListParameters, + ) -> Result, AssignmentProviderError>; + } } diff --git a/crates/core/src/assignment/mod.rs b/crates/core/src/assignment/mod.rs index 7e5411f8..421c1206 100644 --- a/crates/core/src/assignment/mod.rs +++ b/crates/core/src/assignment/mod.rs @@ -49,6 +49,7 @@ use openstack_keystone_config::Config; use crate::assignment::service::AssignmentService; use crate::keystone::ServiceState; use crate::plugin_manager::PluginManagerApi; +use crate::role::types::Role; use types::*; pub use error::AssignmentProviderError; @@ -104,6 +105,19 @@ impl AssignmentApi for AssignmentProvider { } } + // List user roles on project + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_user_roles_on_project( + &self, + state: &ServiceState, + params: &RoleAssignmentListParameters, + ) -> Result, AssignmentProviderError> { + match self { + Self::Service(provider) => provider.list_user_roles_on_project(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_user_roles_on_project(state, params).await, + } + } /// Revoke grant #[tracing::instrument(level = "info", skip(self, state))] async fn revoke_grant( diff --git a/crates/core/src/assignment/service.rs b/crates/core/src/assignment/service.rs index 9eedf74f..463fa937 100644 --- a/crates/core/src/assignment/service.rs +++ b/crates/core/src/assignment/service.rs @@ -77,6 +77,44 @@ impl AssignmentApi for AssignmentService { Ok(assignments) } + /// List user roles on project + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_user_roles_on_project( + &self, + state: &ServiceState, + params: &RoleAssignmentListParameters, + ) -> Result, AssignmentProviderError> { + // Get assignments for the user on project + let assignments = self.backend_driver.list_assignments(state, params).await?; + + // If no assignments, return empty list + if assignments.is_empty() { + return Ok(Vec::new()); + } + + // Extract unique role IDs from assignments + let unique_role_ids: std::collections::BTreeSet = + assignments.iter().map(|a| a.role_id.clone()).collect(); + + // Fetch all roles and build a map + let all_roles: std::collections::BTreeMap = state + .provider + .get_role_provider() + .list_roles(state, &RoleListParameters::default()) + .await? + .into_iter() + .map(|role| (role.id.clone(), role)) + .collect(); + + // Return only the roles that are in the assignments + let roles: Vec = unique_role_ids + .into_iter() + .filter_map(|role_id| all_roles.get(&role_id).cloned()) + .collect(); + + Ok(roles) + } + /// Revoke grant async fn revoke_grant( &self, @@ -290,4 +328,172 @@ mod tests { assert!(provider.revoke_grant(&state, assignment).await.is_ok()); } + + #[tokio::test] + async fn test_list_user_roles_on_project_no_roles() { + let mut role_mock = MockRoleProvider::default(); + role_mock.expect_list_roles().returning(|_, _| Ok(vec![])); + let state = get_mocked_state(None, Some(Provider::mocked_builder().mock_role(role_mock))); + + let mut backend = MockAssignmentBackend::default(); + backend + .expect_list_assignments() + .returning(|_, _| Ok(vec![])); + + let provider = AssignmentService { + backend_driver: Arc::new(backend), + }; + + let roles = provider + .list_user_roles_on_project( + &state, + &RoleAssignmentListParameters { + user_id: Some("user_id".into()), + project_id: Some("project_id".into()), + ..Default::default() + }, + ) + .await + .unwrap(); + + assert_eq!( + roles.len(), + 0, + "Should return empty list when no assignments" + ); + } + + #[tokio::test] + async fn test_list_user_roles_on_project_single_role() { + let mut role_mock = MockRoleProvider::default(); + role_mock.expect_list_roles().returning(|_, _| { + Ok(vec![ + RoleBuilder::default() + .id("role_id_1") + .name("role_name_1") + .build() + .unwrap(), + ]) + }); + let state = get_mocked_state(None, Some(Provider::mocked_builder().mock_role(role_mock))); + + let mut backend = MockAssignmentBackend::default(); + backend + .expect_list_assignments() + .withf(|_, params: &RoleAssignmentListParameters| { + params.user_id == Some("user_id".into()) + && params.project_id == Some("project_id".into()) + }) + .returning(|_, _| { + Ok(vec![ + AssignmentBuilder::default() + .actor_id("user_id") + .role_id("role_id_1") + .target_id("project_id") + .r#type(AssignmentType::UserProject) + .build() + .unwrap(), + ]) + }); + + let provider = AssignmentService { + backend_driver: Arc::new(backend), + }; + + let roles = provider + .list_user_roles_on_project( + &state, + &RoleAssignmentListParameters { + user_id: Some("user_id".into()), + project_id: Some("project_id".into()), + ..Default::default() + }, + ) + .await + .unwrap(); + + assert_eq!(roles.len(), 1, "Should return one role"); + assert_eq!(roles[0].id, "role_id_1"); + assert_eq!(roles[0].name, "role_name_1"); + } + + #[tokio::test] + async fn test_list_user_roles_on_project_multiple_roles() { + let mut role_mock = MockRoleProvider::default(); + role_mock.expect_list_roles().returning(|_, _| { + Ok(vec![ + RoleBuilder::default() + .id("role_id_1") + .name("role_name_1") + .build() + .unwrap(), + RoleBuilder::default() + .id("role_id_2") + .name("role_name_2") + .build() + .unwrap(), + RoleBuilder::default() + .id("role_id_3") + .name("role_name_3") + .build() + .unwrap(), + ]) + }); + let state = get_mocked_state(None, Some(Provider::mocked_builder().mock_role(role_mock))); + + let mut backend = MockAssignmentBackend::default(); + backend + .expect_list_assignments() + .withf(|_, params: &RoleAssignmentListParameters| { + params.user_id == Some("user_id".into()) + && params.project_id == Some("project_id".into()) + }) + .returning(|_, _| { + Ok(vec![ + AssignmentBuilder::default() + .actor_id("user_id") + .role_id("role_id_1") + .target_id("project_id") + .r#type(AssignmentType::UserProject) + .build() + .unwrap(), + AssignmentBuilder::default() + .actor_id("user_id") + .role_id("role_id_2") + .target_id("project_id") + .r#type(AssignmentType::UserProject) + .build() + .unwrap(), + AssignmentBuilder::default() + .actor_id("user_id") + .role_id("role_id_3") + .target_id("project_id") + .r#type(AssignmentType::UserProject) + .build() + .unwrap(), + ]) + }); + + let provider = AssignmentService { + backend_driver: Arc::new(backend), + }; + + let roles = provider + .list_user_roles_on_project( + &state, + &RoleAssignmentListParameters { + user_id: Some("user_id".into()), + project_id: Some("project_id".into()), + ..Default::default() + }, + ) + .await + .unwrap(); + + assert_eq!(roles.len(), 3, "Should return three roles"); + let role_ids: Vec = roles.iter().map(|r| r.id.clone()).collect(); + assert!(role_ids.contains(&"role_id_1".to_string())); + assert!(role_ids.contains(&"role_id_2".to_string())); + assert!(role_ids.contains(&"role_id_3".to_string())); + } } diff --git a/crates/core/src/assignment/types/provider_api.rs b/crates/core/src/assignment/types/provider_api.rs index 9eb07de5..24c121f0 100644 --- a/crates/core/src/assignment/types/provider_api.rs +++ b/crates/core/src/assignment/types/provider_api.rs @@ -17,6 +17,7 @@ use async_trait::async_trait; use super::assignment::*; use crate::assignment::AssignmentProviderError; use crate::keystone::ServiceState; +use crate::role::types::Role; /// The trait covering [`Role`](crate::role::types::Role) assignments between /// `actors` and `objects`. @@ -49,4 +50,11 @@ pub trait AssignmentApi: Send + Sync { state: &ServiceState, params: Assignment, ) -> Result<(), AssignmentProviderError>; + + /// List user roles on project + async fn list_user_roles_on_project( + &self, + state: &ServiceState, + params: &RoleAssignmentListParameters, + ) -> Result, AssignmentProviderError>; } diff --git a/crates/keystone/src/api/v3/role_assignment/project/user/role.rs b/crates/keystone/src/api/v3/role_assignment/project/user/role.rs index c972d76f..4ad9c6d1 100644 --- a/crates/keystone/src/api/v3/role_assignment/project/user/role.rs +++ b/crates/keystone/src/api/v3/role_assignment/project/user/role.rs @@ -18,8 +18,14 @@ use crate::keystone::ServiceState; mod check; mod grant; +mod list; mod revoke; pub(crate) fn openapi_router() -> OpenApiRouter { - OpenApiRouter::new().routes(routes!(check::check, grant::grant, revoke::revoke)) + OpenApiRouter::new().routes(routes!( + check::check, + grant::grant, + revoke::revoke, + list::list + )) } diff --git a/crates/keystone/src/api/v3/role_assignment/project/user/role/list.rs b/crates/keystone/src/api/v3/role_assignment/project/user/role/list.rs new file mode 100644 index 00000000..b8ea9a00 --- /dev/null +++ b/crates/keystone/src/api/v3/role_assignment/project/user/role/list.rs @@ -0,0 +1,548 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Project user role: list. +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; + +use serde_json::json; +use tracing::info; + +use crate::api::error::KeystoneApiError; +use crate::api::v3::role::types::{Role, RoleList}; +use crate::keystone::ServiceState; +use crate::{ + api::auth::Auth, + assignment::{AssignmentApi, types::RoleAssignmentListParameters}, + identity::IdentityApi, + resource::ResourceApi, +}; + +/// Check whether user has role assignment on project. +/// +/// Validates that a user has a role on a project. +#[utoipa::path( + get, + path = "/projects/{project_id}/users/{user_id}/roles", + operation_id = "/project/user/role:list", + params( + ("project_id" = String, Path, description = "The project ID."), + ("user_id" = String, Path, description = "The user ID.") + ), + responses( + (status = OK, description = "Roles listed successfully.", body = RoleList), + (status = FORBIDDEN, description = "User does not have permission to list roles."), + ), + security(("x-auth" = [])), + tag="role_assignments" +)] +#[tracing::instrument( + name = "api::project_user_role_list", + level = "debug", + skip(state, user_auth), + err(Debug) +)] +pub(super) async fn list( + Auth(user_auth): Auth, + Path((project_id, user_id)): Path<(String, String)>, + State(state): State, +) -> Result { + // Get project and user for policy enforcement + let (project, user) = tokio::join!( + state + .provider + .get_resource_provider() + .get_project(&state, &project_id), + state + .provider + .get_identity_provider() + .get_user(&state, &user_id) + ); + + let project = project?.ok_or_else(|| { + info!("Project {} was not found", project_id); + KeystoneApiError::NotFound { + resource: "project".into(), + identifier: project_id.clone(), + } + })?; + + let user = user?.ok_or_else(|| { + info!("User {} was not found", user_id); + KeystoneApiError::NotFound { + resource: "user".into(), + identifier: user_id.clone(), + } + })?; + + // Enforce policy + state + .policy_enforcer + .enforce( + "identity/project/user/role/list", + &user_auth, + json!({ + "user": user, + "project": project, + "target": user // The user being queried + }), + None, + ) + .await?; + + // Get roles + let query_params = RoleAssignmentListParameters { + user_id: Some(user_id.clone()), + project_id: Some(project_id.clone()), + effective: Some(true), + include_names: Some(false), + ..Default::default() + }; + + let roles: Vec = state + .provider + .get_assignment_provider() + .list_user_roles_on_project(&state, &query_params) + .await? + .into_iter() + .map(Into::into) + .collect(); + + Ok((StatusCode::OK, Json(RoleList { roles })).into_response()) +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + use tower::ServiceExt; + use tower_http::trace::TraceLayer; + use tracing_test::traced_test; + + use crate::api::tests::get_mocked_state; + use crate::api::v3::role_assignment::openapi_router; + use crate::assignment::{MockAssignmentProvider, types::*}; + use crate::identity::{MockIdentityProvider, types::*}; + use crate::provider::Provider; + use crate::resource::{MockResourceProvider, types::Project}; + use crate::role::{MockRoleProvider, types::*}; + + #[tokio::test] + #[traced_test] + async fn test_list_no_roles_allowed() { + let mut identity_mock = MockIdentityProvider::default(); + identity_mock + .expect_get_user() + .withf(|_, id: &'_ str| id == "user_id") + .returning(|_, _| { + Ok(Some( + UserResponseBuilder::default() + .id("user_id") + .domain_id("user_domain_id") + .enabled(true) + .name("name") + .build() + .unwrap(), + )) + }); + + let mut assignment_mock = MockAssignmentProvider::default(); + assignment_mock + .expect_list_user_roles_on_project() + .withf(|_, params: &RoleAssignmentListParameters| { + params.user_id.as_ref().is_some_and(|x| x == "user_id") + && params + .project_id + .as_ref() + .is_some_and(|x| x == "project_id") + && params.effective.is_some_and(|x| x) + }) + .returning(|_, _| Ok(vec![])); + + let role_mock = MockRoleProvider::default(); + let mut resource_mock = MockResourceProvider::default(); + resource_mock + .expect_get_project() + .withf(|_, pid: &'_ str| pid == "project_id") + .returning(|_, id: &'_ str| { + Ok(Some(Project { + id: id.to_string(), + domain_id: "project_domain_id".into(), + ..Default::default() + })) + }); + + let provider_builder = Provider::mocked_builder() + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); + // Policy enforcement allowed + let state = get_mocked_state(provider_builder, true, None, None); + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("GET") + .uri("/projects/project_id/users/user_id/roles") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + #[traced_test] + async fn test_list_single_role_allowed() { + let mut identity_mock = MockIdentityProvider::default(); + identity_mock + .expect_get_user() + .withf(|_, id: &'_ str| id == "user_id") + .returning(|_, _| { + Ok(Some( + UserResponseBuilder::default() + .id("user_id") + .domain_id("user_domain_id") + .enabled(true) + .name("name") + .build() + .unwrap(), + )) + }); + + let mut assignment_mock = MockAssignmentProvider::default(); + assignment_mock + .expect_list_user_roles_on_project() + .withf(|_, params: &RoleAssignmentListParameters| { + params.user_id.as_ref().is_some_and(|x| x == "user_id") + && params + .project_id + .as_ref() + .is_some_and(|x| x == "project_id") + && params.effective.is_some_and(|x| x) + }) + .returning(|_, _| { + Ok(vec![ + RoleBuilder::default() + .id("role_id") + .name("role_name") + .build() + .unwrap(), + ]) + }); + + let role_mock = MockRoleProvider::default(); + let mut resource_mock = MockResourceProvider::default(); + resource_mock + .expect_get_project() + .withf(|_, pid: &'_ str| pid == "project_id") + .returning(|_, id: &'_ str| { + Ok(Some(Project { + id: id.to_string(), + domain_id: "project_domain_id".into(), + ..Default::default() + })) + }); + + let provider_builder = Provider::mocked_builder() + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); + let state = get_mocked_state(provider_builder, true, None, None); + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("GET") + .uri("/projects/project_id/users/user_id/roles") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + #[traced_test] + async fn test_list_multiple_roles_allowed() { + let mut identity_mock = MockIdentityProvider::default(); + identity_mock + .expect_get_user() + .withf(|_, id: &'_ str| id == "user_id") + .returning(|_, _| { + Ok(Some( + UserResponseBuilder::default() + .id("user_id") + .domain_id("user_domain_id") + .enabled(true) + .name("name") + .build() + .unwrap(), + )) + }); + + let mut assignment_mock = MockAssignmentProvider::default(); + assignment_mock + .expect_list_user_roles_on_project() + .withf(|_, params: &RoleAssignmentListParameters| { + params.user_id.as_ref().is_some_and(|x| x == "user_id") + && params + .project_id + .as_ref() + .is_some_and(|x| x == "project_id") + && params.effective.is_some_and(|x| x) + }) + .returning(|_, _| { + Ok(vec![ + RoleBuilder::default() + .id("role_id_1") + .name("role_name_1") + .build() + .unwrap(), + RoleBuilder::default() + .id("role_id_2") + .name("role_name_2") + .build() + .unwrap(), + RoleBuilder::default() + .id("role_id_3") + .name("role_name_3") + .build() + .unwrap(), + ]) + }); + + let role_mock = MockRoleProvider::default(); + let mut resource_mock = MockResourceProvider::default(); + resource_mock + .expect_get_project() + .withf(|_, pid: &'_ str| pid == "project_id") + .returning(|_, id: &'_ str| { + Ok(Some(Project { + id: id.to_string(), + domain_id: "project_domain_id".into(), + ..Default::default() + })) + }); + + let provider_builder = Provider::mocked_builder() + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); + let state = get_mocked_state(provider_builder, true, None, None); + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("GET") + .uri("/projects/project_id/users/user_id/roles") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + #[traced_test] + async fn test_list_policy_forbidden() { + let mut identity_mock = MockIdentityProvider::default(); + identity_mock + .expect_get_user() + .withf(|_, id: &'_ str| id == "user_id") + .returning(|_, _| { + Ok(Some( + UserResponseBuilder::default() + .id("user_id") + .domain_id("user_domain_id") + .enabled(true) + .name("name") + .build() + .unwrap(), + )) + }); + + let assignment_mock = MockAssignmentProvider::default(); + let role_mock = MockRoleProvider::default(); + let mut resource_mock = MockResourceProvider::default(); + resource_mock + .expect_get_project() + .withf(|_, pid: &'_ str| pid == "project_id") + .returning(|_, id: &'_ str| { + Ok(Some(Project { + id: id.to_string(), + domain_id: "project_domain_id".into(), + ..Default::default() + })) + }); + + let provider_builder = Provider::mocked_builder() + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); + // Policy enforcement NOT allowed + let state = get_mocked_state(provider_builder, false, None, None); + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("GET") + .uri("/projects/project_id/users/user_id/roles") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::FORBIDDEN); + } + + #[tokio::test] + #[traced_test] + async fn test_list_user_not_found() { + let mut identity_mock = MockIdentityProvider::default(); + identity_mock + .expect_get_user() + .withf(|_, id: &'_ str| id == "user_id") + .returning(|_, _| Ok(None)); + + let assignment_mock = MockAssignmentProvider::default(); + let role_mock = MockRoleProvider::default(); + let mut resource_mock = MockResourceProvider::default(); + resource_mock + .expect_get_project() + .withf(|_, pid: &'_ str| pid == "project_id") + .returning(|_, id: &'_ str| { + Ok(Some(Project { + id: id.to_string(), + domain_id: "project_domain_id".into(), + ..Default::default() + })) + }); + + let provider_builder = Provider::mocked_builder() + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); + let state = get_mocked_state(provider_builder, true, None, None); + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("GET") + .uri("/projects/project_id/users/user_id/roles") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } + + #[tokio::test] + #[traced_test] + async fn test_list_project_not_found() { + let mut identity_mock = MockIdentityProvider::default(); + identity_mock + .expect_get_user() + .withf(|_, id: &'_ str| id == "user_id") + .returning(|_, _| { + Ok(Some( + UserResponseBuilder::default() + .id("user_id") + .domain_id("user_domain_id") + .enabled(true) + .name("name") + .build() + .unwrap(), + )) + }); + + let assignment_mock = MockAssignmentProvider::default(); + let role_mock = MockRoleProvider::default(); + let mut resource_mock = MockResourceProvider::default(); + resource_mock + .expect_get_project() + .withf(|_, pid: &'_ str| pid == "project_id") + .returning(|_, _| Ok(None)); + + let provider_builder = Provider::mocked_builder() + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); + let state = get_mocked_state(provider_builder, true, None, None); + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("GET") + .uri("/projects/project_id/users/user_id/roles") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } +} diff --git a/policy/project/user/role/list.rego b/policy/project/user/role/list.rego new file mode 100644 index 00000000..5c072d56 --- /dev/null +++ b/policy/project/user/role/list.rego @@ -0,0 +1,33 @@ +package identity.project.user.role.list + +import data.identity +import data.identity.assignment + +# List roles granted to a user on a project + +default allow := false + +allow if { + "admin" in input.credentials.roles +} + +allow if { + "reader" in input.credentials.roles + input.credentials.scope == "system" +} + +allow if { + "reader" in input.credentials.roles + assignment.project_user_role_domain_matches +} + +violation contains {"field": "role", "msg": "listing user roles on a project requires admin or reader role."} if { + not "admin" in input.credentials.roles + not "reader" in input.credentials.roles +} + +violation contains {"field": "scope", "msg": "reader role requires system scope or domain scope matching the user and project domain."} if { + "reader" in input.credentials.roles + input.credentials.scope != "system" + not assignment.project_user_role_domain_matches +} diff --git a/policy/project/user/role/list_test.rego b/policy/project/user/role/list_test.rego new file mode 100644 index 00000000..29019064 --- /dev/null +++ b/policy/project/user/role/list_test.rego @@ -0,0 +1,18 @@ +package test_project_user_role_list + +import data.identity.project.user.role.list + +test_allowed if { + list.allow with input as {"credentials": {"roles": ["admin"]}} + list.allow with input as {"credentials": {"roles": ["reader"], "scope": "system"}} + list.allow with input as {"credentials": {"roles": ["reader"], "domain_id": "foo"}, "target": {"user": {"domain_id": "foo"}, "project": {"domain_id": "foo"}, "role": {"domain_id": null}}} + list.allow with input as {"credentials": {"roles": ["reader"], "domain_id": "foo"}, "target": {"user": {"domain_id": "foo"}, "project": {"domain_id": "foo"}, "role": {"domain_id": "foo"}}} +} + +test_forbidden if { + not list.allow with input as {"credentials": {"roles": []}} + not list.allow with input as {"credentials": {"roles": ["member"]}} + not list.allow with input as {"credentials": {"roles": ["reader"], "domain_id": "foo"}, "target": {"user": {"domain_id": "bar"}, "project": {"domain_id": "foo"}, "role": {"domain_id": "foo"}}} + not list.allow with input as {"credentials": {"roles": ["reader"], "domain_id": "foo"}, "target": {"user": {"domain_id": "foo"}, "project": {"domain_id": "bar"}, "role": {"domain_id": "foo"}}} + not list.allow with input as {"credentials": {"roles": ["reader"], "domain_id": "foo"}, "target": {"user": {"domain_id": "bar"}, "project": {"domain_id": "bar"}, "role": {"domain_id": "bar"}}} +}