From cf200aaf4247126192889f554f06aa9bab4d5bd4 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 24 Feb 2025 22:15:50 +0100 Subject: [PATCH] feat: Bootstrap resource provider --- src/api/error.rs | 26 +++- src/api/v3/auth/token/mod.rs | 61 ++++++++- src/api/v3/auth/token/types.rs | 38 ++++-- src/config.rs | 12 +- src/error.rs | 7 + src/plugin_manager.rs | 11 ++ src/provider.rs | 10 ++ src/resource/backends.rs | 16 +++ src/resource/backends/error.rs | 41 ++++++ src/resource/backends/sql.rs | 190 ++++++++++++++++++++++++++++ src/resource/backends/sql/domain.rs | 24 ++++ src/resource/error.rs | 57 +++++++++ src/resource/mod.rs | 86 ++++++++++++- src/resource/types.rs | 39 ++++++ src/resource/types/domain.rs | 33 +++++ src/tests/api.rs | 5 + 16 files changed, 632 insertions(+), 24 deletions(-) create mode 100644 src/resource/backends.rs create mode 100644 src/resource/backends/error.rs create mode 100644 src/resource/backends/sql.rs create mode 100644 src/resource/backends/sql/domain.rs create mode 100644 src/resource/error.rs create mode 100644 src/resource/types.rs create mode 100644 src/resource/types/domain.rs diff --git a/src/api/error.rs b/src/api/error.rs index bf4b240f..0460ae63 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -21,6 +21,7 @@ use serde_json::json; use thiserror::Error; use crate::identity::error::IdentityProviderError; +use crate::resource::error::ResourceProviderError; /// Keystone API operation errors #[derive(Debug, Error)] @@ -52,6 +53,12 @@ pub enum KeystoneApiError { source: crate::api::v3::auth::token::types::TokenBuilderError, }, + #[error("error building token user data: {}", source)] + TokenUserBuilder { + #[from] + source: crate::api::v3::auth::token::types::UserBuilderError, + }, + #[error("internal server error")] InternalError(String), @@ -60,6 +67,12 @@ pub enum KeystoneApiError { #[from] source: IdentityProviderError, }, + + #[error(transparent)] + ResourceError { + #[from] + source: ResourceProviderError, + }, } impl IntoResponse for KeystoneApiError { @@ -84,12 +97,12 @@ impl IntoResponse for KeystoneApiError { Json(json!({"error": {"code": StatusCode::INTERNAL_SERVER_ERROR.as_u16(), "message": self.to_string()}})), ).into_response() } - KeystoneApiError::IdentityError { .. } => { + KeystoneApiError::IdentityError { .. } | KeystoneApiError::ResourceError { .. } => { (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": {"code": StatusCode::INTERNAL_SERVER_ERROR.as_u16(), "message": self.to_string()}})), ).into_response() } - KeystoneApiError::SubjectTokenMissing | KeystoneApiError::InvalidHeader | KeystoneApiError::InvalidToken | KeystoneApiError::TokenBuilder{..} => { + KeystoneApiError::SubjectTokenMissing | KeystoneApiError::InvalidHeader | KeystoneApiError::InvalidToken | KeystoneApiError::TokenBuilder{..} | KeystoneApiError::TokenUserBuilder {..}=> { (StatusCode::BAD_REQUEST, Json(json!({"error": {"code": StatusCode::BAD_REQUEST.as_u16(), "message": self.to_string()}})), ).into_response() @@ -112,4 +125,13 @@ impl KeystoneApiError { _ => Self::IdentityError { source }, } } + pub fn resource(source: ResourceProviderError) -> Self { + match source { + ResourceProviderError::DomainNotFound(x) => Self::NotFound { + resource: "domain".into(), + identifier: x, + }, + _ => Self::ResourceError { source }, + } + } } diff --git a/src/api/v3/auth/token/mod.rs b/src/api/v3/auth/token/mod.rs index cc0acf6c..63e77475 100644 --- a/src/api/v3/auth/token/mod.rs +++ b/src/api/v3/auth/token/mod.rs @@ -19,8 +19,9 @@ use crate::api::auth::Auth; use crate::api::error::KeystoneApiError; use crate::identity::IdentityApi; use crate::keystone::ServiceState; +use crate::resource::ResourceApi; use crate::token::TokenApi; -use types::{TokenBuilder, TokenResponse, User}; +use types::{TokenBuilder, TokenResponse, UserBuilder}; pub mod types; @@ -75,8 +76,23 @@ async fn validate( identifier: token.user_id().clone(), })?; - let user_response: User = user.into(); - response.user(user_response); + let user_domain = state + .provider + .get_resource_provider() + .get_domain(&state.db, user.domain_id.clone()) + .await + .map_err(KeystoneApiError::resource)? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "domain".into(), + identifier: user.domain_id.clone(), + })?; + + let mut user_response: UserBuilder = UserBuilder::default(); + user_response.id(user.id); + user_response.name(user.name); + user_response.password_expires_at(user.password_expires_at); + user_response.domain(user_domain); + response.user(user_response.build()?); Ok(TokenResponse { token: response.build()?, @@ -91,16 +107,24 @@ mod tests { }; use http_body_util::BodyExt; // for `collect` use sea_orm::DatabaseConnection; + use std::sync::Arc; use tower::ServiceExt; // for `call`, `oneshot`, and `ready` use tower_http::trace::TraceLayer; use super::openapi_router; use crate::api::v3::auth::token::types::TokenResponse; + use crate::config::Config; use crate::identity::{MockIdentityProvider, types::User}; - use crate::tests::api::{get_mocked_state, get_mocked_state_unauthed}; + use crate::keystone::Service; + use crate::provider::ProviderBuilder; + use crate::resource::{MockResourceProvider, types::Domain}; + use crate::tests::api::get_mocked_state_unauthed; + use crate::token::{MockTokenProvider, Token, UnscopedToken}; #[tokio::test] async fn test_get() { + let db = DatabaseConnection::Disconnected; + let config = Config::default(); let mut identity_mock = MockIdentityProvider::default(); identity_mock .expect_get_user() @@ -108,11 +132,38 @@ mod tests { .returning(|_, _| { Ok(Some(User { id: "bar".into(), + domain_id: "domain_id".into(), ..Default::default() })) }); - let state = get_mocked_state(identity_mock); + let mut resource_mock = MockResourceProvider::default(); + resource_mock + .expect_get_domain() + .withf(|_: &DatabaseConnection, id: &String| *id == "domain_id") + .returning(|_, _| { + Ok(Some(Domain { + id: "domain_id".into(), + ..Default::default() + })) + }); + let mut token_mock = MockTokenProvider::default(); + token_mock.expect_validate_token().returning(|_, _| { + Ok(Token::Unscoped(UnscopedToken { + user_id: "bar".into(), + ..Default::default() + })) + }); + + let provider = ProviderBuilder::default() + .config(config.clone()) + .identity(identity_mock) + .resource(resource_mock) + .token(token_mock) + .build() + .unwrap(); + + let state = Arc::new(Service::new(config, db, provider).unwrap()); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) diff --git a/src/api/v3/auth/token/types.rs b/src/api/v3/auth/token/types.rs index d57d7023..dafcba30 100644 --- a/src/api/v3/auth/token/types.rs +++ b/src/api/v3/auth/token/types.rs @@ -22,7 +22,7 @@ use derive_builder::Builder; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::identity::types as provider_types; +use crate::resource::types as resource_provider_types; /// Authorization token #[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] @@ -78,9 +78,9 @@ impl IntoResponse for TokenResponse { #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] pub struct Project { /// Project ID - id: String, + pub id: String, /// Project Name - name: String, + pub name: String, } /// User information @@ -88,20 +88,40 @@ pub struct Project { #[builder(setter(into))] pub struct User { /// User ID - id: String, + pub id: String, /// User Name - name: String, + pub name: String, + /// User domain + pub domain: Domain, /// User password expiry date #[serde(skip_serializing_if = "Option::is_none")] - password_expires_at: Option>, + pub password_expires_at: Option>, } -impl From for User { - fn from(value: provider_types::User) -> Self { +/// Domain information +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[builder(setter(into))] +pub struct Domain { + /// Domain ID + pub id: String, + /// Domain Name + pub name: String, +} + +//impl From for User { +// fn from(value: identity_provider_types::User) -> Self { +// Self { +// id: value.id.clone(), +// name: value.name.clone(), +// } +// } +//} + +impl From for Domain { + fn from(value: resource_provider_types::Domain) -> Self { Self { id: value.id.clone(), name: value.name.clone(), - password_expires_at: value.password_expires_at.clone(), } } } diff --git a/src/config.rs b/src/config.rs index 14331603..872d9b7c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -41,6 +41,10 @@ pub struct Config { #[serde(default)] pub identity: IdentitySection, + /// Resource provider related configuration + #[serde(default)] + pub resource: ResourceSection, + /// Security compliance #[serde(default)] pub security_compliance: SecurityComplianceSection, @@ -105,6 +109,11 @@ pub struct IdentitySection { pub password_hash_rounds: Option, } +#[derive(Debug, Default, Deserialize, Clone)] +pub struct ResourceSection { + pub driver: String, +} + #[derive(Debug, Default, Deserialize, Clone)] pub enum PasswordHashingAlgo { #[default] @@ -152,7 +161,8 @@ impl Config { builder = builder .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("fernet_tokens.max_active_keys", "3")? + .set_default("resource.driver", "sql")?; if std::path::Path::new(&path).is_file() { builder = builder.add_source(File::from(path).format(FileFormat::Ini)); } diff --git a/src/error.rs b/src/error.rs index 60b0b039..930aff0d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,6 +15,7 @@ use thiserror::Error; use crate::identity::error::*; +use crate::resource::error::*; use crate::token::TokenProviderError; #[derive(Debug, Error)] @@ -25,6 +26,12 @@ pub enum KeystoneError { source: IdentityProviderError, }, + #[error(transparent)] + ResourceError { + #[from] + source: ResourceProviderError, + }, + #[error(transparent)] TokenProvider { #[from] diff --git a/src/plugin_manager.rs b/src/plugin_manager.rs index 086f65ce..b78c52b2 100644 --- a/src/plugin_manager.rs +++ b/src/plugin_manager.rs @@ -15,6 +15,7 @@ use std::collections::HashMap; use crate::identity::types::IdentityBackend; +use crate::resource::types::ResourceBackend; /// Plugin manager allowing to pass custom backend plugins implementing required trait during the /// service start @@ -22,6 +23,7 @@ use crate::identity::types::IdentityBackend; pub struct PluginManager { /// Identity backend plugins identity_backends: HashMap>, + resource_backends: HashMap>, } impl PluginManager { @@ -43,4 +45,13 @@ impl PluginManager { ) -> Option<&Box> { self.identity_backends.get(name.as_ref()) } + + /// Get registered resource backend + #[allow(clippy::borrowed_box)] + pub fn get_resource_backend>( + &self, + name: S, + ) -> Option<&Box> { + self.resource_backends.get(name.as_ref()) + } } diff --git a/src/provider.rs b/src/provider.rs index ce49a6d5..57899441 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -20,6 +20,9 @@ use crate::identity::IdentityApi; #[double] use crate::identity::IdentityProvider; use crate::plugin_manager::PluginManager; +use crate::resource::ResourceApi; +#[double] +use crate::resource::ResourceProvider; use crate::token::TokenApi; #[double] use crate::token::TokenProvider; @@ -36,17 +39,20 @@ use crate::token::TokenProvider; pub struct Provider { pub config: Config, identity: IdentityProvider, + resource: ResourceProvider, token: TokenProvider, } impl Provider { pub fn new(cfg: Config, plugin_manager: PluginManager) -> Result { let identity_provider = IdentityProvider::new(&cfg, &plugin_manager)?; + let resource_provider = ResourceProvider::new(&cfg, &plugin_manager)?; let token_provider = TokenProvider::new(&cfg)?; Ok(Self { config: cfg, identity: identity_provider, + resource: resource_provider, token: token_provider, }) } @@ -55,6 +61,10 @@ impl Provider { &self.identity } + pub fn get_resource_provider(&self) -> &impl ResourceApi { + &self.resource + } + pub fn get_token_provider(&self) -> &impl TokenApi { &self.token } diff --git a/src/resource/backends.rs b/src/resource/backends.rs new file mode 100644 index 00000000..a4618644 --- /dev/null +++ b/src/resource/backends.rs @@ -0,0 +1,16 @@ +// 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 error; +pub mod sql; diff --git a/src/resource/backends/error.rs b/src/resource/backends/error.rs new file mode 100644 index 00000000..27740473 --- /dev/null +++ b/src/resource/backends/error.rs @@ -0,0 +1,41 @@ +// 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 thiserror::Error; + +use crate::resource::types::*; + +#[derive(Error, Debug)] +pub enum ResourceDatabaseError { + #[error("domain {0} not found")] + DomainNotFound(String), + + #[error("data serialization error")] + Serde { + #[from] + source: serde_json::Error, + }, + + #[error("building domain data")] + DomainBuilderError { + #[from] + source: DomainBuilderError, + }, + + #[error("database data")] + Database { + #[from] + source: sea_orm::DbErr, + }, +} diff --git a/src/resource/backends/sql.rs b/src/resource/backends/sql.rs new file mode 100644 index 00000000..8a3d26da --- /dev/null +++ b/src/resource/backends/sql.rs @@ -0,0 +1,190 @@ +// 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 async_trait::async_trait; +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; +use serde_json::Value; + +mod domain; + +use super::super::types::*; +use crate::config::Config; +use crate::db::entity::{prelude::Project as DbProject, project as db_project}; +use crate::resource::ResourceProviderError; +use crate::resource::backends::error::ResourceDatabaseError; + +#[derive(Clone, Debug, Default)] +pub struct SqlBackend { + pub config: Config, +} + +impl SqlBackend {} + +#[async_trait] +impl ResourceBackend for SqlBackend { + /// Set config + fn set_config(&mut self, config: Config) { + self.config = config; + } + + /// Get single domain by ID + #[tracing::instrument(level = "debug", skip(self, db))] + async fn get_domain( + &self, + db: &DatabaseConnection, + domain_id: String, + ) -> Result, ResourceProviderError> { + Ok(get_domain(&self.config, db, domain_id).await?) + } +} + +pub async fn get_domain( + conf: &Config, + db: &DatabaseConnection, + domain_id: String, +) -> Result, ResourceDatabaseError> { + let domain_select = + DbProject::find_by_id(&domain_id).filter(db_project::Column::IsDomain.eq(true)); + + let domain_entry: Option = domain_select.one(db).await?; + + if let Some(domain) = &domain_entry { + let mut domain_builder = DomainBuilder::default(); + domain_builder.id(domain.id.clone()); + domain_builder.name(domain.name.clone()); + if let Some(description) = &domain.description { + domain_builder.description(description.clone()); + } + domain_builder.enabled(domain.enabled.unwrap_or(false)); + if let Some(extra) = &domain.extra { + domain_builder.extra(serde_json::from_str::(extra).unwrap()); + } + + return Ok(Some(domain_builder.build()?)); + } + + Ok(None) +} + +//#[cfg(test)] +//mod tests { +// #![allow(clippy::derivable_impls)] +// use chrono::Local; +// use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; +// +// use crate::db::entity::{local_user, password, user, user_option}; +// use crate::identity::Config; +// +// use super::*; +// +// fn get_user_mock(user_id: String) -> user::Model { +// user::Model { +// id: user_id.clone(), +// domain_id: "foo_domain".into(), +// enabled: Some(true), +// ..Default::default() +// } +// } +// +// fn get_local_user_with_password_mock( +// user_id: String, +// cnt_password: usize, +// ) -> Vec<(local_user::Model, password::Model)> { +// let lu = local_user::Model { +// user_id: user_id.clone(), +// domain_id: "foo_domain".into(), +// name: "Apple Cake".to_owned(), +// ..Default::default() +// }; +// let mut passwords: Vec = Vec::new(); +// for i in 0..cnt_password { +// passwords.push(password::Model { +// id: i as i32, +// local_user_id: 1, +// expires_at: None, +// self_service: false, +// password_hash: None, +// created_at: Local::now().naive_utc(), +// created_at_int: 12345, +// expires_at_int: None, +// }); +// } +// passwords +// .into_iter() +// .map(|x| (lu.clone(), x.clone())) +// .collect() +// } +// +// #[tokio::test] +// async fn test_get_user_local() { +// // Create MockDatabase with mock query results +// let db = MockDatabase::new(DatabaseBackend::Postgres) +// .append_query_results([ +// // First query result - select user itself +// vec![get_user_mock("1".into())], +// ]) +// .append_query_results([ +// //// Second query result - user options +// vec![user_option::Model { +// user_id: "1".into(), +// option_id: "1000".into(), +// option_value: Some("true".into()), +// }], +// ]) +// .append_query_results([ +// // Third query result - local user with passwords +// get_local_user_with_password_mock("1".into(), 1), +// ]) +// .into_connection(); +// let config = Config::default(); +// assert_eq!( +// get_user(&config, &db, "1".into()).await.unwrap().unwrap(), +// User { +// id: "1".into(), +// domain_id: "foo_domain".into(), +// name: "Apple Cake".to_owned(), +// enabled: true, +// options: UserOptions { +// ignore_change_password_upon_first_use: Some(true), +// ..Default::default() +// }, +// ..Default::default() +// } +// ); +// +// // Checking transaction log +// assert_eq!( +// db.into_transaction_log(), +// [ +// Transaction::from_sql_and_values( +// DatabaseBackend::Postgres, +// r#"SELECT "user"."id", "user"."extra", "user"."enabled", "user"."default_project_id", "user"."created_at", "user"."last_active_at", "user"."domain_id" FROM "user" WHERE "user"."id" = $1 LIMIT $2"#, +// ["1".into(), 1u64.into()] +// ), +// Transaction::from_sql_and_values( +// DatabaseBackend::Postgres, +// r#"SELECT "user_option"."user_id", "user_option"."option_id", "user_option"."option_value" FROM "user_option" INNER JOIN "user" ON "user"."id" = "user_option"."user_id" WHERE "user"."id" = $1"#, +// ["1".into()] +// ), +// Transaction::from_sql_and_values( +// DatabaseBackend::Postgres, +// r#"SELECT "local_user"."id" AS "A_id", "local_user"."user_id" AS "A_user_id", "local_user"."domain_id" AS "A_domain_id", "local_user"."name" AS "A_name", "local_user"."failed_auth_count" AS "A_failed_auth_count", "local_user"."failed_auth_at" AS "A_failed_auth_at", "password"."id" AS "B_id", "password"."local_user_id" AS "B_local_user_id", "password"."self_service" AS "B_self_service", "password"."created_at" AS "B_created_at", "password"."expires_at" AS "B_expires_at", "password"."password_hash" AS "B_password_hash", "password"."created_at_int" AS "B_created_at_int", "password"."expires_at_int" AS "B_expires_at_int" FROM "local_user" LEFT JOIN "password" ON "local_user"."id" = "password"."local_user_id" WHERE "local_user"."user_id" = $1 ORDER BY "local_user"."id" ASC"#, +// ["1".into()] +// ), +// ] +// ); +// } +//} diff --git a/src/resource/backends/sql/domain.rs b/src/resource/backends/sql/domain.rs new file mode 100644 index 00000000..85ce919c --- /dev/null +++ b/src/resource/backends/sql/domain.rs @@ -0,0 +1,24 @@ +// 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 chrono::Local; +//use sea_orm::DatabaseConnection; +//use sea_orm::entity::*; +// +//use crate::config::Config; +// use crate::db::entity::{prelude::Domain as DbDomain, domain}; +//use crate::resource::backends::error::ResourceDatabaseError; + +#[cfg(test)] +mod tests {} diff --git a/src/resource/error.rs b/src/resource/error.rs new file mode 100644 index 00000000..1a2b8dea --- /dev/null +++ b/src/resource/error.rs @@ -0,0 +1,57 @@ +// 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 thiserror::Error; + +use crate::resource::backends::error::*; +use crate::resource::types::DomainBuilderError; + +#[derive(Error, Debug)] +pub enum ResourceProviderError { + /// Unsupported driver + #[error("unsupported driver {0}")] + UnsupportedDriver(String), + + /// Identity provider error + #[error("data serialization error")] + Serde { + #[from] + source: serde_json::Error, + }, + + #[error("domain {0} not found")] + DomainNotFound(String), + + /// Identity provider error + #[error("resource provider error")] + ResourceDatabaseError { + #[from] + source: ResourceDatabaseError, + }, + + #[error("building domain data")] + DomainBuilderError { + #[from] + source: DomainBuilderError, + }, +} + +impl ResourceProviderError { + pub fn database(source: ResourceDatabaseError) -> Self { + match source { + ResourceDatabaseError::DomainNotFound(x) => Self::DomainNotFound(x), + _ => Self::ResourceDatabaseError { source }, + } + } +} diff --git a/src/resource/mod.rs b/src/resource/mod.rs index 9896ea51..2f9697ad 100644 --- a/src/resource/mod.rs +++ b/src/resource/mod.rs @@ -12,16 +12,88 @@ // // SPDX-License-Identifier: Apache-2.0 -pub struct ResourceSrv {} +use async_trait::async_trait; +#[cfg(test)] +use mockall::mock; +use sea_orm::DatabaseConnection; -impl Default for ResourceSrv { - fn default() -> Self { - Self::new() +pub mod backends; +pub mod error; +pub(crate) mod types; + +use crate::config::Config; +use crate::plugin_manager::PluginManager; +use crate::resource::backends::sql::SqlBackend; +use crate::resource::error::ResourceProviderError; +use crate::resource::types::{Domain, ResourceBackend}; + +#[derive(Clone, Debug)] +pub struct ResourceProvider { + backend_driver: Box, +} + +#[async_trait] +pub trait ResourceApi: Send + Sync + Clone { + async fn get_domain( + &self, + db: &DatabaseConnection, + domain_id: String, + ) -> Result, ResourceProviderError>; +} + +#[cfg(test)] +mock! { + pub ResourceProvider { + pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; + } + + #[async_trait] + impl ResourceApi for ResourceProvider { + async fn get_domain( + &self, + db: &DatabaseConnection, + domain_id: String, + ) -> Result, ResourceProviderError>; + } + + impl Clone for ResourceProvider { + fn clone(&self) -> Self; + } +} + +impl ResourceProvider { + pub fn new( + config: &Config, + plugin_manager: &PluginManager, + ) -> Result { + let mut backend_driver = if let Some(driver) = + plugin_manager.get_resource_backend(config.resource.driver.clone()) + { + driver.clone() + } else { + match config.resource.driver.as_str() { + "sql" => Box::new(SqlBackend::default()), + _ => { + return Err(ResourceProviderError::UnsupportedDriver( + config.resource.driver.clone(), + )); + } + } + }; + backend_driver.set_config(config.clone()); + Ok(Self { backend_driver }) } } -impl ResourceSrv { - pub fn new() -> Self { - Self {} +#[async_trait] +impl ResourceApi for ResourceProvider { + /// Get single domain + #[tracing::instrument(level = "info", skip(self, db))] + async fn get_domain( + &self, + db: &DatabaseConnection, + domain_id: String, + ) -> Result, ResourceProviderError> { + self.backend_driver.get_domain(db, domain_id).await } } diff --git a/src/resource/types.rs b/src/resource/types.rs new file mode 100644 index 00000000..842b1cc5 --- /dev/null +++ b/src/resource/types.rs @@ -0,0 +1,39 @@ +// 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 domain; + +use async_trait::async_trait; +use dyn_clone::DynClone; +use sea_orm::DatabaseConnection; + +use crate::config::Config; +use crate::resource::ResourceProviderError; + +pub use crate::resource::types::domain::{Domain, DomainBuilder, DomainBuilderError}; + +#[async_trait] +pub trait ResourceBackend: DynClone + Send + Sync + std::fmt::Debug { + /// Set config + fn set_config(&mut self, config: Config); + + /// Get single domain by ID + async fn get_domain( + &self, + db: &DatabaseConnection, + domain_id: String, + ) -> Result, ResourceProviderError>; +} + +dyn_clone::clone_trait_object!(ResourceBackend); diff --git a/src/resource/types/domain.rs b/src/resource/types/domain.rs new file mode 100644 index 00000000..34164bae --- /dev/null +++ b/src/resource/types/domain.rs @@ -0,0 +1,33 @@ +// 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 derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[builder(setter(strip_option, into))] +pub struct Domain { + /// The domain ID. + pub id: String, + /// The domain name. + pub name: String, + pub enabled: bool, + /// The resource description + #[builder(default)] + pub description: Option, + /// Additional user properties + #[builder(default)] + pub extra: Option, +} diff --git a/src/tests/api.rs b/src/tests/api.rs index 4d506538..ad89130f 100644 --- a/src/tests/api.rs +++ b/src/tests/api.rs @@ -19,12 +19,14 @@ use crate::config::Config; use crate::identity::MockIdentityProvider; use crate::keystone::{Service, ServiceState}; use crate::provider::ProviderBuilder; +use crate::resource::MockResourceProvider; use crate::token::{MockTokenProvider, Token, TokenProviderError, UnscopedToken}; pub(crate) fn get_mocked_state_unauthed() -> ServiceState { let db = DatabaseConnection::Disconnected; let config = Config::default(); let identity_mock = MockIdentityProvider::default(); + let resource_mock = MockResourceProvider::default(); let mut token_mock = MockTokenProvider::default(); token_mock .expect_validate_token() @@ -33,6 +35,7 @@ pub(crate) fn get_mocked_state_unauthed() -> ServiceState { let provider = ProviderBuilder::default() .config(config.clone()) .identity(identity_mock) + .resource(resource_mock) .token(token_mock) .build() .unwrap(); @@ -44,6 +47,7 @@ pub(crate) fn get_mocked_state(identity_mock: MockIdentityProvider) -> ServiceSt let db = DatabaseConnection::Disconnected; let config = Config::default(); let mut token_mock = MockTokenProvider::default(); + let resource_mock = MockResourceProvider::default(); token_mock.expect_validate_token().returning(|_, _| { Ok(Token::Unscoped(UnscopedToken { user_id: "bar".into(), @@ -54,6 +58,7 @@ pub(crate) fn get_mocked_state(identity_mock: MockIdentityProvider) -> ServiceSt let provider = ProviderBuilder::default() .config(config.clone()) .identity(identity_mock) + .resource(resource_mock) .token(token_mock) .build() .unwrap();