From 79cba0bfe247bc1b57bcbefc66187f2b527802f3 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 24 Nov 2025 17:58:30 +0000 Subject: [PATCH] feat: Add `get_project_parents` method For identifying all assignments user has on the project it is necessary to also identify all project parents (inherited assignments). --- src/resource/{backends.rs => backend.rs} | 0 src/resource/{backends => backend}/error.rs | 0 src/resource/{backends => backend}/sql.rs | 19 ++- .../{backends => backend}/sql/domain/get.rs | 2 +- .../{backends => backend}/sql/domain/mod.rs | 2 +- .../{backends => backend}/sql/project/get.rs | 2 +- .../{backends => backend}/sql/project/mod.rs | 14 +- src/resource/backend/sql/project/tree.rs | 140 ++++++++++++++++++ src/resource/error.rs | 2 +- src/resource/mod.rs | 27 +++- src/resource/types.rs | 7 + src/resource/types/project.rs | 8 +- 12 files changed, 207 insertions(+), 16 deletions(-) rename src/resource/{backends.rs => backend.rs} (100%) rename src/resource/{backends => backend}/error.rs (100%) rename src/resource/{backends => backend}/sql.rs (91%) rename src/resource/{backends => backend}/sql/domain/get.rs (96%) rename src/resource/{backends => backend}/sql/domain/mod.rs (96%) rename src/resource/{backends => backend}/sql/project/get.rs (96%) rename src/resource/{backends => backend}/sql/project/mod.rs (78%) create mode 100644 src/resource/backend/sql/project/tree.rs diff --git a/src/resource/backends.rs b/src/resource/backend.rs similarity index 100% rename from src/resource/backends.rs rename to src/resource/backend.rs diff --git a/src/resource/backends/error.rs b/src/resource/backend/error.rs similarity index 100% rename from src/resource/backends/error.rs rename to src/resource/backend/error.rs diff --git a/src/resource/backends/sql.rs b/src/resource/backend/sql.rs similarity index 91% rename from src/resource/backends/sql.rs rename to src/resource/backend/sql.rs index 97f65d32..f2f66b37 100644 --- a/src/resource/backends/sql.rs +++ b/src/resource/backend/sql.rs @@ -21,8 +21,6 @@ use super::super::types::*; use crate::config::Config; use crate::keystone::ServiceState; use crate::resource::ResourceProviderError; -use crate::resource::backends::sql::domain::{get_domain_by_id, get_domain_by_name}; -use crate::resource::backends::sql::project::{get_project, get_project_by_name}; #[derive(Clone, Debug, Default)] pub struct SqlBackend { @@ -44,7 +42,7 @@ impl ResourceBackend for SqlBackend { state: &ServiceState, domain_id: &'a str, ) -> Result, ResourceProviderError> { - Ok(get_domain_by_id(&self.config, &state.db, domain_id).await?) + Ok(domain::get_domain_by_id(&self.config, &state.db, domain_id).await?) } /// Get single domain by Name @@ -53,7 +51,7 @@ impl ResourceBackend for SqlBackend { state: &ServiceState, domain_name: &'a str, ) -> Result, ResourceProviderError> { - Ok(get_domain_by_name(&self.config, &state.db, domain_name).await?) + Ok(domain::get_domain_by_name(&self.config, &state.db, domain_name).await?) } /// Get single project by ID @@ -62,7 +60,7 @@ impl ResourceBackend for SqlBackend { state: &ServiceState, project_id: &'a str, ) -> Result, ResourceProviderError> { - Ok(get_project(&self.config, &state.db, project_id).await?) + Ok(project::get_project(&self.config, &state.db, project_id).await?) } /// Get single project by Name and Domain ID @@ -72,7 +70,16 @@ impl ResourceBackend for SqlBackend { name: &'a str, domain_id: &'a str, ) -> Result, ResourceProviderError> { - Ok(get_project_by_name(&self.config, &state.db, name, domain_id).await?) + Ok(project::get_project_by_name(&self.config, &state.db, name, domain_id).await?) + } + + /// Get project parents + async fn get_project_parents<'a>( + &self, + state: &ServiceState, + project_id: &'a str, + ) -> Result>, ResourceProviderError> { + Ok(project::get_project_parents(&state.db, project_id).await?) } } diff --git a/src/resource/backends/sql/domain/get.rs b/src/resource/backend/sql/domain/get.rs similarity index 96% rename from src/resource/backends/sql/domain/get.rs rename to src/resource/backend/sql/domain/get.rs index 4395a001..65e81198 100644 --- a/src/resource/backends/sql/domain/get.rs +++ b/src/resource/backend/sql/domain/get.rs @@ -18,7 +18,7 @@ use sea_orm::query::*; use crate::db::entity::{prelude::Project as DbProject, project as db_project}; use crate::resource::Config; -use crate::resource::backends::error::{ResourceDatabaseError, db_err}; +use crate::resource::backend::error::{ResourceDatabaseError, db_err}; use crate::resource::types::Domain; pub async fn get_domain_by_id>( diff --git a/src/resource/backends/sql/domain/mod.rs b/src/resource/backend/sql/domain/mod.rs similarity index 96% rename from src/resource/backends/sql/domain/mod.rs rename to src/resource/backend/sql/domain/mod.rs index eda65a92..f62140a2 100644 --- a/src/resource/backends/sql/domain/mod.rs +++ b/src/resource/backend/sql/domain/mod.rs @@ -20,7 +20,7 @@ pub use get::get_domain_by_id; pub use get::get_domain_by_name; use crate::db::entity::project as db_project; -use crate::resource::backends::error::ResourceDatabaseError; +use crate::resource::backend::error::ResourceDatabaseError; use crate::resource::types::Domain; use crate::resource::types::DomainBuilder; diff --git a/src/resource/backends/sql/project/get.rs b/src/resource/backend/sql/project/get.rs similarity index 96% rename from src/resource/backends/sql/project/get.rs rename to src/resource/backend/sql/project/get.rs index d1bcbf37..10c8cc86 100644 --- a/src/resource/backends/sql/project/get.rs +++ b/src/resource/backend/sql/project/get.rs @@ -18,7 +18,7 @@ use sea_orm::query::*; use crate::db::entity::{prelude::Project as DbProject, project as db_project}; use crate::resource::Config; -use crate::resource::backends::error::{ResourceDatabaseError, db_err}; +use crate::resource::backend::error::{ResourceDatabaseError, db_err}; use crate::resource::types::Project; pub async fn get_project>( diff --git a/src/resource/backends/sql/project/mod.rs b/src/resource/backend/sql/project/mod.rs similarity index 78% rename from src/resource/backends/sql/project/mod.rs rename to src/resource/backend/sql/project/mod.rs index 8c7f6b3c..673ab98f 100644 --- a/src/resource/backends/sql/project/mod.rs +++ b/src/resource/backend/sql/project/mod.rs @@ -15,12 +15,14 @@ use serde_json::Value; use tracing::error; mod get; +mod tree; pub use get::get_project; pub use get::get_project_by_name; +pub use tree::get_project_parents; use crate::db::entity::project as db_project; -use crate::resource::backends::error::ResourceDatabaseError; +use crate::resource::backend::error::ResourceDatabaseError; use crate::resource::types::Project; use crate::resource::types::ProjectBuilder; @@ -30,6 +32,9 @@ impl TryFrom for Project { fn try_from(value: db_project::Model) -> Result { let mut project_builder = ProjectBuilder::default(); project_builder.id(value.id.clone()); + if let Some(parent_id) = &value.parent_id { + project_builder.parent_id(parent_id); + } project_builder.name(value.name.clone()); project_builder.domain_id(value.domain_id.clone()); if let Some(description) = &value.description { @@ -39,7 +44,12 @@ impl TryFrom for Project { if let Some(extra) = &value.extra { project_builder.extra( serde_json::from_str::(extra) - .inspect_err(|e| error!("failed to deserialize project extra properties: {e}")) + .inspect_err(|e| { + error!( + "failed to deserialize project [id: {}] extra properties: {e}", + value.id + ) + }) .unwrap_or_default(), ); } diff --git a/src/resource/backend/sql/project/tree.rs b/src/resource/backend/sql/project/tree.rs new file mode 100644 index 00000000..d0756dd8 --- /dev/null +++ b/src/resource/backend/sql/project/tree.rs @@ -0,0 +1,140 @@ +// 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 sea_orm::DatabaseConnection; +use sea_orm::entity::*; + +use crate::db::entity::prelude::Project as DbProject; +use crate::resource::backend::error::{ResourceDatabaseError, db_err}; +use crate::resource::types::Project; + +pub async fn get_project_parents>( + db: &DatabaseConnection, + id: I, +) -> Result>, ResourceDatabaseError> { + let mut res: Vec = Vec::new(); + let mut project_id: Option = Some(id.as_ref().to_string()); + while let Some(pid) = project_id { + let project = DbProject::find_by_id(pid.clone()) + .one(db) + .await + .map_err(|err| db_err(err, "resolving project parents"))?; + if pid == id.as_ref() && project.is_none() { + return Ok(None); + } + if let Some(p) = &project + && pid != id.as_ref() + { + res.push(p.clone().try_into()?); + } + project_id = project.and_then(|project| project.parent_id); + } + + Ok(Some(res)) +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use crate::db::entity::project; + + use super::*; + use crate::resource::types::Project; + + pub(super) fn get_mock, P: AsRef>( + id: I, + parent_id: Option

