diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba616ac4..cd02f984 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,9 @@ jobs: - name: Run tests run: cargo nextest run + - name: Run tests + run: cargo nextest run --test integration + - name: Run Doc tests run: cargo test --doc diff --git a/.github/workflows/functional.yml b/.github/workflows/functional.yml index 4703d294..e1e9bc73 100644 --- a/.github/workflows/functional.yml +++ b/.github/workflows/functional.yml @@ -104,8 +104,8 @@ jobs: curl http://localhost:8080/v3/auth/tokens -H "X-Auth-Token: ${TOKEN2}" -H "X-Subject-Token: ${TOKEN2}" | jq curl http://localhost:5001/v3/auth/tokens -H "X-Auth-Token: ${TOKEN2}" -H "X-Subject-Token: ${TOKEN2}" | jq - - name: Run functional tests - run: cargo test --test functional + - name: Run api tests + run: cargo test --test api - name: Run interop tests run: cargo test --test interop diff --git a/Cargo.lock b/Cargo.lock index 0819afb5..f99fb16d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2398,6 +2398,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ + "cc", "pkg-config", "vcpkg", ] diff --git a/Cargo.toml b/Cargo.toml index b54e58bc..ed31876b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ hyper-util = { version = "0.1", features = ["tokio", "http1"] } keycloak = { version = "26.4" } mockall = { version = "0.14" } reqwest = { version = "0.12", features = ["json", "multipart"] } -sea-orm = { version = "1.1", features = ["mock"]} +sea-orm = { version = "1.1", features = ["mock", "sqlx-sqlite" ]} serde_urlencoded = { version = "0.7" } tempfile = { version = "3.23" } thirtyfour = "0.36" @@ -111,10 +111,14 @@ name = "github" path = "tests/github/main.rs" test = false +[[test]] +name = "integration" +path = "tests/integration/main.rs" +test = false [[test]] -name = "functional" -path = "tests/functional/main.rs" +name = "api" +path = "tests/api/main.rs" test = false [lints.rust] diff --git a/mp:w b/mp:w deleted file mode 100644 index 23514306..00000000 --- a/mp:w +++ /dev/null @@ -1,62 +0,0 @@ -// 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 - -pub mod assignment; -pub mod role; - -use async_trait::async_trait; - -use crate::assignment::AssignmentProviderError; -use crate::keystone::ServiceState; - -pub use crate::assignment::types::assignment::{ - Assignment, AssignmentBuilder, AssignmentBuilderError, AssignmentType, - RoleAssignmentListForMultipleActorTargetParameters, -, - RoleAssignmentListForMultipleActorTargetParametersBuilder, RoleAssignmentListParameters, - RoleAssignmentListParametersBuilder, RoleAssignmentListParametersBuilderError, - RoleAssignmentTarget, -}; -pub use crate::assignment::types::role::{Role, RoleBuilder, RoleBuilderError, RoleListParameters}; - -#[async_trait] -pub trait AssignmentApi: Send + Sync + Clone { - /// List Roles. - async fn list_roles( - &self, - state: &ServiceState, - params: &RoleListParameters, - ) -> Result, AssignmentProviderError>; - - /// Get a single role. - async fn get_role<'a>( - &self, - state: &ServiceState, - role_id: &'a str, - ) -> Result, AssignmentProviderError>; - - /// List role assignments for given target/role/actor. - async fn list_role_assignments( - &self, - state: &ServiceState, - params: &RoleAssignmentListParameters, - ) -> Result, AssignmentProviderError>; - - /// Create assignment grant. - async fn create_grant( - &self, - state: &ServiceState, - params: &Assignment, - ) -> Result<(), AssignmentProviderError>; -} diff --git a/src/assignment/backend/sql/assignment/list.rs b/src/assignment/backend/sql/assignment/list.rs index 87fb1676..e2e39cbc 100644 --- a/src/assignment/backend/sql/assignment/list.rs +++ b/src/assignment/backend/sql/assignment/list.rs @@ -58,17 +58,20 @@ pub async fn list( .filter(db_assignment::Column::Type.is_in([ DbAssignmentType::UserProject, DbAssignmentType::GroupProject, - ])); + ])) + .filter(db_assignment::Column::Inherited.eq(false)); } else if let Some(val) = ¶ms.domain_id { select_assignment = select_assignment .filter(db_assignment::Column::TargetId.eq(val)) .filter( db_assignment::Column::Type .is_in([DbAssignmentType::UserDomain, DbAssignmentType::GroupDomain]), - ); + ) + .filter(db_assignment::Column::Inherited.eq(false)); } else { - select_system_assignment = - select_system_assignment.filter(db_system_assignment::Column::TargetId.eq("system")); + select_system_assignment = select_system_assignment + .filter(db_system_assignment::Column::TargetId.eq("system")) + .filter(db_system_assignment::Column::Inherited.eq(false)); } let results: Result, _> = if let Some(true) = ¶ms.include_names { @@ -254,8 +257,8 @@ mod tests { ), Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT "system_assignment"."type", "system_assignment"."actor_id", "system_assignment"."target_id", "system_assignment"."role_id", "system_assignment"."inherited" FROM "system_assignment" WHERE "system_assignment"."target_id" = $1"#, - ["system".into()] + r#"SELECT "system_assignment"."type", "system_assignment"."actor_id", "system_assignment"."target_id", "system_assignment"."role_id", "system_assignment"."inherited" FROM "system_assignment" WHERE "system_assignment"."target_id" = $1 AND "system_assignment"."inherited" = $2"#, + ["system".into(), false.into()] ), ] ); @@ -310,8 +313,8 @@ mod tests { ), Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT "system_assignment"."type", "system_assignment"."actor_id", "system_assignment"."target_id", "system_assignment"."role_id", "system_assignment"."inherited" FROM "system_assignment" WHERE "system_assignment"."role_id" = $1 AND "system_assignment"."target_id" = $2"#, - ["1".into(), "system".into()] + r#"SELECT "system_assignment"."type", "system_assignment"."actor_id", "system_assignment"."target_id", "system_assignment"."role_id", "system_assignment"."inherited" FROM "system_assignment" WHERE "system_assignment"."role_id" = $1 AND "system_assignment"."target_id" = $2 AND "system_assignment"."inherited" = $3"#, + ["1".into(), "system".into(), false.into()] ), ] ); @@ -349,8 +352,13 @@ mod tests { 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"."target_id" = $1 AND "assignment"."type" IN (CAST($2 AS "type"), CAST($3 AS "type"))"#, - ["target".into(), "UserProject".into(), "GroupProject".into()] + 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"."type" IN (CAST($2 AS "type"), CAST($3 AS "type")) AND "assignment"."inherited" = $4"#, + [ + "target".into(), + "UserProject".into(), + "GroupProject".into(), + false.into() + ] ),] ); } @@ -404,8 +412,8 @@ mod tests { ), Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT "system_assignment"."type" AS "A_type", "system_assignment"."actor_id" AS "A_actor_id", "system_assignment"."target_id" AS "A_target_id", "system_assignment"."role_id" AS "A_role_id", "system_assignment"."inherited" AS "A_inherited", "role"."id" AS "B_id", "role"."name" AS "B_name", "role"."extra" AS "B_extra", "role"."domain_id" AS "B_domain_id", "role"."description" AS "B_description" FROM "system_assignment" LEFT JOIN "role" ON "system_assignment"."role_id" = "role"."id" WHERE "system_assignment"."target_id" = $1"#, - ["system".into()] + r#"SELECT "system_assignment"."type" AS "A_type", "system_assignment"."actor_id" AS "A_actor_id", "system_assignment"."target_id" AS "A_target_id", "system_assignment"."role_id" AS "A_role_id", "system_assignment"."inherited" AS "A_inherited", "role"."id" AS "B_id", "role"."name" AS "B_name", "role"."extra" AS "B_extra", "role"."domain_id" AS "B_domain_id", "role"."description" AS "B_description" FROM "system_assignment" LEFT JOIN "role" ON "system_assignment"."role_id" = "role"."id" WHERE "system_assignment"."target_id" = $1 AND "system_assignment"."inherited" = $2"#, + ["system".into(), false.into()] ), ] ); diff --git a/src/assignment/error.rs b/src/assignment/error.rs index 7f1ef4b9..dbc2cd07 100644 --- a/src/assignment/error.rs +++ b/src/assignment/error.rs @@ -18,6 +18,7 @@ use crate::assignment::backend::error::*; use crate::assignment::types::assignment::RoleAssignmentListForMultipleActorTargetParametersBuilderError; use crate::assignment::types::*; use crate::identity::error::IdentityProviderError; +use crate::resource::error::ResourceProviderError; #[derive(Error, Debug)] pub enum AssignmentProviderError { @@ -43,13 +44,20 @@ pub enum AssignmentProviderError { #[error(transparent)] AssignmentDatabaseError { source: AssignmentDatabaseError }, - /// Identity provider error + /// Identity provider error. #[error(transparent)] IdentityProvider { #[from] source: IdentityProviderError, }, + /// Resource provider error. + #[error(transparent)] + ResourceProvider { + #[from] + source: ResourceProviderError, + }, + /// Invalid assignment type. #[error("{0}")] InvalidAssignmentType(String), diff --git a/src/assignment/mod.rs b/src/assignment/mod.rs index 86167c27..caabdcb1 100644 --- a/src/assignment/mod.rs +++ b/src/assignment/mod.rs @@ -29,6 +29,7 @@ use crate::config::Config; use crate::identity::IdentityApi; use crate::keystone::ServiceState; use crate::plugin_manager::PluginManager; +use crate::resource::ResourceApi; #[cfg(test)] pub use mock::MockAssignmentProvider; @@ -51,9 +52,9 @@ impl AssignmentProvider { } else { match config.assignment.driver.as_str() { "sql" => Box::new(SqlBackend::default()), - _ => { + other => { return Err(AssignmentProviderError::UnsupportedDriver( - config.assignment.driver.clone(), + other.to_string(), )); } } @@ -92,40 +93,54 @@ impl AssignmentApi for AssignmentProvider { state: &ServiceState, params: &RoleAssignmentListParameters, ) -> Result, AssignmentProviderError> { - if let Some(true) = ¶ms.effective { - 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(uid) = ¶ms.user_id { - actors.push(uid.into()); - } - if let Some(true) = ¶ms.effective - && let Some(uid) = ¶ms.user_id + 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(uid) = ¶ms.user_id { + actors.push(uid.into()); + } + if let Some(true) = ¶ms.effective + && let Some(uid) = ¶ms.user_id + { + let users = state + .provider + .get_identity_provider() + .list_groups_of_user(state, 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(), + inherited: Some(false), + }); + if let Some(parents) = state + .provider + .get_resource_provider() + .get_project_parents(state, val) + .await? { - let users = state - .provider - .get_identity_provider() - .list_groups_of_user(state, 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() + parents.iter().for_each(|parent_project| { + targets.push(RoleAssignmentTarget { + target_id: parent_project.id.clone(), + inherited: Some(true), + }); }); } - request.targets(targets); - request.actors(actors); - self.backend_driver - .list_assignments_for_multiple_actors_and_targets(state, &request.build()?) - .await - } else { - self.backend_driver.list_assignments(state, params).await + } else if let Some(val) = ¶ms.domain_id { + targets.push(RoleAssignmentTarget { + target_id: val.clone(), + inherited: Some(false), + }); } + request.targets(targets); + request.actors(actors); + self.backend_driver + .list_assignments_for_multiple_actors_and_targets(state, &request.build()?) + .await } /// Create assignment grant. diff --git a/src/assignment/types/assignment.rs b/src/assignment/types/assignment.rs index 4c877c5f..5bf05e1d 100644 --- a/src/assignment/types/assignment.rs +++ b/src/assignment/types/assignment.rs @@ -29,9 +29,9 @@ pub struct Assignment { pub actor_id: String, /// The target id. pub target_id: String, - /// The assignment type + /// The assignment type. pub r#type: AssignmentType, - /// Inherited flag + /// Inherited flag. pub inherited: bool, } diff --git a/src/config.rs b/src/config.rs index 3de61b4b..fb6dc7dd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -135,24 +135,51 @@ pub struct PolicySection { pub opa_base_url: Option, } -#[derive(Debug, Default, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone)] pub struct AssignmentSection { + #[serde(default = "default_sql_driver")] pub driver: String, } -#[derive(Debug, Default, Deserialize, Clone)] +impl Default for AssignmentSection { + fn default() -> Self { + Self { + driver: default_sql_driver(), + } + } +} + +#[derive(Debug, Deserialize, Clone)] pub struct CatalogSection { + #[serde(default = "default_sql_driver")] pub driver: String, } -#[derive(Debug, Default, Deserialize, Clone)] +impl Default for CatalogSection { + fn default() -> Self { + Self { + driver: default_sql_driver(), + } + } +} + +#[derive(Debug, Deserialize, Clone)] pub struct FederationSection { + #[serde(default = "default_sql_driver")] pub driver: String, } -#[derive(Debug, Default, Deserialize, Clone)] +impl Default for FederationSection { + fn default() -> Self { + Self { + driver: default_sql_driver(), + } + } +} + +#[derive(Debug, Deserialize, Clone)] pub struct IdentitySection { - #[serde(default = "default_identity_driver")] + #[serde(default = "default_sql_driver")] pub driver: String, #[serde(default)] @@ -161,22 +188,52 @@ pub struct IdentitySection { pub password_hash_rounds: Option, } -#[derive(Debug, Default, Deserialize, Clone)] +impl Default for IdentitySection { + fn default() -> Self { + Self { + driver: default_sql_driver(), + password_hashing_algorithm: PasswordHashingAlgo::Bcrypt, + max_password_length: 4096, + password_hash_rounds: None, + } + } +} + +#[derive(Debug, Deserialize, Clone)] pub struct ResourceSection { + #[serde(default = "default_sql_driver")] pub driver: String, } +impl Default for ResourceSection { + fn default() -> Self { + Self { + driver: default_sql_driver(), + } + } +} + /// Revoke provider configuration. -#[derive(Debug, Default, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone)] pub struct RevokeSection { /// Entry point for the token revocation backend driver in the `keystone.revoke` namespace. /// Keystone only provides a `sql` driver. + #[serde(default = "default_sql_driver")] pub driver: String, /// The number of seconds after a token has expired before a corresponding revocation event may /// be purged from the backend. pub expiration_buffer: usize, } +impl Default for RevokeSection { + fn default() -> Self { + Self { + driver: default_sql_driver(), + expiration_buffer: 1800, + } + } +} + #[derive(Debug, Default, Deserialize, Clone)] pub enum PasswordHashingAlgo { #[default] @@ -189,7 +246,7 @@ pub struct SecurityComplianceSection { pub disable_user_account_days_inactive: Option, } -fn default_identity_driver() -> String { +fn default_sql_driver() -> String { "sql".into() } @@ -250,11 +307,6 @@ impl TryFrom> for Config { .set_default("identity.max_password_length", "4096")? .set_default("fernet_tokens.key_repository", "/etc/keystone/fernet-keys/")? .set_default("fernet_tokens.max_active_keys", "3")? - .set_default("assignment.driver", "sql")? - .set_default("catalog.driver", "sql")? - .set_default("federation.driver", "sql")? - .set_default("resource.driver", "sql")? - .set_default("revoke.driver", "sql")? .set_default("revoke.expiration_buffer", "1800")? .set_default("token.expiration", "3600")?; diff --git a/tests/functional/auth.rs b/tests/api/auth.rs similarity index 100% rename from tests/functional/auth.rs rename to tests/api/auth.rs diff --git a/tests/functional/auth/token.rs b/tests/api/auth/token.rs similarity index 100% rename from tests/functional/auth/token.rs rename to tests/api/auth/token.rs diff --git a/tests/functional/auth/token/revoke.rs b/tests/api/auth/token/revoke.rs similarity index 100% rename from tests/functional/auth/token/revoke.rs rename to tests/api/auth/token/revoke.rs diff --git a/tests/functional/auth/token/validate.rs b/tests/api/auth/token/validate.rs similarity index 100% rename from tests/functional/auth/token/validate.rs rename to tests/api/auth/token/validate.rs diff --git a/tests/functional/common.rs b/tests/api/common.rs similarity index 100% rename from tests/functional/common.rs rename to tests/api/common.rs diff --git a/tests/functional/main.rs b/tests/api/main.rs similarity index 100% rename from tests/functional/main.rs rename to tests/api/main.rs diff --git a/tests/integration/assignment.rs b/tests/integration/assignment.rs new file mode 100644 index 00000000..88486981 --- /dev/null +++ b/tests/integration/assignment.rs @@ -0,0 +1,15 @@ +// 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 + +mod grant; diff --git a/tests/integration/assignment/grant.rs b/tests/integration/assignment/grant.rs new file mode 100644 index 00000000..156fb762 --- /dev/null +++ b/tests/integration/assignment/grant.rs @@ -0,0 +1,46 @@ +// 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 + +use eyre::Report; +use sea_orm::{DatabaseBackend, DbConn, query::*, schema::Schema, sea_query::*}; + +use openstack_keystone::db::entity::prelude::*; + +mod list; + +async fn setup_schema(db: &DbConn) -> Result<(), Report> { + // TODO: with sea-orm 2.0 it can be improved + //db.get_schema_registry("crate::db::entity::*").sync(db).await?; + // Setup Schema helper + let schema = Schema::new(DatabaseBackend::Sqlite); + + // Derive from Entity + let stmts: Vec = vec![ + schema.create_table_from_entity(Assignment), + schema.create_table_from_entity(Group), + schema.create_table_from_entity(ImpliedRole), + schema.create_table_from_entity(Project), + schema.create_table_from_entity(Role), + schema.create_table_from_entity(SystemAssignment), + schema.create_table_from_entity(User), + schema.create_table_from_entity(UserGroupMembership), + ]; + + // Execute create table statement + for stmt in stmts.iter() { + db.execute(db.get_database_backend().build(stmt)).await?; + } + + Ok(()) +} diff --git a/tests/integration/assignment/grant/list.rs b/tests/integration/assignment/grant/list.rs new file mode 100644 index 00000000..77c45c87 --- /dev/null +++ b/tests/integration/assignment/grant/list.rs @@ -0,0 +1,417 @@ +// 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 +//! Test role assignments. + +use eyre::Report; +use sea_orm::{Database, DbConn, entity::*}; +use std::collections::BTreeSet; +use std::sync::Arc; + +use openstack_keystone::assignment::AssignmentApi; +use openstack_keystone::assignment::types::{ + RoleAssignmentListParameters, RoleAssignmentListParametersBuilder, +}; +use openstack_keystone::config::Config; +use openstack_keystone::db::entity::{ + assignment, group, project, role, sea_orm_active_enums::Type, user, user_group_membership, +}; +use openstack_keystone::keystone::{Service, ServiceState}; +use openstack_keystone::plugin_manager::PluginManager; +use openstack_keystone::policy::PolicyFactory; +use openstack_keystone::provider::Provider; + +use super::setup_schema; + +async fn setup_assignment_data(db: &DbConn) -> Result<(), Report> { + // Domain/project data + let root_domain = project::ActiveModel { + is_domain: Set(true), + id: Set("<>".into()), + name: Set("<>".into()), + extra: NotSet, + description: NotSet, + enabled: Set(Some(true)), + domain_id: Set("<>".into()), + parent_id: NotSet, + } + .insert(db) + .await?; + let domain_a = project::ActiveModel { + is_domain: Set(true), + id: Set("domain_a".into()), + name: Set("domain_a".into()), + extra: NotSet, + description: NotSet, + enabled: Set(Some(true)), + domain_id: Set(root_domain.id.clone()), + parent_id: NotSet, + } + .insert(db) + .await?; + let project_a = project::ActiveModel { + is_domain: Set(false), + id: Set("project_a".into()), + name: Set("project_a".into()), + extra: NotSet, + description: NotSet, + enabled: Set(Some(true)), + domain_id: Set(domain_a.id.clone()), + parent_id: Set(Some(domain_a.id.clone())), + } + .insert(db) + .await?; + let _project_a_1 = project::ActiveModel { + is_domain: Set(false), + id: Set("project_a_1".into()), + name: Set("project_a_1".into()), + extra: NotSet, + description: NotSet, + enabled: Set(Some(true)), + domain_id: Set(domain_a.id.clone()), + parent_id: Set(Some(project_a.id.clone())), + } + .insert(db) + .await?; + + // Roles + let role_a = role::ActiveModel { + id: Set("role_a".into()), + name: Set("role_a".into()), + extra: NotSet, + description: NotSet, + domain_id: Set(domain_a.id.clone()), + } + .insert(db) + .await?; + let role_ga = role::ActiveModel { + id: Set("role_ga".into()), + name: Set("role_ga".into()), + extra: NotSet, + description: NotSet, + domain_id: Set(domain_a.id.clone()), + } + .insert(db) + .await?; + let role_b = role::ActiveModel { + id: Set("role_b".into()), + name: Set("role_b".into()), + extra: NotSet, + description: NotSet, + domain_id: Set(domain_a.id.clone()), + } + .insert(db) + .await?; + let role_gb = role::ActiveModel { + id: Set("role_gb".into()), + name: Set("role_gb".into()), + extra: NotSet, + description: NotSet, + domain_id: Set(domain_a.id.clone()), + } + .insert(db) + .await?; + let role_c = role::ActiveModel { + id: Set("role_c".into()), + name: Set("role_c".into()), + extra: NotSet, + description: NotSet, + domain_id: Set(domain_a.id.clone()), + } + .insert(db) + .await?; + let role_gc = role::ActiveModel { + id: Set("role_gc".into()), + name: Set("role_gc".into()), + extra: NotSet, + description: NotSet, + domain_id: Set(domain_a.id.clone()), + } + .insert(db) + .await?; + let role_d = role::ActiveModel { + id: Set("role_d".into()), + name: Set("role_d".into()), + extra: NotSet, + description: NotSet, + domain_id: Set(domain_a.id.clone()), + } + .insert(db) + .await?; + let role_gd = role::ActiveModel { + id: Set("role_gd".into()), + name: Set("role_gd".into()), + extra: NotSet, + description: NotSet, + domain_id: Set(domain_a.id.clone()), + } + .insert(db) + .await?; + + // Group + let group_a = group::ActiveModel { + id: Set("group_a".into()), + name: Set("group_a".into()), + domain_id: Set(domain_a.id.clone()), + extra: NotSet, + description: NotSet, + } + .insert(db) + .await?; + // User + let user_a = user::ActiveModel { + id: Set("user_a".into()), + extra: NotSet, + enabled: Set(Some(true)), + default_project_id: NotSet, + last_active_at: NotSet, + created_at: NotSet, + domain_id: Set(domain_a.id.clone()), + } + .insert(db) + .await?; + user_group_membership::ActiveModel { + user_id: Set(user_a.id.clone()), + group_id: Set(group_a.id.clone()), + } + .insert(db) + .await?; + + // Assignments + assignment::ActiveModel { + r#type: Set(Type::UserDomain), + actor_id: Set(user_a.id.clone()), + target_id: Set(domain_a.id.clone()), + role_id: Set(role_a.id.clone()), + inherited: Set(false), + } + .insert(db) + .await?; + assignment::ActiveModel { + r#type: Set(Type::GroupDomain), + actor_id: Set(group_a.id.clone()), + target_id: Set(domain_a.id.clone()), + role_id: Set(role_ga.id.clone()), + inherited: Set(false), + } + .insert(db) + .await?; + assignment::ActiveModel { + r#type: Set(Type::UserDomain), + actor_id: Set(user_a.id.clone()), + target_id: Set(domain_a.id.clone()), + role_id: Set(role_b.id.clone()), + inherited: Set(true), + } + .insert(db) + .await?; + assignment::ActiveModel { + r#type: Set(Type::GroupDomain), + actor_id: Set(group_a.id.clone()), + target_id: Set(domain_a.id.clone()), + role_id: Set(role_gb.id.clone()), + inherited: Set(true), + } + .insert(db) + .await?; + assignment::ActiveModel { + r#type: Set(Type::UserProject), + actor_id: Set(user_a.id.clone()), + target_id: Set(project_a.id.clone()), + role_id: Set(role_c.id.clone()), + inherited: Set(false), + } + .insert(db) + .await?; + assignment::ActiveModel { + r#type: Set(Type::GroupProject), + actor_id: Set(group_a.id.clone()), + target_id: Set(project_a.id.clone()), + role_id: Set(role_gc.id.clone()), + inherited: Set(false), + } + .insert(db) + .await?; + assignment::ActiveModel { + r#type: Set(Type::UserProject), + actor_id: Set(user_a.id.clone()), + target_id: Set(project_a.id.clone()), + role_id: Set(role_d.id.clone()), + inherited: Set(true), + } + .insert(db) + .await?; + assignment::ActiveModel { + r#type: Set(Type::GroupProject), + actor_id: Set(group_a.id.clone()), + target_id: Set(project_a.id.clone()), + role_id: Set(role_gd.id.clone()), + inherited: Set(true), + } + .insert(db) + .await?; + + Ok(()) +} + +async fn get_state() -> Result, Report> { + let db = Database::connect("sqlite::memory:").await?; + setup_schema(&db).await?; + setup_assignment_data(&db).await?; + + let cfg: Config = Config::default(); + + let plugin_manager = PluginManager::default(); + let provider = Provider::new(cfg.clone(), plugin_manager)?; + Ok(Arc::new(Service::new( + cfg, + db, + provider, + PolicyFactory::default(), + )?)) +} + +async fn list_grants( + state: &ServiceState, + params: &RoleAssignmentListParameters, +) -> Result, Report> { + Ok(state + .provider + .get_assignment_provider() + .list_role_assignments(state, params) + .await? + .into_iter() + .map(|grant| grant.role_id) + .collect()) +} + +#[tokio::test] +async fn test_list_user_domain() -> Result<(), Report> { + let state = get_state().await?; + + assert_eq!( + list_grants( + &state, + &RoleAssignmentListParametersBuilder::default() + .user_id("user_a") + .domain_id("domain_a") + .build()?, + ) + .await?, + BTreeSet::from(["role_a".into()]), + "user has only role_a on the domain" + ); + assert_eq!( + list_grants( + &state, + &RoleAssignmentListParametersBuilder::default() + .user_id("user_a") + .domain_id("domain_a") + .effective(true) + .build()?, + ) + .await?, + BTreeSet::from(["role_a".into(), "role_ga".into()]), + "user has role_a, role_ga on the domain" + ); + assert_eq!( + list_grants( + &state, + &RoleAssignmentListParametersBuilder::default() + .group_id("group_a") + .domain_id("domain_a") + .effective(true) + .build()?, + ) + .await?, + BTreeSet::from(["role_a".into(), "role_ga".into()]), + "group has role_ga on the domain" + ); + Ok(()) +} + +#[tokio::test] +async fn test_list_user_tl_project() -> Result<(), Report> { + let state = get_state().await?; + + assert_eq!( + list_grants( + &state, + &RoleAssignmentListParametersBuilder::default() + .user_id("user_a") + .project_id("project_a") + .effective(false) + .build()?, + ) + .await?, + BTreeSet::from(["role_b".into(), "role_c".into()]), + "user has role_b inherited from the domain and direct role_c on the TL project (direct)" + ); + assert_eq!( + list_grants( + &state, + &RoleAssignmentListParametersBuilder::default() + .user_id("user_a") + .project_id("project_a") + .effective(true) + .build()?, + ) + .await?, + BTreeSet::from([ + "role_b".into(), + "role_c".into(), + "role_gb".into(), + "role_gc".into() + ]), + "user has role_b inherited from the domain on the TL project (effective)" + ); + Ok(()) +} + +#[tokio::test] +async fn test_list_user_sub_project() -> Result<(), Report> { + let state = get_state().await?; + + assert_eq!( + list_grants( + &state, + &RoleAssignmentListParametersBuilder::default() + .user_id("user_a") + .project_id("project_a_1") + .effective(false) + .build()?, + ) + .await?, + BTreeSet::from(["role_b".into(), "role_d".into()]), + "user has only inherited roles on the subproject (effective)" + ); + assert_eq!( + list_grants( + &state, + &RoleAssignmentListParametersBuilder::default() + .user_id("user_a") + .project_id("project_a_1") + .effective(true) + .build()?, + ) + .await?, + BTreeSet::from([ + "role_b".into(), + "role_d".into(), + "role_gb".into(), + "role_gd".into() + ]), + "user has only inherited roles and groups expanded on the subproject (effective)" + ); + Ok(()) +} diff --git a/tests/integration/main.rs b/tests/integration/main.rs new file mode 100644 index 00000000..b2a3af1e --- /dev/null +++ b/tests/integration/main.rs @@ -0,0 +1,15 @@ +// 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 + +mod assignment;