diff --git a/crates/keystone/src/api/v3/group/create.rs b/crates/keystone/src/api/v3/group/create.rs index 9390fe47..4ef82e47 100644 --- a/crates/keystone/src/api/v3/group/create.rs +++ b/crates/keystone/src/api/v3/group/create.rs @@ -12,6 +12,7 @@ // // SPDX-License-Identifier: Apache-2.0 use axum::{Json, debug_handler, extract::State, http::StatusCode, response::IntoResponse}; +use validator::Validate; use super::types::{Group, GroupCreateRequest, GroupResponse}; use crate::api::auth::Auth; @@ -35,6 +36,17 @@ pub async fn create( State(state): State, Json(req): Json, ) -> Result { + req.validate()?; + state + .policy_enforcer + .enforce( + "identity/group/create", + &user_auth, + serde_json::to_value(&req.group)?, + None, + ) + .await?; + let res = state .provider .get_identity_provider() @@ -126,4 +138,81 @@ mod tests { assert_eq!(res.group.name, req.group.name); assert_eq!(res.group.domain_id, req.group.domain_id); } + + #[tokio::test] + async fn test_create_unauth() { + let state = crate::api::tests::get_mocked_state( + crate::provider::Provider::mocked_builder(), + false, + None, + None, + ) + .await; + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let req = crate::api::v3::group::types::GroupCreateRequest { + group: crate::api::v3::group::types::GroupCreateBuilder::default() + .domain_id("domain") + .name("name") + .build() + .unwrap(), + }; + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("POST") + .header(header::CONTENT_TYPE, "application/json") + .uri("/") + .body(Body::from(serde_json::to_string(&req).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn test_create_not_allowed() { + let state = crate::api::tests::get_mocked_state( + crate::provider::Provider::mocked_builder(), + false, + None, + None, + ) + .await; + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let req = crate::api::v3::group::types::GroupCreateRequest { + group: crate::api::v3::group::types::GroupCreateBuilder::default() + .domain_id("domain") + .name("name") + .build() + .unwrap(), + }; + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("POST") + .header(header::CONTENT_TYPE, "application/json") + .uri("/") + .header("x-auth-token", "foo") + .body(Body::from(serde_json::to_string(&req).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::FORBIDDEN); + } } diff --git a/crates/keystone/src/api/v3/group/remove.rs b/crates/keystone/src/api/v3/group/delete.rs similarity index 55% rename from crates/keystone/src/api/v3/group/remove.rs rename to crates/keystone/src/api/v3/group/delete.rs index e52cc6c2..b103529a 100644 --- a/crates/keystone/src/api/v3/group/remove.rs +++ b/crates/keystone/src/api/v3/group/delete.rs @@ -34,11 +34,27 @@ use crate::keystone::ServiceState; tag="groups" )] #[tracing::instrument(name = "api::group_delete", level = "debug", skip(state))] -pub async fn remove( +pub async fn delete( Auth(user_auth): Auth, Path(group_id): Path, State(state): State, ) -> Result { + let current = state + .provider + .get_identity_provider() + .get_group(&state, &group_id) + .await?; + + state + .policy_enforcer + .enforce( + "identity/group/delete", + &user_auth, + serde_json::to_value(¤t)?, + None, + ) + .await?; + state .provider .get_identity_provider() @@ -56,6 +72,8 @@ mod tests { use tower::ServiceExt; // for `call`, `oneshot`, and `ready` use tower_http::trace::TraceLayer; + use openstack_keystone_core_types::identity::*; + use super::super::openapi_router; use crate::api::tests::get_mocked_state; use crate::identity::{MockIdentityProvider, error::IdentityProviderError}; @@ -65,10 +83,22 @@ mod tests { async fn test_delete() { let mut identity_mock = MockIdentityProvider::default(); identity_mock - .expect_delete_group() + .expect_get_group() .withf(|_, id: &'_ str| id == "foo") .returning(|_, _| Err(IdentityProviderError::GroupNotFound("foo".into()))); + identity_mock + .expect_get_group() + .withf(|_, id: &'_ str| id == "bar") + .returning(|_, _| { + Ok(Some(Group { + id: "bar".into(), + name: "name".into(), + domain_id: "did".into(), + ..Default::default() + })) + }); + identity_mock .expect_delete_group() .withf(|_, id: &'_ str| id == "bar") @@ -116,4 +146,76 @@ mod tests { assert_eq!(response.status(), StatusCode::NO_CONTENT); } + + #[tokio::test] + async fn test_remove_unauth() { + let state = crate::api::tests::get_mocked_state( + crate::provider::Provider::mocked_builder(), + false, + None, + None, + ) + .await; + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("DELETE") + .uri("/foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn test_delete_not_allowed() { + let mut identity_mock = MockIdentityProvider::default(); + identity_mock + .expect_get_group() + .withf(|_, id: &'_ str| id == "foo") + .returning(|_, _| { + Ok(Some(Group { + id: "foo".into(), + name: "name".into(), + domain_id: "did".into(), + ..Default::default() + })) + }); + + let state = get_mocked_state( + Provider::mocked_builder().mock_identity(identity_mock), + false, + None, + None, + ) + .await; + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("DELETE") + .uri("/foo") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::FORBIDDEN); + } } diff --git a/crates/keystone/src/api/v3/group/list.rs b/crates/keystone/src/api/v3/group/list.rs index 07bf5721..73d3f584 100644 --- a/crates/keystone/src/api/v3/group/list.rs +++ b/crates/keystone/src/api/v3/group/list.rs @@ -17,6 +17,7 @@ use axum::{ http::StatusCode, response::IntoResponse, }; +use validator::Validate; use super::types::{Group, GroupList, GroupListParameters}; use crate::api::auth::Auth; @@ -41,6 +42,17 @@ pub async fn list( Query(query): Query, State(state): State, ) -> Result { + query.validate()?; + state + .policy_enforcer + .enforce( + "identity/group/list", + &user_auth, + serde_json::to_value(&query)?, + None, + ) + .await?; + let groups: Vec = state .provider .get_identity_provider() @@ -186,4 +198,33 @@ mod tests { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } + + #[tokio::test] + async fn test_list_not_allowed() { + let state = crate::api::tests::get_mocked_state( + crate::provider::Provider::mocked_builder(), + false, + None, + None, + ) + .await; + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::FORBIDDEN); + } } diff --git a/crates/keystone/src/api/v3/group/mod.rs b/crates/keystone/src/api/v3/group/mod.rs index c0a7d505..85a7dafa 100644 --- a/crates/keystone/src/api/v3/group/mod.rs +++ b/crates/keystone/src/api/v3/group/mod.rs @@ -17,13 +17,16 @@ use utoipa_axum::{router::OpenApiRouter, routes}; use crate::keystone::ServiceState; pub mod create; +pub mod delete; pub mod list; -pub mod remove; pub mod show; pub mod types; pub(crate) fn openapi_router() -> OpenApiRouter { OpenApiRouter::new() .routes(routes!(list::list, create::create)) - .routes(routes!(show::show, remove::remove)) + .routes(routes!(show::show, delete::delete)) } + +#[cfg(test)] +mod tests {} diff --git a/crates/keystone/src/api/v3/group/show.rs b/crates/keystone/src/api/v3/group/show.rs index 815994e5..68558f99 100644 --- a/crates/keystone/src/api/v3/group/show.rs +++ b/crates/keystone/src/api/v3/group/show.rs @@ -41,22 +41,31 @@ pub async fn show( Path(group_id): Path, State(state): State, ) -> Result { + let current = state + .provider + .get_identity_provider() + .get_group(&state, &group_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "group".into(), + identifier: group_id, + }) + })??; + state + .policy_enforcer + .enforce( + "identity/group/show", + &user_auth, + serde_json::to_value(¤t)?, + None, + ) + .await?; + Ok(( StatusCode::OK, Json(GroupResponse { - group: Group::from( - state - .provider - .get_identity_provider() - .get_group(&state, &group_id) - .await - .map(|x| { - x.ok_or_else(|| KeystoneApiError::NotFound { - resource: "group".into(), - identifier: group_id, - }) - })??, - ), + group: Group::from(current), }), ) .into_response()) @@ -153,4 +162,70 @@ mod tests { res.group, ); } + + #[tokio::test] + async fn test_get_unauth() { + let state = crate::api::tests::get_mocked_state( + crate::provider::Provider::mocked_builder(), + false, + None, + None, + ) + .await; + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot(Request::builder().uri("/foo").body(Body::empty()).unwrap()) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn test_get_not_allowed() { + let mut identity_mock = MockIdentityProvider::default(); + + identity_mock + .expect_get_group() + .withf(|_, id: &'_ str| id == "foo") + .returning(|_, _| { + Ok(Some(Group { + id: "foo".into(), + name: "name".into(), + domain_id: "did".into(), + ..Default::default() + })) + }); + + let state = get_mocked_state( + Provider::mocked_builder().mock_identity(identity_mock), + false, + None, + None, + ) + .await; + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/foo") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::FORBIDDEN); + } } diff --git a/crates/keystone/src/api/v4/group/mod.rs b/crates/keystone/src/api/v4/group/mod.rs index 0b8192be..2da812f0 100644 --- a/crates/keystone/src/api/v4/group/mod.rs +++ b/crates/keystone/src/api/v4/group/mod.rs @@ -23,325 +23,4 @@ pub(super) fn openapi_router() -> OpenApiRouter { } #[cfg(test)] -mod tests { - use axum::{ - body::Body, - http::{Request, StatusCode, header}, - }; - use http_body_util::BodyExt; // for `collect` - use tower::ServiceExt; // for `call`, `oneshot`, and `ready` - use tower_http::trace::TraceLayer; - - use openstack_keystone_core_types::identity::{Group, GroupCreate, GroupListParameters}; - - use super::openapi_router; - use crate::api::tests::get_mocked_state; - use crate::api::v3::group::types::{ - GroupBuilder as ApiGroupBuilder, GroupCreateBuilder as ApiGroupCreateBuilder, - GroupCreateRequest, GroupList, GroupResponse, - }; - use crate::identity::{MockIdentityProvider, error::IdentityProviderError}; - use crate::provider::Provider; - - #[tokio::test] - async fn test_list() { - let mut identity_mock = MockIdentityProvider::default(); - identity_mock - .expect_list_groups() - .withf(|_, _: &GroupListParameters| true) - .returning(|_, _| { - Ok(vec![Group { - id: "1".into(), - name: "2".into(), - domain_id: "did".into(), - ..Default::default() - }]) - }); - - let state = get_mocked_state( - Provider::mocked_builder().mock_identity(identity_mock), - true, - None, - None, - ) - .await; - - let mut api = openapi_router() - .layer(TraceLayer::new_for_http()) - .with_state(state); - - let response = api - .as_service() - .oneshot( - Request::builder() - .uri("/") - .header("x-auth-token", "foo") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - - let body = response.into_body().collect().await.unwrap().to_bytes(); - let res: GroupList = serde_json::from_slice(&body).unwrap(); - assert_eq!( - vec![ - ApiGroupBuilder::default() - .id("1") - .name("2") - .domain_id("did") - .build() - .unwrap() - ], - res.groups - ); - } - - #[tokio::test] - async fn test_list_qp() { - let mut identity_mock = MockIdentityProvider::default(); - identity_mock - .expect_list_groups() - .withf(|_, qp: &GroupListParameters| { - GroupListParameters { - domain_id: Some("domain".into()), - name: Some("name".into()), - } == *qp - }) - .returning(|_, _| Ok(Vec::new())); - - let state = get_mocked_state( - Provider::mocked_builder().mock_identity(identity_mock), - true, - None, - None, - ) - .await; - - let mut api = openapi_router() - .layer(TraceLayer::new_for_http()) - .with_state(state); - - let response = api - .as_service() - .oneshot( - Request::builder() - .uri("/?domain_id=domain&name=name") - .header("x-auth-token", "foo") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - - let body = response.into_body().collect().await.unwrap().to_bytes(); - let _res: GroupList = serde_json::from_slice(&body).unwrap(); - } - - #[tokio::test] - async fn test_list_unauth() { - let state = get_mocked_state(Provider::mocked_builder(), false, None, None).await; - - let mut api = openapi_router() - .layer(TraceLayer::new_for_http()) - .with_state(state); - - let response = api - .as_service() - .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn test_get() { - let mut identity_mock = MockIdentityProvider::default(); - identity_mock - .expect_get_group() - .withf(|_, id: &'_ str| id == "foo") - .returning(|_, _| Ok(None)); - - identity_mock - .expect_get_group() - .withf(|_, id: &'_ str| id == "bar") - .returning(|_, _| { - Ok(Some(Group { - id: "bar".into(), - name: "name".into(), - domain_id: "did".into(), - ..Default::default() - })) - }); - - let state = get_mocked_state( - Provider::mocked_builder().mock_identity(identity_mock), - true, - None, - None, - ) - .await; - - let mut api = openapi_router() - .layer(TraceLayer::new_for_http()) - .with_state(state.clone()); - - let response = api - .as_service() - .oneshot( - Request::builder() - .uri("/foo") - .header("x-auth-token", "foo") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - - let response = api - .as_service() - .oneshot( - Request::builder() - .uri("/bar") - .header("x-auth-token", "foo") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - - let body = response.into_body().collect().await.unwrap().to_bytes(); - let res: GroupResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!( - ApiGroupBuilder::default() - .id("bar") - .name("name") - .domain_id("did") - .build() - .unwrap(), - res.group, - ); - } - - #[tokio::test] - async fn test_create() { - let mut identity_mock = MockIdentityProvider::default(); - identity_mock - .expect_create_group() - .withf(|_, req: &GroupCreate| req.domain_id == "domain" && req.name == "name") - .returning(|_, req| { - Ok(Group { - id: "bar".into(), - domain_id: req.domain_id, - name: req.name, - ..Default::default() - }) - }); - - let state = get_mocked_state( - Provider::mocked_builder().mock_identity(identity_mock), - true, - None, - None, - ) - .await; - - let mut api = openapi_router() - .layer(TraceLayer::new_for_http()) - .with_state(state.clone()); - - let req = GroupCreateRequest { - group: ApiGroupCreateBuilder::default() - .domain_id("domain") - .name("name") - .build() - .unwrap(), - }; - - let response = api - .as_service() - .oneshot( - Request::builder() - .method("POST") - .header(header::CONTENT_TYPE, "application/json") - .uri("/") - .header("x-auth-token", "foo") - .body(Body::from(serde_json::to_string(&req).unwrap())) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::CREATED); - - let body = response.into_body().collect().await.unwrap().to_bytes(); - let res: GroupResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(res.group.name, req.group.name); - assert_eq!(res.group.domain_id, req.group.domain_id); - } - - #[tokio::test] - async fn test_delete() { - let mut identity_mock = MockIdentityProvider::default(); - identity_mock - .expect_delete_group() - .withf(|_, id: &'_ str| id == "foo") - .returning(|_, _| Err(IdentityProviderError::GroupNotFound("foo".into()))); - - identity_mock - .expect_delete_group() - .withf(|_, id: &'_ str| id == "bar") - .returning(|_, _| Ok(())); - - let state = get_mocked_state( - Provider::mocked_builder().mock_identity(identity_mock), - true, - None, - None, - ) - .await; - - let mut api = openapi_router() - .layer(TraceLayer::new_for_http()) - .with_state(state.clone()); - - let response = api - .as_service() - .oneshot( - Request::builder() - .method("DELETE") - .uri("/foo") - .header("x-auth-token", "foo") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - - let response = api - .as_service() - .oneshot( - Request::builder() - .method("DELETE") - .uri("/bar") - .header("x-auth-token", "foo") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NO_CONTENT); - } -} +mod tests {} diff --git a/policy/identity.rego b/policy/identity.rego index 096a3bb8..12dde020 100644 --- a/policy/identity.rego +++ b/policy/identity.rego @@ -77,3 +77,13 @@ foreign_target if { input.target.domain_id != null input.target.domain_id != input.credentials.domain_id } + +project_domain_matches_domain_scope if { + input.target.project.domain_id != null + input.target.project.domain_id = input.credentials.domain_id +} + +domain_matches_domain_scope if { + input.target.domain_id != null + input.target.domain_id = input.credentials.domain_id +} diff --git a/policy/identity/group/create.rego b/policy/identity/group/create.rego new file mode 100644 index 00000000..0664269b --- /dev/null +++ b/policy/identity/group/create.rego @@ -0,0 +1,28 @@ +package identity.group.create + +import data.identity + +# Create a new user group +# +default allow := false + +allow if { + "admin" in input.credentials.roles +} + +allow if { + "manager" in input.credentials.roles + identity.domain_matches_domain_scope +} + +violation contains {"field": "domain_id", "msg": "creating a new user group in domain different to the domain scope requires `admin` role."} if { + not "admin" in input.credentials.roles + "manager" in input.credentials.roles + not identity.domain_matches_domain_scope +} + +violation contains {"field": "domain_id", "msg": "creating a new user group requires a manager role with the domain scope."} if { + not "admin" in input.credentials.roles + not "manager" in input.credentials.roles + identity.domain_matches_domain_scope +} diff --git a/policy/identity/group/create_test.rego b/policy/identity/group/create_test.rego new file mode 100644 index 00000000..469f0adb --- /dev/null +++ b/policy/identity/group/create_test.rego @@ -0,0 +1,15 @@ +package test_group_create + +import data.identity.group.create + +test_allowed if { + create.allow with input as {"credentials": {"roles": ["admin"]}} + create.allow with input as {"credentials": {"roles": ["manager"], "domain_id": "foo"}, "target": {"domain_id": "foo"}} +} + +test_forbidden if { + not create.allow with input as {"credentials": {"roles": []}} + not create.allow with input as {"credentials": {"roles": ["manager"], "domain_id": "foo"}, "target": {"domain_id": "foo1"}} + not create.allow with input as {"credentials": {"roles": ["manager"]}, "target": {"domain_id": "foo"}} + not create.allow with input as {"credentials": {"roles": ["reader"], "domain_id": "foo"}, "target": {"domain_id": "foo"}} +} diff --git a/policy/identity/group/delete.rego b/policy/identity/group/delete.rego new file mode 100644 index 00000000..ca2d484e --- /dev/null +++ b/policy/identity/group/delete.rego @@ -0,0 +1,26 @@ +package identity.group.delete + +import data.identity + +default allow := false + +allow if { + "admin" in input.credentials.roles +} + +allow if { + "manager" in input.credentials.roles + identity.domain_matches_domain_scope +} + +violation contains {"field": "domain_id", "msg": "removing a user group in domain different to the domain scope requires `admin` role."} if { + not "admin" in input.credentials.roles + "manager" in input.credentials.roles + not identity.domain_matches_domain_scope +} + +violation contains {"field": "domain_id", "msg": "removing a user group requires a manager role with the domain scope."} if { + not "admin" in input.credentials.roles + not "manager" in input.credentials.roles + identity.domain_matches_domain_scope +} diff --git a/policy/identity/group/delete_test.rego b/policy/identity/group/delete_test.rego new file mode 100644 index 00000000..a6117873 --- /dev/null +++ b/policy/identity/group/delete_test.rego @@ -0,0 +1,15 @@ +package test_group_delete + +import data.identity.group.delete + +test_allowed if { + delete.allow with input as {"credentials": {"roles": ["admin"]}} + delete.allow with input as {"credentials": {"roles": ["manager"], "domain_id": "foo"}, "target": {"domain_id": "foo"}} +} + +test_forbidden if { + not delete.allow with input as {"credentials": {"roles": []}} + not delete.allow with input as {"credentials": {"roles": ["manager"], "domain_id": "foo"}, "target": {"domain_id": "foo1"}} + not delete.allow with input as {"credentials": {"roles": ["manager"]}, "target": {"domain_id": "foo"}} + not delete.allow with input as {"credentials": {"roles": ["reader"], "domain_id": "foo"}, "target": {"domain_id": "foo"}} +} diff --git a/policy/identity/group/list.rego b/policy/identity/group/list.rego new file mode 100644 index 00000000..f654dff3 --- /dev/null +++ b/policy/identity/group/list.rego @@ -0,0 +1,26 @@ +package identity.group.list + +import data.identity + +default allow := false + +allow if { + "admin" in input.credentials.roles +} + +allow if { + "reader" in input.credentials.roles + identity.domain_matches_domain_scope +} + +violation contains {"field": "domain_id", "msg": "listing user groups in domain different to the domain scope requires `admin` role."} if { + not "admin" in input.credentials.roles + "manager" in input.credentials.roles + not identity.domain_matches_domain_scope +} + +violation contains {"field": "domain_id", "msg": "listing user groups requires a reader role with the domain scope."} if { + not "admin" in input.credentials.roles + not "reader" in input.credentials.roles + identity.domain_matches_domain_scope +} diff --git a/policy/identity/group/list_test.rego b/policy/identity/group/list_test.rego new file mode 100644 index 00000000..fcd82f5b --- /dev/null +++ b/policy/identity/group/list_test.rego @@ -0,0 +1,15 @@ +package test_group_list + +import data.identity.group.list + +test_allowed if { + list.allow with input as {"credentials": {"roles": ["admin"]}} + list.allow with input as {"credentials": {"roles": ["reader"], "domain_id": "foo"}, "target": {"domain_id": "foo"}} +} + +test_forbidden if { + not list.allow with input as {"credentials": {"roles": []}} + not list.allow with input as {"credentials": {"roles": ["manager"], "domain_id": "foo"}, "target": {"domain_id": "foo1"}} + not list.allow with input as {"credentials": {"roles": ["manager"]}, "target": {"domain_id": "foo"}} + not list.allow with input as {"credentials": {"roles": ["reader"], "domain_id": "foo"}, "target": {"domain_id": "foo2"}} +} diff --git a/policy/identity/group/show.rego b/policy/identity/group/show.rego new file mode 100644 index 00000000..4add3649 --- /dev/null +++ b/policy/identity/group/show.rego @@ -0,0 +1,26 @@ +package identity.group.show + +import data.identity + +default allow := false + +allow if { + "admin" in input.credentials.roles +} + +allow if { + "reader" in input.credentials.roles + identity.domain_matches_domain_scope +} + +violation contains {"field": "domain_id", "msg": "reading a user group in domain different to the domain scope requires `admin` role."} if { + not "admin" in input.credentials.roles + "manager" in input.credentials.roles + not identity.domain_matches_domain_scope +} + +violation contains {"field": "domain_id", "msg": "reading a user group requires a reader role with the domain scope."} if { + not "admin" in input.credentials.roles + not "reader" in input.credentials.roles + identity.domain_matches_domain_scope +} diff --git a/policy/identity/group/show_test.rego b/policy/identity/group/show_test.rego new file mode 100644 index 00000000..3b8b6e82 --- /dev/null +++ b/policy/identity/group/show_test.rego @@ -0,0 +1,15 @@ +package test_group_show + +import data.identity.group.show + +test_allowed if { + show.allow with input as {"credentials": {"roles": ["admin"]}} + show.allow with input as {"credentials": {"roles": ["manager", "reader"], "domain_id": "foo"}, "target": {"domain_id": "foo"}} +} + +test_forbidden if { + not show.allow with input as {"credentials": {"roles": []}} + not show.allow with input as {"credentials": {"roles": ["manager"], "domain_id": "foo"}, "target": {"domain_id": "foo1"}} + not show.allow with input as {"credentials": {"roles": ["manager"]}, "target": {"domain_id": "foo"}} + not show.allow with input as {"credentials": {"roles": ["reader"], "domain_id": "foo"}, "target": {"domain_id": "foo2"}} +}