, + ) -> project::Model { + project::Model { + id: id.as_ref().to_string(), + name: "project_name".into(), + extra: None, + description: Some("description".into()), + enabled: Some(true), + domain_id: "domain_id".into(), + parent_id: parent_id.as_ref().map(|val| val.as_ref().into()), + is_domain: parent_id.as_ref().is_some(), + } + } + + #[tokio::test] + async fn test_get_project_parents() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_mock("3", Some("2"))]]) + .append_query_results([vec![get_mock("2", Some("1"))]]) + .append_query_results([vec![get_mock("1", Some("domain_id"))]]) + .append_query_results([vec![get_mock("domain_id", None::<&str>)]]) + .into_connection(); + assert_eq!( + get_project_parents(&db, "3").await.unwrap().unwrap(), + vec![ + Project { + id: "2".into(), + parent_id: Some("1".into()), + name: "project_name".into(), + domain_id: "domain_id".into(), + enabled: true, + description: Some("description".into()), + extra: None + }, + Project { + id: "1".into(), + parent_id: Some("domain_id".into()), + name: "project_name".into(), + domain_id: "domain_id".into(), + enabled: true, + description: Some("description".into()), + extra: None + }, + Project { + id: "domain_id".into(), + parent_id: None, + name: "project_name".into(), + domain_id: "domain_id".into(), + enabled: true, + description: Some("description".into()), + extra: None + } + ] + ); + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "project"."id", "project"."name", "project"."extra", "project"."description", "project"."enabled", "project"."domain_id", "project"."parent_id", "project"."is_domain" FROM "project" WHERE "project"."id" = $1 LIMIT $2"#, + ["3".into(), 1u64.into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "project"."id", "project"."name", "project"."extra", "project"."description", "project"."enabled", "project"."domain_id", "project"."parent_id", "project"."is_domain" FROM "project" WHERE "project"."id" = $1 LIMIT $2"#, + ["2".into(), 1u64.into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "project"."id", "project"."name", "project"."extra", "project"."description", "project"."enabled", "project"."domain_id", "project"."parent_id", "project"."is_domain" FROM "project" WHERE "project"."id" = $1 LIMIT $2"#, + ["1".into(), 1u64.into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "project"."id", "project"."name", "project"."extra", "project"."description", "project"."enabled", "project"."domain_id", "project"."parent_id", "project"."is_domain" FROM "project" WHERE "project"."id" = $1 LIMIT $2"#, + ["domain_id".into(), 1u64.into()] + ), + ] + ); + } +} diff --git a/src/resource/error.rs b/src/resource/error.rs index ee6458f5..c5473558 100644 --- a/src/resource/error.rs +++ b/src/resource/error.rs @@ -14,7 +14,7 @@ use thiserror::Error; -use crate::resource::backends::error::*; +use crate::resource::backend::error::*; use crate::resource::types::DomainBuilderError; #[derive(Error, Debug)] diff --git a/src/resource/mod.rs b/src/resource/mod.rs index 75419277..47e0ed1f 100644 --- a/src/resource/mod.rs +++ b/src/resource/mod.rs @@ -16,14 +16,14 @@ use async_trait::async_trait; #[cfg(test)] use mockall::mock; -pub mod backends; +pub mod backend; pub mod error; pub(crate) mod types; use crate::config::Config; use crate::keystone::ServiceState; use crate::plugin_manager::PluginManager; -use crate::resource::backends::sql::SqlBackend; +use crate::resource::backend::sql::SqlBackend; use crate::resource::error::ResourceProviderError; use crate::resource::types::{Domain, Project, ResourceBackend}; @@ -58,6 +58,13 @@ pub trait ResourceApi: Send + Sync + Clone { name: &'a str, domain_id: &'a str, ) -> Result, ResourceProviderError>; + + /// Get project parents + async fn get_project_parents<'a>( + &self, + state: &ServiceState, + project_id: &'a str, + ) -> Result>, ResourceProviderError>; } #[cfg(test)] @@ -93,6 +100,11 @@ mock! { domain_id: &'a str, ) -> Result, ResourceProviderError>; + async fn get_project_parents<'a>( + &self, + state: &ServiceState, + project_id: &'a str, + ) -> Result>, ResourceProviderError>; } impl Clone for ResourceProvider { @@ -170,4 +182,15 @@ impl ResourceApi for ResourceProvider { .get_project_by_name(state, name, domain_id) .await } + + /// Get project parents + async fn get_project_parents<'a>( + &self, + state: &ServiceState, + project_id: &'a str, + ) -> Result>, ResourceProviderError> { + self.backend_driver + .get_project_parents(state, project_id) + .await + } } diff --git a/src/resource/types.rs b/src/resource/types.rs index 3924e5e8..63556735 100644 --- a/src/resource/types.rs +++ b/src/resource/types.rs @@ -58,6 +58,13 @@ pub trait ResourceBackend: DynClone + Send + Sync + std::fmt::Debug { name: &'a str, domain_id: &'a str, ) -> Result, ResourceProviderError>; + + /// Get project parents + async fn get_project_parents<'a>( + &self, + state: &ServiceState, + project_id: &'a str, + ) -> Result>, ResourceProviderError>; } dyn_clone::clone_trait_object!(ResourceBackend); diff --git a/src/resource/types/project.rs b/src/resource/types/project.rs index 131a5613..e846c968 100644 --- a/src/resource/types/project.rs +++ b/src/resource/types/project.rs @@ -21,15 +21,19 @@ use serde_json::Value; pub struct Project { /// The project ID. pub id: String, + /// The ID of the parent for the project. + #[builder(default)] + pub parent_id: Option, /// The project name. pub name: String, /// The project domain_id. pub domain_id: String, + /// If set to true, project is enabled. If set to false, project is disabled. pub enabled: bool, - /// The resource description + /// The description of the project. #[builder(default)] pub description: Option, - /// Additional project properties + /// Additional project properties. #[builder(default)] pub extra: Option, }