diff --git a/Cargo.lock b/Cargo.lock index 2fd316a8..e25e07dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -999,7 +999,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1306,9 +1306,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" [[package]] name = "hex" @@ -1631,13 +1631,13 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1951,9 +1951,9 @@ checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "oorandom" -version = "11.1.4" +version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl" @@ -2632,15 +2632,15 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f8dcd64f141950290e45c99f7710ede1b600297c91818bb30b3667c0f45dc0" +checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3254,7 +3254,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3390,9 +3390,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.43.0" +version = "1.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" dependencies = [ "backtrace", "bytes", @@ -3898,7 +3898,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index be0e5510..4c6c5885 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ serde = { version = "^1.0" } serde_bytes = "0.11.16" serde_json = { version = "^1.0" } thiserror = { version = "^2.0" } -tokio = { version = "^1.43", features = ["fs", "macros", "signal", "rt-multi-thread"] } +tokio = { version = "^1.44", features = ["fs", "macros", "signal", "rt-multi-thread"] } tower = { version = "^0.5" } tower-http = { version = "^0.6", features = ["compression-full", "request-id", "sensitive-headers", "trace", "util"] } tracing = { version = "^0.1" } diff --git a/deny.toml b/deny.toml index 939ca7ff..705f1b4d 100644 --- a/deny.toml +++ b/deny.toml @@ -72,7 +72,8 @@ feature-depth = 1 ignore = [ #"RUSTSEC-0000-0000", "RUSTSEC-2023-0018", - "RUSTSEC-2023-0071" + "RUSTSEC-2023-0071", + "RUSTSEC-2024-0436" #{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, diff --git a/src/api/v3/role_assignment/mod.rs b/src/api/v3/role_assignment/mod.rs index da720075..f03dec84 100644 --- a/src/api/v3/role_assignment/mod.rs +++ b/src/api/v3/role_assignment/mod.rs @@ -42,16 +42,20 @@ pub(super) fn openapi_router() -> OpenApiRouter { ), tag="roles" )] -#[tracing::instrument(name = "api::role_assignment_list", level = "debug", skip(state))] +#[tracing::instrument( + name = "api::role_assignment_list", + level = "debug", + skip(state, _user_auth) +)] async fn list( - Auth(user_auth): Auth, + Auth(_user_auth): Auth, Query(query): Query, State(state): State, ) -> Result { let assignments: Result, _> = state .provider .get_assignment_provider() - .list_role_assignments(&state.db, &query.try_into()?) + .list_role_assignments(&state.db, &state.provider, &query.try_into()?) .await .map_err(KeystoneApiError::assignment)? .into_iter() @@ -87,7 +91,7 @@ mod tests { use crate::config::Config; use crate::identity::MockIdentityProvider; use crate::keystone::{Service, ServiceState}; - use crate::provider::ProviderBuilder; + use crate::provider::{Provider, ProviderBuilder}; use crate::resource::MockResourceProvider; use crate::token::{MockTokenProvider, Token, UnscopedToken}; @@ -121,8 +125,8 @@ mod tests { let mut assignment_mock = MockAssignmentProvider::default(); assignment_mock .expect_list_role_assignments() - .withf(|_: &DatabaseConnection, _: &RoleAssignmentListParameters| true) - .returning(|_, _| { + .withf(|_: &DatabaseConnection, _: &Provider, _: &RoleAssignmentListParameters| true) + .returning(|_, _, _| { Ok(vec![Assignment { role_id: "role".into(), actor_id: "actor".into(), @@ -173,16 +177,16 @@ mod tests { assignment_mock .expect_list_role_assignments() .withf( - |_: &DatabaseConnection, qp: &RoleAssignmentListParameters| { + |_: &DatabaseConnection, _: &Provider, qp: &RoleAssignmentListParameters| { RoleAssignmentListParameters { role_id: Some("role".into()), - actor_id: Some("user1".into()), - target_id: Some("project1".into()), + user_id: Some("user1".into()), + project_id: Some("project1".into()), ..Default::default() } == *qp }, ) - .returning(|_, _| { + .returning(|_, _, _| { Ok(vec![Assignment { role_id: "role".into(), actor_id: "actor".into(), @@ -195,16 +199,16 @@ mod tests { assignment_mock .expect_list_role_assignments() .withf( - |_: &DatabaseConnection, qp: &RoleAssignmentListParameters| { + |_: &DatabaseConnection, _: &Provider, qp: &RoleAssignmentListParameters| { RoleAssignmentListParameters { role_id: Some("role".into()), - actor_id: Some("user2".into()), - target_id: Some("domain2".into()), + user_id: Some("user2".into()), + domain_id: Some("domain2".into()), ..Default::default() } == *qp }, ) - .returning(|_, _| { + .returning(|_, _, _| { Ok(vec![Assignment { role_id: "role".into(), actor_id: "actor".into(), @@ -217,15 +221,15 @@ mod tests { assignment_mock .expect_list_role_assignments() .withf( - |_: &DatabaseConnection, qp: &RoleAssignmentListParameters| { + |_: &DatabaseConnection, _: &Provider, qp: &RoleAssignmentListParameters| { RoleAssignmentListParameters { - actor_id: Some("user3".into()), - target_id: Some("project3".into()), + group_id: Some("group3".into()), + project_id: Some("project3".into()), ..Default::default() } == *qp }, ) - .returning(|_, _| { + .returning(|_, _, _| { Ok(vec![Assignment { role_id: "role".into(), actor_id: "actor".into(), @@ -276,7 +280,7 @@ mod tests { .as_service() .oneshot( Request::builder() - .uri("/?group.id=user3&scope.project.id=project3") + .uri("/?group.id=group3&scope.project.id=project3") .header("x-auth-token", "foo") .body(Body::empty()) .unwrap(), diff --git a/src/api/v3/role_assignment/types.rs b/src/api/v3/role_assignment/types.rs index eac9ba42..6af0c7df 100644 --- a/src/api/v3/role_assignment/types.rs +++ b/src/api/v3/role_assignment/types.rs @@ -139,18 +139,32 @@ impl IntoResponse for AssignmentList { } } +/// List role assignments query parameters #[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)] pub struct RoleAssignmentListParameters { - #[serde(rename = "role.id")] - pub role_id: Option, - #[serde(rename = "user.id")] - pub user_id: Option, + /// Filters the response by a domain ID. + #[serde(rename = "scope.domain.id")] + pub domain_id: Option, + + /// Filters the response by a group ID. #[serde(rename = "group.id")] pub group_id: Option, + + /// Returns the effective assignments, including any assignments gained by virtue of group + /// membership. + pub effective: Option, + + /// Filters the response by a project ID. #[serde(rename = "scope.project.id")] pub project_id: Option, - #[serde(rename = "scope.domain.id")] - pub domain_id: Option, + + /// Filters the response by a role ID. + #[serde(rename = "role.id")] + pub role_id: Option, + + /// Filters the response by a user ID. + #[serde(rename = "user.id")] + pub user_id: Option, } impl TryFrom for types::RoleAssignmentListParameters { @@ -158,18 +172,27 @@ impl TryFrom for types::RoleAssignmentListParamete fn try_from(value: RoleAssignmentListParameters) -> Result { let mut builder = types::RoleAssignmentListParametersBuilder::default(); + // Filter by role if let Some(val) = &value.role_id { builder.role_id(val.clone()); } + + // Filter by actor if let Some(val) = &value.user_id { - builder.actor_id(val.clone()); + builder.user_id(val.clone()); } else if let Some(val) = &value.group_id { - builder.actor_id(val.clone()); + builder.group_id(val.clone()); } + + // Filter by target if let Some(val) = &value.project_id { - builder.target_id(val.clone()); + builder.project_id(val.clone()); } else if let Some(val) = &value.domain_id { - builder.target_id(val.clone()); + builder.domain_id(val.clone()); + } + + if let Some(val) = value.effective { + builder.effective(val); } Ok(builder.build()?) } diff --git a/src/assignment/backends/sql.rs b/src/assignment/backends/sql.rs index 110f3059..93ca1f6d 100644 --- a/src/assignment/backends/sql.rs +++ b/src/assignment/backends/sql.rs @@ -65,4 +65,14 @@ impl AssignmentBackend for SqlBackend { ) -> Result, AssignmentProviderError> { Ok(assignment::list(&self.config, db, params).await?) } + + /// List role assignments for multiple actors/targets + #[tracing::instrument(level = "info", skip(self, db))] + async fn list_assignments_for_multiple_actors_and_targets( + &self, + db: &DatabaseConnection, + params: &RoleAssignmentListForMultipleActorTargetParameters, + ) -> Result, AssignmentProviderError> { + Ok(assignment::list_for_multiple_actors_and_targets(&self.config, db, params).await?) + } } diff --git a/src/assignment/backends/sql/assignment.rs b/src/assignment/backends/sql/assignment.rs index 9fdaa8f1..6293d99e 100644 --- a/src/assignment/backends/sql/assignment.rs +++ b/src/assignment/backends/sql/assignment.rs @@ -34,10 +34,14 @@ pub async fn list( if let Some(val) = ¶ms.role_id { select = select.filter(db_assignment::Column::RoleId.eq(val)); } - if let Some(val) = ¶ms.actor_id { + if let Some(val) = ¶ms.user_id { + select = select.filter(db_assignment::Column::ActorId.eq(val)); + } else if let Some(val) = ¶ms.group_id { select = select.filter(db_assignment::Column::ActorId.eq(val)); } - if let Some(val) = ¶ms.target_id { + if let Some(val) = ¶ms.project_id { + select = select.filter(db_assignment::Column::TargetId.eq(val)); + } else if let Some(val) = ¶ms.domain_id { select = select.filter(db_assignment::Column::TargetId.eq(val)); } @@ -50,6 +54,49 @@ pub async fn list( results } +/// Get all role assignments by list of actors on list of targets. +/// +/// It is a naive interpretation of the effective role assignments where we check all roles +/// assigned to the user (including groups) on a concrete target (including all higher targets the +/// role can be inherited from) +pub async fn list_for_multiple_actors_and_targets( + _conf: &Config, + db: &DatabaseConnection, + params: &RoleAssignmentListForMultipleActorTargetParameters, +) -> Result, AssignmentDatabaseError> { + let mut select = DbAssignment::find(); + + if !params.actors.is_empty() { + select = select.filter(db_assignment::Column::ActorId.is_in(params.actors.clone())); + } + if let Some(rid) = ¶ms.role_id { + select = select.filter(db_assignment::Column::RoleId.eq(rid)); + } + if !params.targets.is_empty() { + let mut cond = Condition::any(); + for target in params.targets.iter() { + cond = cond.add( + Condition::all() + .add(db_assignment::Column::TargetId.eq(&target.target_id)) + .add_option( + target + .inherited + .map(|x| db_assignment::Column::Inherited.eq(x)), + ), + ); + } + select = select.filter(cond); + } + + let db_entities: Vec = select.all(db).await?; + let results: Result, _> = db_entities + .into_iter() + .map(TryInto::::try_into) + .collect(); + + results +} + impl TryFrom for Assignment { type Error = AssignmentDatabaseError; @@ -135,7 +182,7 @@ mod tests { &db, &RoleAssignmentListParameters { role_id: Some("foo".into()), - actor_id: Some("actor".into()), + group_id: Some("actor".into()), ..Default::default() } ) @@ -148,8 +195,8 @@ mod tests { &db, &RoleAssignmentListParameters { role_id: Some("foo".into()), - actor_id: Some("actor".into()), - target_id: Some("target".into()), + user_id: Some("actor".into()), + project_id: Some("target".into()), ..Default::default() } ) @@ -184,4 +231,172 @@ mod tests { ] ); } + + #[tokio::test] + async fn test_list_for_multuple_actor_targets() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_role_assignment_mock("1".into())]]) + .append_query_results([vec![get_role_assignment_mock("1".into())]]) + .append_query_results([vec![get_role_assignment_mock("1".into())]]) + .append_query_results([vec![get_role_assignment_mock("1".into())]]) + .append_query_results([Vec::::new()]) + .into_connection(); + let config = Config::default(); + // multiple actors single simple target + assert_eq!( + list_for_multiple_actors_and_targets( + &config, + &db, + &RoleAssignmentListForMultipleActorTargetParameters { + actors: vec!["uid1".into(), "gid1".into(), "gid2".into()], + targets: vec![RoleAssignmentTarget { + target_id: "pid1".into(), + inherited: None + }], + role_id: Some("rid".into()) + } + ) + .await + .unwrap(), + vec![Assignment { + role_id: "1".into(), + actor_id: "actor".into(), + target_id: "target".into(), + r#type: AssignmentType::UserProject, + inherited: false, + }] + ); + // multiple actors multiple complex targets + assert!( + list_for_multiple_actors_and_targets( + &config, + &db, + &RoleAssignmentListForMultipleActorTargetParameters { + actors: vec!["uid1".into(), "gid1".into(), "gid2".into()], + targets: vec![ + RoleAssignmentTarget { + target_id: "pid1".into(), + inherited: None + }, + RoleAssignmentTarget { + target_id: "pid2".into(), + inherited: Some(true) + } + ], + role_id: None + } + ) + .await + .is_ok() + ); + + // empty actors and targets + assert!( + list_for_multiple_actors_and_targets( + &config, + &db, + &RoleAssignmentListForMultipleActorTargetParameters { + actors: vec![], + targets: vec![], + role_id: None + } + ) + .await + .is_ok() + ); + + // only mixed targets + assert!( + list_for_multiple_actors_and_targets( + &config, + &db, + &RoleAssignmentListForMultipleActorTargetParameters { + actors: vec![], + targets: vec![ + RoleAssignmentTarget { + target_id: "pid1".into(), + inherited: None + }, + RoleAssignmentTarget { + target_id: "pid2".into(), + inherited: Some(true) + } + ], + role_id: None + } + ) + .await + .is_ok() + ); + + // only complex targets + assert!( + list_for_multiple_actors_and_targets( + &config, + &db, + &RoleAssignmentListForMultipleActorTargetParameters { + actors: vec![], + targets: vec![ + RoleAssignmentTarget { + target_id: "pid1".into(), + inherited: Some(false) + }, + RoleAssignmentTarget { + target_id: "pid2".into(), + inherited: Some(true) + } + ], + role_id: None + } + ) + .await + .is_ok() + ); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT CAST("assignment"."type" AS text), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE "assignment"."actor_id" IN ($1, $2, $3) AND "assignment"."role_id" = $4 AND "assignment"."target_id" = $5"#, + [ + "uid1".into(), + "gid1".into(), + "gid2".into(), + "rid".into(), + "pid1".into() + ] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT CAST("assignment"."type" AS text), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE "assignment"."actor_id" IN ($1, $2, $3) AND ("assignment"."target_id" = $4 OR ("assignment"."target_id" = $5 AND "assignment"."inherited" = $6))"#, + [ + "uid1".into(), + "gid1".into(), + "gid2".into(), + "pid1".into(), + "pid2".into(), + true.into() + ] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT CAST("assignment"."type" AS text), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment""#, + [] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT CAST("assignment"."type" AS text), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE "assignment"."target_id" = $1 OR ("assignment"."target_id" = $2 AND "assignment"."inherited" = $3)"#, + ["pid1".into(), "pid2".into(), true.into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT CAST("assignment"."type" AS text), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE ("assignment"."target_id" = $1 AND "assignment"."inherited" = $2) OR ("assignment"."target_id" = $3 AND "assignment"."inherited" = $4)"#, + ["pid1".into(), false.into(), "pid2".into(), true.into()] + ), + ] + ); + } } diff --git a/src/assignment/error.rs b/src/assignment/error.rs index 87bb0be0..7d364cc8 100644 --- a/src/assignment/error.rs +++ b/src/assignment/error.rs @@ -15,7 +15,9 @@ use thiserror::Error; use crate::assignment::backends::error::*; -use crate::assignment::types::RoleBuilderError; +use crate::assignment::types::assignment::RoleAssignmentListForMultipleActorTargetParametersBuilderError; +use crate::assignment::types::*; +use crate::identity::error::IdentityProviderError; #[derive(Error, Debug)] pub enum AssignmentProviderError { @@ -40,6 +42,19 @@ pub enum AssignmentProviderError { source: AssignmentDatabaseError, }, + /// Identity provider error + #[error(transparent)] + IdentityProvider { + #[from] + source: IdentityProviderError, + }, + + #[error("building role assignment query: {}", source)] + RoleAssignmentParametersBuilder { + #[from] + source: RoleAssignmentListForMultipleActorTargetParametersBuilderError, + }, + #[error("building role data: {}", source)] RoleBuilderError { #[from] diff --git a/src/assignment/mod.rs b/src/assignment/mod.rs index f4e61b98..c5d06d07 100644 --- a/src/assignment/mod.rs +++ b/src/assignment/mod.rs @@ -24,10 +24,13 @@ pub(crate) mod types; use crate::assignment::backends::sql::SqlBackend; use crate::assignment::error::AssignmentProviderError; use crate::assignment::types::{ - Assignment, AssignmentBackend, Role, RoleAssignmentListParameters, RoleListParameters, + Assignment, AssignmentBackend, Role, RoleAssignmentListForMultipleActorTargetParametersBuilder, + RoleAssignmentListParameters, RoleAssignmentTarget, RoleListParameters, }; use crate::config::Config; +use crate::identity::IdentityApi; use crate::plugin_manager::PluginManager; +use crate::provider::Provider; #[derive(Clone, Debug)] pub struct AssignmentProvider { @@ -43,15 +46,18 @@ pub trait AssignmentApi: Send + Sync + Clone { params: &RoleListParameters, ) -> Result, AssignmentProviderError>; + /// Get a single role async fn get_role<'a>( &self, db: &DatabaseConnection, role_id: &'a str, ) -> Result, AssignmentProviderError>; + /// List role assignments for given target/role/actor async fn list_role_assignments( &self, db: &DatabaseConnection, + provider: &Provider, params: &RoleAssignmentListParameters, ) -> Result, AssignmentProviderError>; } @@ -79,6 +85,7 @@ mock! { async fn list_role_assignments( &self, db: &DatabaseConnection, + provider: &Provider, params: &RoleAssignmentListParameters, ) -> Result, AssignmentProviderError>; } @@ -135,12 +142,38 @@ impl AssignmentApi for AssignmentProvider { } /// List role assignments - #[tracing::instrument(level = "info", skip(self, db))] + #[tracing::instrument(level = "info", skip(self, db, provider))] async fn list_role_assignments( &self, db: &DatabaseConnection, + provider: &Provider, params: &RoleAssignmentListParameters, ) -> Result, AssignmentProviderError> { - self.backend_driver.list_assignments(db, params).await + let mut request = RoleAssignmentListForMultipleActorTargetParametersBuilder::default(); + let mut actors: Vec = Vec::new(); + let mut targets: Vec = Vec::new(); + if let Some(role_id) = ¶ms.role_id { + request.role_id(role_id); + } + if let Some(true) = ¶ms.effective { + if let Some(uid) = ¶ms.user_id { + let users = provider + .get_identity_provider() + .list_groups_for_user(db, uid) + .await?; + actors.extend(users.into_iter().map(|x| x.id)); + }; + } + if let Some(val) = ¶ms.project_id { + targets.push(RoleAssignmentTarget { + target_id: val.clone(), + ..Default::default() + }); + } + request.targets(targets); + request.actors(actors); + self.backend_driver + .list_assignments_for_multiple_actors_and_targets(db, &request.build()?) + .await } } diff --git a/src/assignment/types.rs b/src/assignment/types.rs index 5c80f7d4..1cce88fd 100644 --- a/src/assignment/types.rs +++ b/src/assignment/types.rs @@ -24,8 +24,10 @@ use crate::config::Config; pub use crate::assignment::types::assignment::{ Assignment, AssignmentBuilder, AssignmentBuilderError, AssignmentType, - RoleAssignmentListParameters, RoleAssignmentListParametersBuilder, - RoleAssignmentListParametersBuilderError, + RoleAssignmentListForMultipleActorTargetParameters, + RoleAssignmentListForMultipleActorTargetParametersBuilder, RoleAssignmentListParameters, + RoleAssignmentListParametersBuilder, RoleAssignmentListParametersBuilderError, + RoleAssignmentTarget, }; pub use crate::assignment::types::role::{Role, RoleBuilder, RoleBuilderError, RoleListParameters}; @@ -54,6 +56,17 @@ pub trait AssignmentBackend: DynClone + Send + Sync + std::fmt::Debug { db: &DatabaseConnection, params: &RoleAssignmentListParameters, ) -> Result, AssignmentProviderError>; + + /// List all role assignments for multiple actors on multiple targets + /// + /// It is a naive interpretation of the effective role assignments where we check all roles + /// assigned to the user (including groups) on a concrete target (including all higher targets + /// the role can be inherited from) + async fn list_assignments_for_multiple_actors_and_targets( + &self, + db: &DatabaseConnection, + params: &RoleAssignmentListForMultipleActorTargetParameters, + ) -> Result, AssignmentProviderError>; } dyn_clone::clone_trait_object!(AssignmentBackend); diff --git a/src/assignment/types/assignment.rs b/src/assignment/types/assignment.rs index 67488c82..a7914a2e 100644 --- a/src/assignment/types/assignment.rs +++ b/src/assignment/types/assignment.rs @@ -15,6 +15,7 @@ use derive_builder::Builder; use serde::{Deserialize, Serialize}; +/// Role #[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize)] #[builder(setter(strip_option, into))] pub struct Assignment { @@ -30,6 +31,7 @@ pub struct Assignment { pub inherited: bool, } +/// Role assignment type #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] pub enum AssignmentType { GroupDomain, @@ -38,15 +40,60 @@ pub enum AssignmentType { UserProject, } +/// Parameters for listing role assignments for role/target/actor #[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[builder(setter(strip_option, into))] pub struct RoleAssignmentListParameters { + /// Query role assignments filtering results by the role #[builder(default)] pub role_id: Option, + + /// Get role assignments for the user + #[builder(default)] + pub user_id: Option, + /// Get role assignments for the group + #[builder(default)] + pub group_id: Option, + + /// Query role assignments on the project + #[builder(default)] + pub project_id: Option, + /// Query role assignments on the domain #[builder(default)] - pub actor_id: Option, + pub domain_id: Option, + /// Query role assignments on the system #[builder(default)] - pub target_id: Option, + pub system: Option, + + // #[builder(default)] + // pub inherited: Option, + /// Query the effective assignments, including any assignments gained by virtue of group + /// membership. #[builder(default)] - pub r#type: Option, + pub effective: Option, +} + +/// Querying effective role assignments for list of actors (typically user with all groups user is +/// member of) on list of targets (exactl project + inherited from uppoer projects/domain) +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[builder(setter(strip_option, into))] +pub struct RoleAssignmentListForMultipleActorTargetParameters { + /// List of actors for which assignments are looked up + #[builder(default)] + pub actors: Vec, + + /// Optionally filter for the concrete role ID + #[builder(default)] + pub role_id: Option, + + /// List of targets for which assignments are looked up + #[builder(default)] + pub targets: Vec, +} + +/// Role assignment target which is either target_id or target_id with explicit inherited parameter +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct RoleAssignmentTarget { + pub target_id: String, + pub inherited: Option, } diff --git a/src/identity/backends/sql/group.rs b/src/identity/backends/sql/group.rs index 4d2f01bc..09ff4b82 100644 --- a/src/identity/backends/sql/group.rs +++ b/src/identity/backends/sql/group.rs @@ -115,8 +115,7 @@ pub async fn list_for_user( let results: Vec = groups .into_iter() - .map(|(_, x)| x.into_iter()) - .flatten() + .flat_map(|(_, x)| x.into_iter()) .map(Into::into) .collect(); Ok(results)