diff --git a/src/identity/backends.rs b/src/identity/backends.rs index 4a69a20b..e9f433ed 100644 --- a/src/identity/backends.rs +++ b/src/identity/backends.rs @@ -12,6 +12,201 @@ // // SPDX-License-Identifier: Apache-2.0 +use async_trait::async_trait; +use dyn_clone::DynClone; +use std::collections::HashSet; +use webauthn_rs::prelude::{Passkey, PasskeyAuthentication, PasskeyRegistration}; + +use crate::auth::AuthenticatedInfo; +use crate::config::Config; +use crate::identity::IdentityProviderError; +use crate::identity::types::*; +use crate::keystone::ServiceState; + pub mod error; -//pub(crate) mod fake; pub mod sql; + +#[async_trait] +pub trait IdentityBackend: DynClone + Send + Sync + std::fmt::Debug { + /// Set config. + fn set_config(&mut self, config: Config); + + /// Authenticate a user by a password. + async fn authenticate_by_password( + &self, + state: &ServiceState, + auth: UserPasswordAuthRequest, + ) -> Result; + + /// List Users. + async fn list_users( + &self, + state: &ServiceState, + params: &UserListParameters, + ) -> Result, IdentityProviderError>; + + /// Get single user by ID. + async fn get_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError>; + + /// Find federated user by IDP and Unique ID. + async fn find_federated_user<'a>( + &self, + state: &ServiceState, + idp_id: &'a str, + unique_id: &'a str, + ) -> Result, IdentityProviderError>; + + /// Create user. + async fn create_user( + &self, + state: &ServiceState, + user: UserCreate, + ) -> Result; + + /// Delete user. + async fn delete_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + /// List groups. + async fn list_groups( + &self, + state: &ServiceState, + params: &GroupListParameters, + ) -> Result, IdentityProviderError>; + + /// Get single group by ID. + async fn get_group<'a>( + &self, + state: &ServiceState, + group_id: &'a str, + ) -> Result, IdentityProviderError>; + + /// Create group. + async fn create_group( + &self, + state: &ServiceState, + group: GroupCreate, + ) -> Result; + + /// Delete group by ID. + async fn delete_group<'a>( + &self, + state: &ServiceState, + group_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + /// List groups a user is member of. + async fn list_groups_of_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError>; + + /// Add the user to the group. + async fn add_user_to_group<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + /// Add user group membership relations. + async fn add_users_to_groups<'a>( + &self, + state: &ServiceState, + memberships: Vec<(&'a str, &'a str)>, + ) -> Result<(), IdentityProviderError>; + + /// Remove the user from the group. + async fn remove_user_from_group<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + /// Remove the user from multiple groups. + async fn remove_user_from_groups<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + ) -> Result<(), IdentityProviderError>; + + /// Set group memberships for the user. + async fn set_user_groups<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + ) -> Result<(), IdentityProviderError>; + + /// List user passkeys. + async fn list_user_webauthn_credentials<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError>; + + /// Create passkey. + async fn create_user_webauthn_credential<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + passkey: &Passkey, + description: Option<&'a str>, + ) -> Result; + + /// Save passkey registration state. + async fn create_user_webauthn_credential_registration_state<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + state: PasskeyRegistration, + ) -> Result<(), IdentityProviderError>; + + /// Save passkey auth state. + async fn create_user_webauthn_credential_authentication_state<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + state: PasskeyAuthentication, + ) -> Result<(), IdentityProviderError>; + + /// Get passkey registration state. + async fn get_user_webauthn_credential_registration_state<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError>; + + /// Get passkey authentication state. + async fn get_user_webauthn_credential_authentication_state<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError>; + + /// Delete passkey registration state of a user. + async fn delete_user_webauthn_credential_registration_state<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + /// Delete passkey authentication state of a user. + async fn delete_user_webauthn_credential_authentication_state<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result<(), IdentityProviderError>; +} + +dyn_clone::clone_trait_object!(IdentityBackend); diff --git a/src/identity/backends/sql.rs b/src/identity/backends/sql.rs index 332869d7..853375f8 100644 --- a/src/identity/backends/sql.rs +++ b/src/identity/backends/sql.rs @@ -13,16 +13,14 @@ // SPDX-License-Identifier: Apache-2.0 use async_trait::async_trait; -use sea_orm::DatabaseConnection; -use sea_orm::entity::*; -use sea_orm::query::*; use std::collections::HashSet; use webauthn_rs::prelude::{Passkey, PasskeyAuthentication, PasskeyRegistration}; -mod common; +mod authenticate; mod federated_user; mod group; mod local_user; +mod nonlocal_user; mod password; mod user; mod user_group; @@ -30,17 +28,11 @@ mod user_option; mod webauthn; use super::super::types::*; -use crate::auth::{AuthenticatedInfo, AuthenticationError}; +use crate::auth::AuthenticatedInfo; use crate::config::Config; -use crate::db::entity::{ - federated_user as db_federated_user, local_user as db_local_user, - nonlocal_user as db_nonlocal_user, password as db_password, - prelude::{FederatedUser, LocalUser, NonlocalUser, User as DbUser, UserOption}, - user as db_user, -}; use crate::identity::IdentityProviderError; +use crate::identity::backends::IdentityBackend; use crate::identity::backends::error::{IdentityDatabaseError, db_err}; -use crate::identity::password_hashing; use crate::keystone::ServiceState; #[derive(Clone, Debug, Default)] @@ -63,47 +55,7 @@ impl IdentityBackend for SqlBackend { state: &ServiceState, auth: UserPasswordAuthRequest, ) -> Result { - let user_with_passwords = local_user::load_local_user_with_passwords( - &state.db, - auth.id, - auth.name, - auth.domain.and_then(|x| x.id), - ) - .await?; - if let Some((local_user, password)) = user_with_passwords { - let passwords: Vec = password.into_iter().collect(); - if let Some(latest_password) = passwords.first() - && let Some(expected_hash) = &latest_password.password_hash - { - let user_opts = user_option::get(&state.db, local_user.user_id.clone()).await?; - - if password_hashing::verify_password(&self.config, auth.password, expected_hash) - .await? - { - if let Some(user) = user::get(&state.db, &local_user.user_id).await? { - // TODO: Check password is expired - // TODO: reset failed login attempt - let user_builder = common::get_local_user_builder( - &self.config, - &user, - local_user, - Some(passwords), - user_opts, - ); - let user = user_builder.build()?; - return Ok(AuthenticatedInfo::builder() - .user_id(user.id.clone()) - .user(user) - .methods(vec!["password".into()]) - .build() - .map_err(AuthenticationError::from)?); - } - } else { - return Err(IdentityProviderError::WrongUsernamePassword); - } - } - } - return Err(IdentityProviderError::WrongUsernamePassword); + authenticate::authenticate_by_password(&self.config, &state.db, auth).await } /// Fetch users from the database @@ -113,7 +65,7 @@ impl IdentityBackend for SqlBackend { state: &ServiceState, params: &UserListParameters, ) -> Result, IdentityProviderError> { - Ok(list_users(&self.config, &state.db, params).await?) + Ok(user::list(&self.config, &state.db, params).await?) } /// Get single user by ID @@ -123,7 +75,7 @@ impl IdentityBackend for SqlBackend { state: &ServiceState, user_id: &'a str, ) -> Result, IdentityProviderError> { - Ok(get_user(&self.config, &state.db, user_id).await?) + Ok(user::get(&self.config, &state.db, user_id).await?) } /// Find federated user by IDP and Unique ID @@ -134,7 +86,13 @@ impl IdentityBackend for SqlBackend { idp_id: &'a str, unique_id: &'a str, ) -> Result, IdentityProviderError> { - Ok(find_federated_user(&self.config, &state.db, idp_id, unique_id).await?) + if let Some(federated_user) = + federated_user::find_by_idp_and_unique_id(&self.config, &state.db, idp_id, unique_id) + .await? + { + return Ok(user::get(&self.config, &state.db, &federated_user.user_id).await?); + } + Ok(None) } /// Create user @@ -144,7 +102,7 @@ impl IdentityBackend for SqlBackend { state: &ServiceState, user: UserCreate, ) -> Result { - Ok(create_user(&self.config, &state.db, user).await?) + Ok(user::create(&self.config, &state.db, user).await?) } /// Delete user @@ -346,363 +304,5 @@ impl IdentityBackend for SqlBackend { } } -async fn list_users( - conf: &Config, - db: &DatabaseConnection, - params: &UserListParameters, -) -> Result, IdentityDatabaseError> { - // Prepare basic selects - let mut user_select = DbUser::find(); - let mut local_user_select = LocalUser::find(); - let mut nonlocal_user_select = NonlocalUser::find(); - let mut federated_user_select = FederatedUser::find(); - - if let Some(domain_id) = ¶ms.domain_id { - user_select = user_select.filter(db_user::Column::DomainId.eq(domain_id)); - } - if let Some(name) = ¶ms.name { - local_user_select = local_user_select.filter(db_local_user::Column::Name.eq(name)); - nonlocal_user_select = nonlocal_user_select.filter(db_nonlocal_user::Column::Name.eq(name)); - federated_user_select = - federated_user_select.filter(db_federated_user::Column::DisplayName.eq(name)); - } - - let db_users: Vec = user_select - .all(db) - .await - .map_err(|err| db_err(err, "fetching users data"))?; - - let (user_opts, local_users, nonlocal_users, federated_users) = tokio::join!( - db_users.load_many(UserOption, db), - db_users.load_one(local_user_select, db), - db_users.load_one(nonlocal_user_select, db), - db_users.load_many(federated_user_select, db) - ); - - let locals = local_users.map_err(|err| db_err(err, "fetching local users data"))?; - - let local_users_passwords: Vec>> = - local_user::load_local_users_passwords(db, locals.iter().cloned().map(|u| u.map(|x| x.id))) - .await?; - - let mut results: Vec = Vec::new(); - for (u, (o, (l, (p, (n, f))))) in db_users.into_iter().zip( - user_opts - .map_err(|err| db_err(err, "fetching user options"))? - .into_iter() - .zip( - locals.into_iter().zip( - local_users_passwords.into_iter().zip( - nonlocal_users - .map_err(|err| db_err(err, "fetching nonlocal users data"))? - .into_iter() - .zip( - federated_users - .map_err(|err| db_err(err, "fetching federated users data"))? - .into_iter(), - ), - ), - ), - ), - ) { - if l.is_none() && n.is_none() && f.is_empty() { - continue; - } - let user_builder: UserResponseBuilder = if let Some(local) = l { - common::get_local_user_builder(conf, &u, local, p.map(|x| x.into_iter()), o) - } else if let Some(nonlocal) = n { - common::get_nonlocal_user_builder(&u, nonlocal, o) - } else if !f.is_empty() { - common::get_federated_user_builder(&u, f, o) - } else { - return Err(IdentityDatabaseError::MalformedUser(u.id))?; - }; - results.push(user_builder.build()?); - } - - //let select: Vec<(String, Option, )> = DbUser::find() - //let select = DbUser::find(); - //let select = Prefixer::new(DbUser::find().select_only()) - // .add_columns(DbUser) - // .add_columns(LocalUser) - // .add_columns(NonlocalUser) - // .selector - // .left_join(LocalUser) - // .left_join(NonlocalUser) - // //.left_join(FederatedUser) - // .into_model::() - // .all(db) - // .await - // .unwrap(); - Ok(results) -} - -pub async fn get_user( - conf: &Config, - db: &DatabaseConnection, - user_id: &str, -) -> Result, IdentityDatabaseError> { - let user_select = DbUser::find_by_id(user_id); - - let user_entry: Option = user_select - .one(db) - .await - .map_err(|err| db_err(err, "fetching the user data"))?; - - if let Some(user) = user_entry { - let (user_opts, local_user_with_passwords) = tokio::join!( - user.find_related(UserOption).all(db), - local_user::load_local_user_with_passwords( - db, - Some(&user_id), - None::<&str>, - None::<&str>, - ) - ); - - let user_builder: UserResponseBuilder = match local_user_with_passwords? { - Some(local_user_with_passwords) => common::get_local_user_builder( - conf, - &user, - local_user_with_passwords.0, - Some(local_user_with_passwords.1), - user_opts.map_err(|err| db_err(err, "fetching user options"))?, - ), - _ => match user - .find_related(NonlocalUser) - .one(db) - .await - .map_err(|err| db_err(err, "fetching nonlocal user data"))? - { - Some(nonlocal_user) => common::get_nonlocal_user_builder( - &user, - nonlocal_user, - user_opts.map_err(|err| db_err(err, "fetching user options"))?, - ), - _ => { - let federated_user = user - .find_related(FederatedUser) - .all(db) - .await - .map_err(|err| db_err(err, "fetching federated user data"))?; - if !federated_user.is_empty() { - common::get_federated_user_builder( - &user, - federated_user, - user_opts.map_err(|err| db_err(err, "fetching user options"))?, - ) - } else { - return Err(IdentityDatabaseError::MalformedUser(user_id.to_string()))?; - } - } - }, - }; - - return Ok(Some(user_builder.build()?)); - } - - Ok(None) -} - -pub async fn find_federated_user, U: AsRef>( - conf: &Config, - db: &DatabaseConnection, - idp_id: I, - unique_id: U, -) -> Result, IdentityDatabaseError> { - if let Some(federated_entry) = - federated_user::find_by_idp_and_unique_id(conf, db, idp_id, unique_id).await? - { - return get_user(conf, db, &federated_entry.user_id).await; - } - Ok(None) -} - -async fn create_user( - conf: &Config, - db: &DatabaseConnection, - user: UserCreate, -) -> Result { - let main_user = user::create(conf, db, &user).await?; - if let Some(federation_data) = &user.federated { - let mut federated_entities: Vec = Vec::new(); - for federated_user in federation_data { - if federated_user.protocols.is_empty() { - federated_entities.push( - federated_user::create( - conf, - db, - db_federated_user::ActiveModel { - id: NotSet, - user_id: Set(user.id.clone()), - idp_id: Set(federated_user.idp_id.clone()), - protocol_id: Set("oidc".into()), - unique_id: Set(federated_user.unique_id.clone()), - display_name: Set(Some(user.name.clone())), - }, - ) - .await?, - ); - } else { - for proto in &federated_user.protocols { - federated_entities.push( - federated_user::create( - conf, - db, - db_federated_user::ActiveModel { - id: NotSet, - user_id: Set(user.id.clone()), - idp_id: Set(federated_user.idp_id.clone()), - protocol_id: Set(proto.protocol_id.clone()), - unique_id: Set(proto.unique_id.clone()), - display_name: Set(Some(user.name.clone())), - }, - ) - .await?, - ); - } - } - } - - let builder = - common::get_federated_user_builder(&main_user, federated_entities, Vec::new()); - - Ok(builder.build()?) - } else { - // Local user - let local_user = local_user::create(conf, db, &user).await?; - let mut passwords: Vec = Vec::new(); - if let Some(password) = &user.password { - let password_entry = password::create( - db, - local_user.id, - password_hashing::hash_password(conf, password).await?, - None, - ) - .await?; - - passwords.push(password_entry); - } - Ok(common::get_local_user_builder( - conf, - &main_user, - local_user, - Some(passwords), - Vec::new(), - ) - .build()?) - } - // let ub = common::get_user_builder(&main_user, Vec::new()).build()?; - - // Ok(ub) -} - #[cfg(test)] -mod tests { - #![allow(clippy::derivable_impls)] - use chrono::Local; - use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; - - use crate::config::Config; - use crate::db::entity::{local_user, password, user, user_option}; - - 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").await.unwrap().unwrap(), - UserResponse { - 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, "password"."created_at_int" DESC"#, - ["1".into()] - ), - ] - ); - } -} +mod tests {} diff --git a/src/identity/backends/sql/authenticate.rs b/src/identity/backends/sql/authenticate.rs new file mode 100644 index 00000000..6a1be549 --- /dev/null +++ b/src/identity/backends/sql/authenticate.rs @@ -0,0 +1,72 @@ +// 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 super::local_user; +use super::user; +use super::user_option; +use crate::auth::{AuthenticatedInfo, AuthenticationError}; +use crate::config::Config; +use crate::db::entity::password as db_password; +use crate::identity::IdentityProviderError; +use crate::identity::password_hashing; +use crate::identity::types::*; + +/// Authenticate a user by a password +pub async fn authenticate_by_password( + config: &Config, + db: &DatabaseConnection, + auth: UserPasswordAuthRequest, +) -> Result { + let user_with_passwords = local_user::load_local_user_with_passwords( + db, + auth.id, + auth.name, + auth.domain.and_then(|x| x.id), + ) + .await?; + if let Some((local_user, password)) = user_with_passwords { + let passwords: Vec = password.into_iter().collect(); + if let Some(latest_password) = passwords.first() + && let Some(expected_hash) = &latest_password.password_hash + { + let user_opts = user_option::list_by_user_id(db, local_user.user_id.clone()).await?; + + if password_hashing::verify_password(config, auth.password, expected_hash).await? { + if let Some(user) = user::get_main_entry(db, &local_user.user_id).await? { + // TODO: Check password is expired + // TODO: reset failed login attempt + let user_builder = local_user::get_local_user_builder( + config, + &user, + local_user, + Some(passwords), + user_opts, + ); + let user = user_builder.build()?; + return Ok(AuthenticatedInfo::builder() + .user_id(user.id.clone()) + .user(user) + .methods(vec!["password".into()]) + .build() + .map_err(AuthenticationError::from)?); + } + } else { + return Err(IdentityProviderError::WrongUsernamePassword); + } + } + } + Err(IdentityProviderError::WrongUsernamePassword) +} diff --git a/src/identity/backends/sql/common.rs b/src/identity/backends/sql/common.rs deleted file mode 100644 index 3ca07964..00000000 --- a/src/identity/backends/sql/common.rs +++ /dev/null @@ -1,119 +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 - -use chrono::{DateTime, Days, Utc}; -use serde_json::Value; -use tracing::error; - -use crate::config::Config; -use crate::db::entity::federated_user; -use crate::db::entity::local_user; -use crate::db::entity::nonlocal_user; -use crate::db::entity::password; -use crate::db::entity::user; -use crate::db::entity::user_option; - -use crate::identity::types::*; - -pub fn get_user_builder>( - user: &user::Model, - opts: O, -) -> UserResponseBuilder { - let mut user_builder: UserResponseBuilder = UserResponseBuilder::default(); - user_builder.id(user.id.clone()); - user_builder.domain_id(user.domain_id.clone()); - // TODO: default enabled logic - user_builder.enabled(user.enabled.unwrap_or(false)); - if let Some(extra) = &user.extra { - user_builder.extra( - serde_json::from_str::(extra) - .inspect_err(|e| error!("failed to deserialize user extra: {e}")) - .unwrap_or_default(), - ); - } - - user_builder.options(UserOptions::from_iter(opts)); - - user_builder -} - -pub fn get_local_user_builder< - O: IntoIterator, - P: IntoIterator, ->( - conf: &Config, - user: &user::Model, - data: local_user::Model, - passwords: Option

, - opts: O, -) -> UserResponseBuilder { - let mut user_builder: UserResponseBuilder = get_user_builder(user, opts); - user_builder.name(data.name.clone()); - if let Some(password_expires_days) = conf.security_compliance.password_expires_days - && let Some(pass) = passwords - && let (Some(current_password), Some(options)) = - (pass.into_iter().next(), user_builder.get_options()) - && let Some(false) = options.ignore_password_expiry.or(Some(false)) - && let Some(dt) = DateTime::from_timestamp_micros(current_password.created_at_int) - .unwrap_or(DateTime::from_naive_utc_and_offset( - current_password.created_at, - Utc, - )) - .checked_add_days(Days::new(password_expires_days)) - { - user_builder.password_expires_at(dt); - } - user_builder -} - -pub fn get_nonlocal_user_builder>( - user: &user::Model, - data: nonlocal_user::Model, - opts: O, -) -> UserResponseBuilder { - let mut user_builder: UserResponseBuilder = get_user_builder(user, opts); - user_builder.name(data.name.clone()); - user_builder -} - -pub fn get_federated_user_builder< - O: IntoIterator, - F: IntoIterator, ->( - user: &user::Model, - data: F, - opts: O, -) -> UserResponseBuilder { - let mut user_builder: UserResponseBuilder = get_user_builder(user, opts); - let mut feds: Vec = Vec::new(); - if let Some(first) = data.into_iter().next() { - if let Some(name) = first.display_name { - user_builder.name(name.clone()); - } - - let mut fed = FederationBuilder::default(); - fed.idp_id(first.idp_id.clone()); - fed.unique_id(first.unique_id.clone()); - let protocol = FederationProtocol { - protocol_id: first.protocol_id.clone(), - unique_id: first.unique_id.clone(), - }; - fed.protocols(vec![protocol]); - if let Ok(fed_obj) = fed.build() { - feds.push(fed_obj); - } - } - user_builder.federated(feds); - user_builder -} diff --git a/src/identity/backends/sql/federated_user.rs b/src/identity/backends/sql/federated_user.rs index 66378b97..aa1a8bfd 100644 --- a/src/identity/backends/sql/federated_user.rs +++ b/src/identity/backends/sql/federated_user.rs @@ -12,47 +12,45 @@ // // SPDX-License-Identifier: Apache-2.0 -use sea_orm::DatabaseConnection; -use sea_orm::entity::*; -use sea_orm::query::*; +use super::user; +use crate::db::entity::{ + federated_user as db_federated_user, user as db_user, user_option as db_user_option, +}; +use crate::identity::types::*; -use crate::config::Config; -use crate::db::entity::{federated_user, prelude::FederatedUser}; -use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; +mod create; +mod find; -pub async fn create( - _conf: &Config, - db: &DatabaseConnection, - federation: A, -) -> Result -where - A: Into, -{ - let db_user: federated_user::Model = federation - .into() - .insert(db) - .await - .map_err(|err| db_err(err, "persisting federated user data"))?; +pub use create::create; +pub use find::find_by_idp_and_unique_id; - Ok(db_user) -} +pub fn get_federated_user_builder< + O: IntoIterator, + F: IntoIterator, +>( + user: &db_user::Model, + data: F, + opts: O, +) -> UserResponseBuilder { + let mut user_builder: UserResponseBuilder = user::get_user_builder(user, opts); + let mut feds: Vec = Vec::new(); + if let Some(first) = data.into_iter().next() { + if let Some(name) = first.display_name { + user_builder.name(name.clone()); + } -/// Get federated user entry by the idp_id and the unique_id -pub async fn find_by_idp_and_unique_id, U: AsRef>( - _conf: &Config, - db: &DatabaseConnection, - idp_id: I, - unique_id: U, -) -> Result, IdentityDatabaseError> { - Ok(FederatedUser::find() - .filter(federated_user::Column::IdpId.eq(idp_id.as_ref())) - .filter(federated_user::Column::UniqueId.eq(unique_id.as_ref())) - .all(db) - .await - .map_err(|err| db_err(err, "searching federated user by the idp and unique id"))? - .first() - .cloned()) + let mut fed = FederationBuilder::default(); + fed.idp_id(first.idp_id.clone()); + fed.unique_id(first.unique_id.clone()); + let protocol = FederationProtocol { + protocol_id: first.protocol_id.clone(), + unique_id: first.unique_id.clone(), + }; + fed.protocols(vec![protocol]); + if let Ok(fed_obj) = fed.build() { + feds.push(fed_obj); + } + } + user_builder.federated(feds); + user_builder } - -#[cfg(test)] -mod tests {} diff --git a/src/identity/backends/sql/federated_user/create.rs b/src/identity/backends/sql/federated_user/create.rs new file mode 100644 index 00000000..bff62e41 --- /dev/null +++ b/src/identity/backends/sql/federated_user/create.rs @@ -0,0 +1,40 @@ +// 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::config::Config; +use crate::db::entity::federated_user; +use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; + +pub async fn create( + _conf: &Config, + db: &DatabaseConnection, + federation: A, +) -> Result +where + A: Into, +{ + let db_user: federated_user::Model = federation + .into() + .insert(db) + .await + .map_err(|err| db_err(err, "persisting federated user data"))?; + + Ok(db_user) +} + +#[cfg(test)] +mod tests {} diff --git a/src/identity/backends/sql/federated_user/find.rs b/src/identity/backends/sql/federated_user/find.rs new file mode 100644 index 00000000..18effbcc --- /dev/null +++ b/src/identity/backends/sql/federated_user/find.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 sea_orm::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; + +use crate::config::Config; +use crate::db::entity::{federated_user, prelude::FederatedUser}; +use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; + +/// Get federated user entry by the idp_id and the unique_id +pub async fn find_by_idp_and_unique_id, U: AsRef>( + _conf: &Config, + db: &DatabaseConnection, + idp_id: I, + unique_id: U, +) -> Result, IdentityDatabaseError> { + Ok(FederatedUser::find() + .filter(federated_user::Column::IdpId.eq(idp_id.as_ref())) + .filter(federated_user::Column::UniqueId.eq(unique_id.as_ref())) + .all(db) + .await + .map_err(|err| db_err(err, "searching federated user by the idp and unique id"))? + .first() + .cloned()) +} + +#[cfg(test)] +mod tests {} diff --git a/src/identity/backends/sql/local_user.rs b/src/identity/backends/sql/local_user.rs index deb72927..f6ca6bd6 100644 --- a/src/identity/backends/sql/local_user.rs +++ b/src/identity/backends/sql/local_user.rs @@ -12,152 +12,49 @@ // // SPDX-License-Identifier: Apache-2.0 -use sea_orm::DatabaseConnection; -use sea_orm::entity::*; -use sea_orm::query::*; -use std::collections::HashMap; +use chrono::{DateTime, Days, Utc}; +use super::user; use crate::config::Config; use crate::db::entity::{ - local_user, password, - prelude::{LocalUser, Password}, + local_user as db_local_user, password as db_password, user as db_user, + user_option as db_user_option, }; -use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; -use crate::identity::types::UserCreate; +use crate::identity::types::*; -/// Load local user record with passwords from database -pub async fn load_local_user_with_passwords, S2: AsRef, S3: AsRef>( - db: &DatabaseConnection, - user_id: Option, - name: Option, - domain_id: Option, -) -> Result< - Option<(local_user::Model, impl IntoIterator)>, - IdentityDatabaseError, -> { - let mut select = LocalUser::find(); - if let Some(user_id) = user_id { - select = select.filter(local_user::Column::UserId.eq(user_id.as_ref())) - } else { - select = select - .filter( - local_user::Column::Name.eq(name - .ok_or(IdentityDatabaseError::UserIdOrNameWithDomain)? - .as_ref()), - ) - .filter( - local_user::Column::DomainId.eq(domain_id - .ok_or(IdentityDatabaseError::UserIdOrNameWithDomain)? - .as_ref()), - ); - } - let results: Vec<(local_user::Model, Vec)> = select - .find_with_related(Password) - .order_by(password::Column::CreatedAtInt, Order::Desc) - .all(db) - .await - .map_err(|err| db_err(err, "fetching user with passwords"))?; - Ok(results.first().cloned()) -} - -/// Fetch passwords for list of optional local user ids -/// -/// Returns vector of optional vectors with passwords in the same order as requested -/// keeping None in place where local_user was empty. -pub async fn load_local_users_passwords>>( - db: &DatabaseConnection, - user_ids: L, -) -> Result>>, IdentityDatabaseError> { - let ids: Vec> = user_ids.into_iter().collect(); - // Collect local user IDs that we need to query - let keys: Vec = ids.iter().filter_map(Option::as_ref).copied().collect(); - - // Fetch passwords for the local users by keys - let passwords: Vec = Password::find() - .filter(password::Column::LocalUserId.is_in(keys.clone())) - .order_by(password::Column::CreatedAtInt, Order::Desc) - .all(db) - .await - .map_err(|err| db_err(err, "fetching user passwords"))?; - - // Prepare hashmap of passwords per local_user_id from requested users - let mut hashmap: HashMap> = - keys.iter().fold(HashMap::new(), |mut acc, key| { - acc.insert(*key, Vec::new()); - acc - }); - - // Collect passwords into hashmap by the local_user_id - passwords.into_iter().for_each(|item| { - hashmap - .entry(item.local_user_id) - .and_modify(|e| e.push(item.clone())) - .or_insert_with(|| Vec::from([item])); - }); - - // Prepare final result keeping the order of the requested local_users - // with vec of passwords for the ones - let result: Vec>> = ids - .iter() - .map(|lid| lid.map(|x| hashmap.get(&x).cloned()).unwrap_or_default()) - .collect(); +mod create; +mod get; +mod load; - Ok(result) -} +pub use create::create; +pub use load::load_local_user_with_passwords; +pub use load::load_local_users_passwords; -pub async fn create( +pub fn get_local_user_builder< + O: IntoIterator, + P: IntoIterator, +>( conf: &Config, - db: &DatabaseConnection, - user: &UserCreate, -) -> Result { - let mut entry = local_user::ActiveModel { - id: NotSet, - user_id: Set(user.id.clone()), - domain_id: Set(user.domain_id.clone()), - name: Set(user.name.clone()), - failed_auth_count: NotSet, - failed_auth_at: NotSet, - }; - // Set failed_auth_count to 0 if compliance disabling is on - if let Some(true) = &user.enabled - && conf - .security_compliance - .disable_user_account_days_inactive - .is_some() + user: &db_user::Model, + data: db_local_user::Model, + passwords: Option

, + opts: O, +) -> UserResponseBuilder { + let mut user_builder: UserResponseBuilder = user::get_user_builder(user, opts); + user_builder.name(data.name.clone()); + if let Some(password_expires_days) = conf.security_compliance.password_expires_days + && let Some(pass) = passwords + && let (Some(current_password), Some(options)) = + (pass.into_iter().next(), user_builder.get_options()) + && let Some(false) = options.ignore_password_expiry.or(Some(false)) + && let Some(dt) = DateTime::from_timestamp_micros(current_password.created_at_int) + .unwrap_or(DateTime::from_naive_utc_and_offset( + current_password.created_at, + Utc, + )) + .checked_add_days(Days::new(password_expires_days)) { - entry.failed_auth_count = Set(Some(0)); + user_builder.password_expires_at(dt); } - - let db_user: local_user::Model = entry - .insert(db) - .await - .map_err(|err| db_err(err, "inserting new user record"))?; - - Ok(db_user) -} - -pub async fn get_by_name_and_domain, D: AsRef>( - _conf: &Config, - db: &DatabaseConnection, - name: N, - domain_id: D, -) -> Result, IdentityDatabaseError> { - LocalUser::find() - .filter(local_user::Column::Name.eq(name.as_ref())) - .filter(local_user::Column::DomainId.eq(domain_id.as_ref())) - .one(db) - .await - .map_err(|err| db_err(err, "searching user by name and domain")) -} - -pub async fn get_by_user_id>( - _conf: &Config, - db: &DatabaseConnection, - user_id: U, -) -> Result, IdentityDatabaseError> { - LocalUser::find() - .filter(local_user::Column::UserId.eq(user_id.as_ref())) - .one(db) - .await - .map_err(|err| db_err(err, "fetching the user by ID")) + user_builder } diff --git a/src/identity/backends/sql/local_user/create.rs b/src/identity/backends/sql/local_user/create.rs new file mode 100644 index 00000000..f9875dda --- /dev/null +++ b/src/identity/backends/sql/local_user/create.rs @@ -0,0 +1,52 @@ +// 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::config::Config; +use crate::db::entity::local_user; +use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; +use crate::identity::types::UserCreate; + +pub async fn create( + conf: &Config, + db: &DatabaseConnection, + user: &UserCreate, +) -> Result { + let mut entry = local_user::ActiveModel { + id: NotSet, + user_id: Set(user.id.clone()), + domain_id: Set(user.domain_id.clone()), + name: Set(user.name.clone()), + failed_auth_count: NotSet, + failed_auth_at: NotSet, + }; + // Set failed_auth_count to 0 if compliance disabling is on + if let Some(true) = &user.enabled + && conf + .security_compliance + .disable_user_account_days_inactive + .is_some() + { + entry.failed_auth_count = Set(Some(0)); + } + + let db_user: local_user::Model = entry + .insert(db) + .await + .map_err(|err| db_err(err, "inserting new user record"))?; + + Ok(db_user) +} diff --git a/src/identity/backends/sql/local_user/get.rs b/src/identity/backends/sql/local_user/get.rs new file mode 100644 index 00000000..bb2c57a8 --- /dev/null +++ b/src/identity/backends/sql/local_user/get.rs @@ -0,0 +1,47 @@ +// 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 sea_orm::query::*; + +use crate::config::Config; +use crate::db::entity::{local_user, prelude::LocalUser}; +use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; + +pub async fn get_by_name_and_domain, D: AsRef>( + _conf: &Config, + db: &DatabaseConnection, + name: N, + domain_id: D, +) -> Result, IdentityDatabaseError> { + LocalUser::find() + .filter(local_user::Column::Name.eq(name.as_ref())) + .filter(local_user::Column::DomainId.eq(domain_id.as_ref())) + .one(db) + .await + .map_err(|err| db_err(err, "searching user by name and domain")) +} + +pub async fn get_by_user_id>( + _conf: &Config, + db: &DatabaseConnection, + user_id: U, +) -> Result, IdentityDatabaseError> { + LocalUser::find() + .filter(local_user::Column::UserId.eq(user_id.as_ref())) + .one(db) + .await + .map_err(|err| db_err(err, "fetching the user by ID")) +} diff --git a/src/identity/backends/sql/local_user/load.rs b/src/identity/backends/sql/local_user/load.rs new file mode 100644 index 00000000..6eb4c6e6 --- /dev/null +++ b/src/identity/backends/sql/local_user/load.rs @@ -0,0 +1,104 @@ +// 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 sea_orm::query::*; +use std::collections::HashMap; + +use crate::db::entity::{ + local_user, password, + prelude::{LocalUser, Password}, +}; +use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; + +/// Load local user record with passwords from database +pub async fn load_local_user_with_passwords, S2: AsRef, S3: AsRef>( + db: &DatabaseConnection, + user_id: Option, + name: Option, + domain_id: Option, +) -> Result< + Option<(local_user::Model, impl IntoIterator)>, + IdentityDatabaseError, +> { + let mut select = LocalUser::find(); + if let Some(user_id) = user_id { + select = select.filter(local_user::Column::UserId.eq(user_id.as_ref())) + } else { + select = select + .filter( + local_user::Column::Name.eq(name + .ok_or(IdentityDatabaseError::UserIdOrNameWithDomain)? + .as_ref()), + ) + .filter( + local_user::Column::DomainId.eq(domain_id + .ok_or(IdentityDatabaseError::UserIdOrNameWithDomain)? + .as_ref()), + ); + } + let results: Vec<(local_user::Model, Vec)> = select + .find_with_related(Password) + .order_by(password::Column::CreatedAtInt, Order::Desc) + .all(db) + .await + .map_err(|err| db_err(err, "fetching user with passwords"))?; + Ok(results.first().cloned()) +} + +/// Fetch passwords for list of optional local user ids +/// +/// Returns vector of optional vectors with passwords in the same order as requested +/// keeping None in place where local_user was empty. +pub async fn load_local_users_passwords>>( + db: &DatabaseConnection, + user_ids: L, +) -> Result>>, IdentityDatabaseError> { + let ids: Vec> = user_ids.into_iter().collect(); + // Collect local user IDs that we need to query + let keys: Vec = ids.iter().filter_map(Option::as_ref).copied().collect(); + + // Fetch passwords for the local users by keys + let passwords: Vec = Password::find() + .filter(password::Column::LocalUserId.is_in(keys.clone())) + .order_by(password::Column::CreatedAtInt, Order::Desc) + .all(db) + .await + .map_err(|err| db_err(err, "fetching user passwords"))?; + + // Prepare hashmap of passwords per local_user_id from requested users + let mut hashmap: HashMap> = + keys.iter().fold(HashMap::new(), |mut acc, key| { + acc.insert(*key, Vec::new()); + acc + }); + + // Collect passwords into hashmap by the local_user_id + passwords.into_iter().for_each(|item| { + hashmap + .entry(item.local_user_id) + .and_modify(|e| e.push(item.clone())) + .or_insert_with(|| Vec::from([item])); + }); + + // Prepare final result keeping the order of the requested local_users + // with vec of passwords for the ones + let result: Vec>> = ids + .iter() + .map(|lid| lid.map(|x| hashmap.get(&x).cloned()).unwrap_or_default()) + .collect(); + + Ok(result) +} diff --git a/src/identity/backends/sql/nonlocal_user.rs b/src/identity/backends/sql/nonlocal_user.rs new file mode 100644 index 00000000..3094241a --- /dev/null +++ b/src/identity/backends/sql/nonlocal_user.rs @@ -0,0 +1,29 @@ +// 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 super::user; +use crate::db::entity::{ + nonlocal_user as db_nonlocal_user, user as db_user, user_option as db_user_option, +}; +use crate::identity::types::*; + +pub fn get_nonlocal_user_builder>( + user: &db_user::Model, + data: db_nonlocal_user::Model, + opts: O, +) -> UserResponseBuilder { + let mut user_builder: UserResponseBuilder = user::get_user_builder(user, opts); + user_builder.name(data.name.clone()); + user_builder +} diff --git a/src/identity/backends/sql/password.rs b/src/identity/backends/sql/password.rs index 631c0a17..688839e4 100644 --- a/src/identity/backends/sql/password.rs +++ b/src/identity/backends/sql/password.rs @@ -11,38 +11,6 @@ // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 +mod create; -use chrono::{DateTime, Local, Utc}; -use sea_orm::DatabaseConnection; -use sea_orm::entity::*; - -use crate::db::entity::password; -use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; - -pub async fn create>( - db: &DatabaseConnection, - local_user_id: i32, - password_hash: S, - expires_at: Option>, -) -> Result { - let now = Local::now().naive_utc(); - let mut entry = password::ActiveModel { - id: NotSet, - local_user_id: Set(local_user_id), - self_service: Set(false), - expires_at: NotSet, - password_hash: Set(Some(password_hash.as_ref().into())), - created_at: Set(now), - created_at_int: Set(now.and_utc().timestamp_micros()), - expires_at_int: NotSet, - }; - if let Some(expire) = expires_at { - entry.expires_at = Set(Some(expire.naive_utc())); - entry.expires_at_int = Set(Some(expire.timestamp_micros())); - } - let db_entry: password::Model = entry - .insert(db) - .await - .map_err(|err| db_err(err, "inserting new password record"))?; - Ok(db_entry) -} +pub use create::create; diff --git a/src/identity/backends/sql/password/create.rs b/src/identity/backends/sql/password/create.rs new file mode 100644 index 00000000..631c0a17 --- /dev/null +++ b/src/identity/backends/sql/password/create.rs @@ -0,0 +1,48 @@ +// 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::{DateTime, Local, Utc}; +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; + +use crate::db::entity::password; +use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; + +pub async fn create>( + db: &DatabaseConnection, + local_user_id: i32, + password_hash: S, + expires_at: Option>, +) -> Result { + let now = Local::now().naive_utc(); + let mut entry = password::ActiveModel { + id: NotSet, + local_user_id: Set(local_user_id), + self_service: Set(false), + expires_at: NotSet, + password_hash: Set(Some(password_hash.as_ref().into())), + created_at: Set(now), + created_at_int: Set(now.and_utc().timestamp_micros()), + expires_at_int: NotSet, + }; + if let Some(expire) = expires_at { + entry.expires_at = Set(Some(expire.naive_utc())); + entry.expires_at_int = Set(Some(expire.timestamp_micros())); + } + let db_entry: password::Model = entry + .insert(db) + .await + .map_err(|err| db_err(err, "inserting new password record"))?; + Ok(db_entry) +} diff --git a/src/identity/backends/sql/user.rs b/src/identity/backends/sql/user.rs index 8f35a06f..dd4e7b86 100644 --- a/src/identity/backends/sql/user.rs +++ b/src/identity/backends/sql/user.rs @@ -12,114 +12,88 @@ // // SPDX-License-Identifier: Apache-2.0 -use chrono::Local; -use sea_orm::DatabaseConnection; -use sea_orm::entity::*; -use serde_json::json; +use serde_json::Value; +use tracing::error; -use crate::config::Config; -use crate::db::entity::{prelude::User as DbUser, user}; -use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; -use crate::identity::types::UserCreate; +use crate::db::entity::{user as db_user, user_option as db_user_option}; +use crate::identity::types::*; -pub async fn get>( - db: &DatabaseConnection, - user_id: U, -) -> Result, IdentityDatabaseError> { - DbUser::find_by_id(user_id.as_ref()) - .one(db) - .await - .map_err(|err| db_err(err, "fetching user by ID")) -} - -pub(super) async fn create( - conf: &Config, - db: &DatabaseConnection, - user: &UserCreate, -) -> Result { - let now = Local::now().naive_utc(); - // Set last_active to now if compliance disabling is on - let last_active_at = if let Some(true) = &user.enabled { - if conf - .security_compliance - .disable_user_account_days_inactive - .is_some() - { - Set(Some(now.date())) - } else { - NotSet - } - } else { - NotSet - }; +mod create; +mod delete; +mod get; +mod list; - let entry: user::ActiveModel = user::ActiveModel { - id: Set(user.id.clone()), - enabled: Set(user.enabled), - extra: Set(Some(serde_json::to_string( - // For keystone it is important to have at least "{}" - &user.extra.as_ref().or(Some(&json!({}))), - )?)), - default_project_id: Set(user.default_project_id.clone()), - last_active_at, - created_at: Set(Some(now)), - domain_id: Set(user.domain_id.clone()), - }; - let db_user: user::Model = entry - .insert(db) - .await - .map_err(|err| db_err(err, "inserting user entry"))?; - Ok(db_user) -} +pub use create::create; +pub use delete::delete; +pub use get::get; +pub(super) use get::get_main_entry; +pub use list::list; -pub async fn delete>( - _conf: &Config, - db: &DatabaseConnection, - user_id: U, -) -> Result<(), IdentityDatabaseError> { - let res = DbUser::delete_by_id(user_id.as_ref()) - .exec(db) - .await - .map_err(|err| db_err(err, "deleting the user record"))?; - if res.rows_affected == 1 { - Ok(()) - } else { - Err(IdentityDatabaseError::UserNotFound( - user_id.as_ref().to_string(), - )) +pub fn get_user_builder>( + user: &db_user::Model, + opts: O, +) -> UserResponseBuilder { + let mut user_builder: UserResponseBuilder = UserResponseBuilder::default(); + user_builder.id(user.id.clone()); + user_builder.domain_id(user.domain_id.clone()); + // TODO: default enabled logic + user_builder.enabled(user.enabled.unwrap_or(false)); + if let Some(extra) = &user.extra { + user_builder.extra( + serde_json::from_str::(extra) + .inspect_err(|e| error!("failed to deserialize user extra: {e}")) + .unwrap_or_default(), + ); } + + user_builder.options(UserOptions::from_iter(opts)); + + user_builder } #[cfg(test)] mod tests { - #![allow(clippy::derivable_impls)] - - use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + use chrono::Local; - use crate::identity::Config; - - use super::*; + use crate::db::entity::{ + local_user as db_local_user, password as db_password, user as db_user, + }; - #[tokio::test] - async fn test_delete() { - // Create MockDatabase with mock query results - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_exec_results([MockExecResult { - rows_affected: 1, - ..Default::default() - }]) - .into_connection(); - let config = Config::default(); + pub(super) fn get_user_mock>(user_id: U) -> db_user::Model { + db_user::Model { + id: user_id.as_ref().into(), + domain_id: "foo_domain".into(), + enabled: Some(true), + ..Default::default() + } + } - delete(&config, &db, "id").await.unwrap(); - // Checking transaction log - assert_eq!( - db.into_transaction_log(), - [Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"DELETE FROM "user" WHERE "user"."id" = $1"#, - ["id".into()] - ),] - ); + pub(super) fn get_local_user_with_password_mock>( + user_id: U, + cnt_password: usize, + ) -> Vec<(db_local_user::Model, db_password::Model)> { + let lu = db_local_user::Model { + user_id: user_id.as_ref().into(), + 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(db_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() } } diff --git a/src/identity/backends/sql/user/create.rs b/src/identity/backends/sql/user/create.rs new file mode 100644 index 00000000..6215af50 --- /dev/null +++ b/src/identity/backends/sql/user/create.rs @@ -0,0 +1,154 @@ +// 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 serde_json::json; + +use crate::config::Config; +use crate::db::entity::{ + federated_user as db_federated_user, password as db_password, user as db_user, +}; +use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; +use crate::identity::password_hashing; +use crate::identity::types::*; + +use super::super::federated_user; +use super::super::local_user; +use super::super::password; + +async fn create_main( + conf: &Config, + db: &DatabaseConnection, + user: &UserCreate, +) -> Result { + let now = Local::now().naive_utc(); + // Set last_active to now if compliance disabling is on + let last_active_at = if let Some(true) = &user.enabled { + if conf + .security_compliance + .disable_user_account_days_inactive + .is_some() + { + Set(Some(now.date())) + } else { + NotSet + } + } else { + NotSet + }; + + let entry: db_user::ActiveModel = db_user::ActiveModel { + id: Set(user.id.clone()), + enabled: Set(user.enabled), + extra: Set(Some(serde_json::to_string( + // For keystone it is important to have at least "{}" + &user.extra.as_ref().or(Some(&json!({}))), + )?)), + default_project_id: Set(user.default_project_id.clone()), + last_active_at, + created_at: Set(Some(now)), + domain_id: Set(user.domain_id.clone()), + }; + let db_user: db_user::Model = entry + .insert(db) + .await + .map_err(|err| db_err(err, "inserting user entry"))?; + Ok(db_user) +} + +pub async fn create( + conf: &Config, + db: &DatabaseConnection, + user: UserCreate, +) -> Result { + let main_user = create_main(conf, db, &user).await?; + if let Some(federation_data) = &user.federated { + let mut federated_entities: Vec = Vec::new(); + for federated_user in federation_data { + if federated_user.protocols.is_empty() { + federated_entities.push( + federated_user::create( + conf, + db, + db_federated_user::ActiveModel { + id: NotSet, + user_id: Set(user.id.clone()), + idp_id: Set(federated_user.idp_id.clone()), + protocol_id: Set("oidc".into()), + unique_id: Set(federated_user.unique_id.clone()), + display_name: Set(Some(user.name.clone())), + }, + ) + .await?, + ); + } else { + for proto in &federated_user.protocols { + federated_entities.push( + federated_user::create( + conf, + db, + db_federated_user::ActiveModel { + id: NotSet, + user_id: Set(user.id.clone()), + idp_id: Set(federated_user.idp_id.clone()), + protocol_id: Set(proto.protocol_id.clone()), + unique_id: Set(proto.unique_id.clone()), + display_name: Set(Some(user.name.clone())), + }, + ) + .await?, + ); + } + } + } + + let builder = + federated_user::get_federated_user_builder(&main_user, federated_entities, Vec::new()); + + Ok(builder.build()?) + } else { + // Local user + let local_user = local_user::create(conf, db, &user).await?; + let mut passwords: Vec = Vec::new(); + if let Some(password) = &user.password { + let password_entry = password::create( + db, + local_user.id, + password_hashing::hash_password(conf, password).await?, + None, + ) + .await?; + + passwords.push(password_entry); + } + Ok(local_user::get_local_user_builder( + conf, + &main_user, + local_user, + Some(passwords), + Vec::new(), + ) + .build()?) + } + // let ub = common::get_user_builder(&main_user, Vec::new()).build()?; + + // Ok(ub) +} + +#[cfg(test)] +mod tests { + // use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; +} diff --git a/src/identity/backends/sql/user/delete.rs b/src/identity/backends/sql/user/delete.rs new file mode 100644 index 00000000..676bb939 --- /dev/null +++ b/src/identity/backends/sql/user/delete.rs @@ -0,0 +1,72 @@ +// 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::config::Config; +use crate::db::entity::prelude::User as DbUser; +use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; + +pub async fn delete>( + _conf: &Config, + db: &DatabaseConnection, + user_id: U, +) -> Result<(), IdentityDatabaseError> { + let res = DbUser::delete_by_id(user_id.as_ref()) + .exec(db) + .await + .map_err(|err| db_err(err, "deleting the user record"))?; + if res.rows_affected == 1 { + Ok(()) + } else { + Err(IdentityDatabaseError::UserNotFound( + user_id.as_ref().to_string(), + )) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::derivable_impls)] + + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + + use crate::identity::Config; + + use super::*; + + #[tokio::test] + async fn test_delete() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_exec_results([MockExecResult { + rows_affected: 1, + ..Default::default() + }]) + .into_connection(); + let config = Config::default(); + + delete(&config, &db, "id").await.unwrap(); + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"DELETE FROM "user" WHERE "user"."id" = $1"#, + ["id".into()] + ),] + ); + } +} diff --git a/src/identity/backends/sql/user/get.rs b/src/identity/backends/sql/user/get.rs new file mode 100644 index 00000000..f2e65219 --- /dev/null +++ b/src/identity/backends/sql/user/get.rs @@ -0,0 +1,197 @@ +// 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 super::super::federated_user; +use super::super::local_user; +use super::super::nonlocal_user; +use crate::config::Config; +use crate::db::entity::{ + prelude::{FederatedUser, NonlocalUser, User as DbUser, UserOption}, + user as db_user, +}; +use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; +use crate::identity::types::*; + +pub async fn get_main_entry>( + db: &DatabaseConnection, + user_id: U, +) -> Result, IdentityDatabaseError> { + DbUser::find_by_id(user_id.as_ref()) + .one(db) + .await + .map_err(|err| db_err(err, "fetching user by ID")) +} + +pub async fn get( + conf: &Config, + db: &DatabaseConnection, + user_id: &str, +) -> Result, IdentityDatabaseError> { + let user_select = DbUser::find_by_id(user_id); + + let user_entry: Option = user_select + .one(db) + .await + .map_err(|err| db_err(err, "fetching the user data"))?; + + if let Some(user) = user_entry { + let (user_opts, local_user_with_passwords) = tokio::join!( + user.find_related(UserOption).all(db), + local_user::load_local_user_with_passwords( + db, + Some(&user_id), + None::<&str>, + None::<&str>, + ) + ); + + let user_builder: UserResponseBuilder = match local_user_with_passwords? { + Some(local_user_with_passwords) => local_user::get_local_user_builder( + conf, + &user, + local_user_with_passwords.0, + Some(local_user_with_passwords.1), + user_opts.map_err(|err| db_err(err, "fetching user options"))?, + ), + _ => match user + .find_related(NonlocalUser) + .one(db) + .await + .map_err(|err| db_err(err, "fetching nonlocal user data"))? + { + Some(nonlocal_user) => nonlocal_user::get_nonlocal_user_builder( + &user, + nonlocal_user, + user_opts.map_err(|err| db_err(err, "fetching user options"))?, + ), + _ => { + let federated_user = user + .find_related(FederatedUser) + .all(db) + .await + .map_err(|err| db_err(err, "fetching federated user data"))?; + if !federated_user.is_empty() { + federated_user::get_federated_user_builder( + &user, + federated_user, + user_opts.map_err(|err| db_err(err, "fetching user options"))?, + ) + } else { + return Err(IdentityDatabaseError::MalformedUser(user_id.to_string()))?; + } + } + }, + }; + + return Ok(Some(user_builder.build()?)); + } + + Ok(None) +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use crate::config::Config; + use crate::db::entity::user_option as db_user_option; + + use super::super::tests::*; + use super::*; + + #[tokio::test] + async fn test_get() { //// Create MockDatabase with mock query results + //let db = MockDatabase::new(DatabaseBackend::Postgres) + // .append_exec_results([MockExecResult { + // rows_affected: 1, + // ..Default::default() + // }]) + // .into_connection(); + //let config = Config::default(); + + //delete(&config, &db, "id").await.unwrap(); + //// Checking transaction log + //assert_eq!( + // db.into_transaction_log(), + // [Transaction::from_sql_and_values( + // DatabaseBackend::Postgres, + // r#"DELETE FROM "user" WHERE "user"."id" = $1"#, + // ["id".into()] + // ),] + //); + } + + #[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")], + ]) + .append_query_results([ + //// Second query result - user options + vec![db_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", 1), + ]) + .into_connection(); + let config = Config::default(); + assert_eq!( + get(&config, &db, "1").await.unwrap().unwrap(), + UserResponse { + 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, "password"."created_at_int" DESC"#, + ["1".into()] + ), + ] + ); + } +} diff --git a/src/identity/backends/sql/user/list.rs b/src/identity/backends/sql/user/list.rs new file mode 100644 index 00000000..91999420 --- /dev/null +++ b/src/identity/backends/sql/user/list.rs @@ -0,0 +1,121 @@ +// 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 sea_orm::query::*; + +use super::super::federated_user; +use super::super::local_user; +use super::super::nonlocal_user; +use crate::config::Config; +use crate::db::entity::{ + federated_user as db_federated_user, local_user as db_local_user, + nonlocal_user as db_nonlocal_user, password as db_password, + prelude::{FederatedUser, LocalUser, NonlocalUser, User as DbUser, UserOption}, + user as db_user, +}; +use crate::identity::backends::error::{IdentityDatabaseError, db_err}; +use crate::identity::types::*; + +pub async fn list( + conf: &Config, + db: &DatabaseConnection, + params: &UserListParameters, +) -> Result, IdentityDatabaseError> { + // Prepare basic selects + let mut user_select = DbUser::find(); + let mut local_user_select = LocalUser::find(); + let mut nonlocal_user_select = NonlocalUser::find(); + let mut federated_user_select = FederatedUser::find(); + + if let Some(domain_id) = ¶ms.domain_id { + user_select = user_select.filter(db_user::Column::DomainId.eq(domain_id)); + } + if let Some(name) = ¶ms.name { + local_user_select = local_user_select.filter(db_local_user::Column::Name.eq(name)); + nonlocal_user_select = nonlocal_user_select.filter(db_nonlocal_user::Column::Name.eq(name)); + federated_user_select = + federated_user_select.filter(db_federated_user::Column::DisplayName.eq(name)); + } + + let db_users: Vec = user_select + .all(db) + .await + .map_err(|err| db_err(err, "fetching users data"))?; + + let (user_opts, local_users, nonlocal_users, federated_users) = tokio::join!( + db_users.load_many(UserOption, db), + db_users.load_one(local_user_select, db), + db_users.load_one(nonlocal_user_select, db), + db_users.load_many(federated_user_select, db) + ); + + let locals = local_users.map_err(|err| db_err(err, "fetching local users data"))?; + + let local_users_passwords: Vec>> = + local_user::load_local_users_passwords(db, locals.iter().cloned().map(|u| u.map(|x| x.id))) + .await?; + + let mut results: Vec = Vec::new(); + for (u, (o, (l, (p, (n, f))))) in db_users.into_iter().zip( + user_opts + .map_err(|err| db_err(err, "fetching user options"))? + .into_iter() + .zip( + locals.into_iter().zip( + local_users_passwords.into_iter().zip( + nonlocal_users + .map_err(|err| db_err(err, "fetching nonlocal users data"))? + .into_iter() + .zip( + federated_users + .map_err(|err| db_err(err, "fetching federated users data"))? + .into_iter(), + ), + ), + ), + ), + ) { + if l.is_none() && n.is_none() && f.is_empty() { + continue; + } + let user_builder: UserResponseBuilder = if let Some(local) = l { + local_user::get_local_user_builder(conf, &u, local, p.map(|x| x.into_iter()), o) + } else if let Some(nonlocal) = n { + nonlocal_user::get_nonlocal_user_builder(&u, nonlocal, o) + } else if !f.is_empty() { + federated_user::get_federated_user_builder(&u, f, o) + } else { + return Err(IdentityDatabaseError::MalformedUser(u.id))?; + }; + results.push(user_builder.build()?); + } + + //let select: Vec<(String, Option, )> = DbUser::find() + //let select = DbUser::find(); + //let select = Prefixer::new(DbUser::find().select_only()) + // .add_columns(DbUser) + // .add_columns(LocalUser) + // .add_columns(NonlocalUser) + // .selector + // .left_join(LocalUser) + // .left_join(NonlocalUser) + // //.left_join(FederatedUser) + // .into_model::() + // .all(db) + // .await + // .unwrap(); + Ok(results) +} diff --git a/src/identity/backends/sql/user_group.rs b/src/identity/backends/sql/user_group.rs index b0065c7e..b398385f 100644 --- a/src/identity/backends/sql/user_group.rs +++ b/src/identity/backends/sql/user_group.rs @@ -12,84 +12,21 @@ // // SPDX-License-Identifier: Apache-2.0 -use sea_orm::DatabaseConnection; -use sea_orm::entity::*; -use sea_orm::query::*; -use std::collections::BTreeSet; - -use crate::db::entity::{prelude::UserGroupMembership, user_group_membership}; -use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; - mod add; mod list; mod remove; +mod set; + pub use add::{add_user_to_group, add_users_to_groups}; pub use list::list_user_groups; pub use remove::{remove_user_from_group, remove_user_from_groups}; - -/// Set user group memberships. -/// -/// Add user to the groups it should be in and remove from the groups where the user is currently -/// member of, but should not be. This is only incremental operation and is not deleting group -/// membership where the user should stay. -pub async fn set_user_groups( - db: &DatabaseConnection, - user_id: U, - group_ids: I, -) -> Result<(), IdentityDatabaseError> -where - I: IntoIterator, - U: AsRef, - G: AsRef, -{ - // Use BTreeSet to keep order for helping tests - let expected_groups: BTreeSet = - BTreeSet::from_iter(group_ids.into_iter().map(|group| group.as_ref().into())); - let current_groups: BTreeSet = BTreeSet::from_iter( - UserGroupMembership::find() - .filter(user_group_membership::Column::UserId.eq(user_id.as_ref())) - .all(db) - .await - .map_err(|e| db_err(e, "selecting group memberships of the user"))? - .into_iter() - .map(|item| item.group_id), - ); - - let groups_to_remove: BTreeSet = current_groups - .iter() - .filter(|&item| !expected_groups.contains(item)) - .cloned() - .collect(); - - let groups_to_add: BTreeSet = expected_groups - .iter() - .filter(|&item| !current_groups.contains(item)) - .cloned() - .collect(); - - if !groups_to_remove.is_empty() { - remove_user_from_groups(db, user_id.as_ref(), groups_to_remove).await?; - } - if !groups_to_add.is_empty() { - add_users_to_groups( - db, - groups_to_add - .into_iter() - .map(|group| (user_id.as_ref(), group.clone())), - ) - .await?; - } - - Ok(()) -} +pub use set::set_user_groups; #[cfg(test)] mod tests { - use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + use crate::db::entity::user_group_membership; - use super::*; - - fn get_data_mock, G: AsRef>( + pub(super) fn get_mock, G: AsRef>( user_id: U, group_id: G, ) -> user_group_membership::Model { @@ -98,148 +35,4 @@ mod tests { group_id: group_id.as_ref().to_string(), } } - - #[tokio::test] - async fn test_add_and_remove() { - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![ - get_data_mock("u1", "g1"), - get_data_mock("u1", "g2"), - get_data_mock("u1", "g3"), - get_data_mock("u1", "g4"), - ]]) - .append_exec_results([MockExecResult { - rows_affected: 1, - ..Default::default() - }]) - .append_exec_results([MockExecResult { - rows_affected: 1, - ..Default::default() - }]) - .into_connection(); - - set_user_groups(&db, "u1", vec!["g2", "g4", "g5", "g0"]) - .await - .unwrap(); - - // Checking transaction log - assert_eq!( - db.into_transaction_log(), - [ - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"SELECT "user_group_membership"."user_id", "user_group_membership"."group_id" FROM "user_group_membership" WHERE "user_group_membership"."user_id" = $1"#, - ["u1".into()] - ), - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"DELETE FROM "user_group_membership" WHERE "user_group_membership"."user_id" = $1 AND "user_group_membership"."group_id" IN ($2, $3)"#, - ["u1".into(), "g1".into(), "g3".into()] - ), - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"INSERT INTO "user_group_membership" ("user_id", "group_id") VALUES ($1, $2), ($3, $4) RETURNING "user_id", "group_id""#, - ["u1".into(), "g0".into(), "u1".into(), "g5".into()] - ), - ] - ); - } - - #[tokio::test] - async fn test_only_add() { - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![ - get_data_mock("u1", "g1"), - get_data_mock("u1", "g2"), - get_data_mock("u1", "g3"), - get_data_mock("u1", "g4"), - ]]) - .append_exec_results([MockExecResult { - rows_affected: 1, - ..Default::default() - }]) - .into_connection(); - - set_user_groups(&db, "u1", vec!["g1", "g2", "g3", "g4", "g5"]) - .await - .unwrap(); - - // Checking transaction log - assert_eq!( - db.into_transaction_log(), - [ - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"SELECT "user_group_membership"."user_id", "user_group_membership"."group_id" FROM "user_group_membership" WHERE "user_group_membership"."user_id" = $1"#, - ["u1".into()] - ), - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"INSERT INTO "user_group_membership" ("user_id", "group_id") VALUES ($1, $2) RETURNING "user_id", "group_id""#, - ["u1".into(), "g5".into()] - ), - ] - ); - } - - #[tokio::test] - async fn test_only_delete() { - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![ - get_data_mock("u1", "g1"), - get_data_mock("u1", "g2"), - get_data_mock("u1", "g3"), - get_data_mock("u1", "g4"), - ]]) - .append_exec_results([MockExecResult { - rows_affected: 1, - ..Default::default() - }]) - .into_connection(); - - set_user_groups(&db, "u1", vec!["g2", "g4"]).await.unwrap(); - - // Checking transaction log - assert_eq!( - db.into_transaction_log(), - [ - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"SELECT "user_group_membership"."user_id", "user_group_membership"."group_id" FROM "user_group_membership" WHERE "user_group_membership"."user_id" = $1"#, - ["u1".into()] - ), - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"DELETE FROM "user_group_membership" WHERE "user_group_membership"."user_id" = $1 AND "user_group_membership"."group_id" IN ($2, $3)"#, - ["u1".into(), "g1".into(), "g3".into()] - ), - ] - ); - } - - #[tokio::test] - async fn test_no_change() { - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![ - get_data_mock("u1", "g1"), - get_data_mock("u1", "g2"), - get_data_mock("u1", "g3"), - get_data_mock("u1", "g4"), - ]]) - .into_connection(); - - set_user_groups(&db, "u1", vec!["g1", "g2", "g3", "g4"]) - .await - .unwrap(); - - // Checking transaction log - assert_eq!( - db.into_transaction_log(), - [Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"SELECT "user_group_membership"."user_id", "user_group_membership"."group_id" FROM "user_group_membership" WHERE "user_group_membership"."user_id" = $1"#, - ["u1".into()] - ),] - ); - } } diff --git a/src/identity/backends/sql/user_group/add.rs b/src/identity/backends/sql/user_group/add.rs index 14ac0c96..a8ceb8b0 100644 --- a/src/identity/backends/sql/user_group/add.rs +++ b/src/identity/backends/sql/user_group/add.rs @@ -65,18 +65,9 @@ where mod tests { use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + use super::super::tests::get_mock; use super::*; - fn get_mock, G: AsRef>( - user_id: U, - group_id: G, - ) -> user_group_membership::Model { - user_group_membership::Model { - user_id: user_id.as_ref().into(), - group_id: group_id.as_ref().into(), - } - } - #[tokio::test] async fn test_create() { // Create MockDatabase with mock query results diff --git a/src/identity/backends/sql/user_group/set.rs b/src/identity/backends/sql/user_group/set.rs new file mode 100644 index 00000000..575ffa2e --- /dev/null +++ b/src/identity/backends/sql/user_group/set.rs @@ -0,0 +1,231 @@ +// 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 sea_orm::query::*; +use std::collections::BTreeSet; + +use crate::db::entity::{prelude::UserGroupMembership, user_group_membership}; +use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; + +use super::{add_users_to_groups, remove_user_from_groups}; + +/// Set user group memberships. +/// +/// Add user to the groups it should be in and remove from the groups where the user is currently +/// member of, but should not be. This is only incremental operation and is not deleting group +/// membership where the user should stay. +pub async fn set_user_groups( + db: &DatabaseConnection, + user_id: U, + group_ids: I, +) -> Result<(), IdentityDatabaseError> +where + I: IntoIterator, + U: AsRef, + G: AsRef, +{ + // Use BTreeSet to keep order for helping tests + let expected_groups: BTreeSet = + BTreeSet::from_iter(group_ids.into_iter().map(|group| group.as_ref().into())); + let current_groups: BTreeSet = BTreeSet::from_iter( + UserGroupMembership::find() + .filter(user_group_membership::Column::UserId.eq(user_id.as_ref())) + .all(db) + .await + .map_err(|e| db_err(e, "selecting group memberships of the user"))? + .into_iter() + .map(|item| item.group_id), + ); + + let groups_to_remove: BTreeSet = current_groups + .iter() + .filter(|&item| !expected_groups.contains(item)) + .cloned() + .collect(); + + let groups_to_add: BTreeSet = expected_groups + .iter() + .filter(|&item| !current_groups.contains(item)) + .cloned() + .collect(); + + if !groups_to_remove.is_empty() { + remove_user_from_groups(db, user_id.as_ref(), groups_to_remove).await?; + } + if !groups_to_add.is_empty() { + add_users_to_groups( + db, + groups_to_add + .into_iter() + .map(|group| (user_id.as_ref(), group.clone())), + ) + .await?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + + use super::super::tests::get_mock; + use super::*; + + #[tokio::test] + async fn test_add_and_remove() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![ + get_mock("u1", "g1"), + get_mock("u1", "g2"), + get_mock("u1", "g3"), + get_mock("u1", "g4"), + ]]) + .append_exec_results([MockExecResult { + rows_affected: 1, + ..Default::default() + }]) + .append_exec_results([MockExecResult { + rows_affected: 1, + ..Default::default() + }]) + .into_connection(); + + set_user_groups(&db, "u1", vec!["g2", "g4", "g5", "g0"]) + .await + .unwrap(); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "user_group_membership"."user_id", "user_group_membership"."group_id" FROM "user_group_membership" WHERE "user_group_membership"."user_id" = $1"#, + ["u1".into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"DELETE FROM "user_group_membership" WHERE "user_group_membership"."user_id" = $1 AND "user_group_membership"."group_id" IN ($2, $3)"#, + ["u1".into(), "g1".into(), "g3".into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"INSERT INTO "user_group_membership" ("user_id", "group_id") VALUES ($1, $2), ($3, $4) RETURNING "user_id", "group_id""#, + ["u1".into(), "g0".into(), "u1".into(), "g5".into()] + ), + ] + ); + } + + #[tokio::test] + async fn test_only_add() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![ + get_mock("u1", "g1"), + get_mock("u1", "g2"), + get_mock("u1", "g3"), + get_mock("u1", "g4"), + ]]) + .append_exec_results([MockExecResult { + rows_affected: 1, + ..Default::default() + }]) + .into_connection(); + + set_user_groups(&db, "u1", vec!["g1", "g2", "g3", "g4", "g5"]) + .await + .unwrap(); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "user_group_membership"."user_id", "user_group_membership"."group_id" FROM "user_group_membership" WHERE "user_group_membership"."user_id" = $1"#, + ["u1".into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"INSERT INTO "user_group_membership" ("user_id", "group_id") VALUES ($1, $2) RETURNING "user_id", "group_id""#, + ["u1".into(), "g5".into()] + ), + ] + ); + } + + #[tokio::test] + async fn test_only_delete() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![ + get_mock("u1", "g1"), + get_mock("u1", "g2"), + get_mock("u1", "g3"), + get_mock("u1", "g4"), + ]]) + .append_exec_results([MockExecResult { + rows_affected: 1, + ..Default::default() + }]) + .into_connection(); + + set_user_groups(&db, "u1", vec!["g2", "g4"]).await.unwrap(); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "user_group_membership"."user_id", "user_group_membership"."group_id" FROM "user_group_membership" WHERE "user_group_membership"."user_id" = $1"#, + ["u1".into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"DELETE FROM "user_group_membership" WHERE "user_group_membership"."user_id" = $1 AND "user_group_membership"."group_id" IN ($2, $3)"#, + ["u1".into(), "g1".into(), "g3".into()] + ), + ] + ); + } + + #[tokio::test] + async fn test_no_change() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![ + get_mock("u1", "g1"), + get_mock("u1", "g2"), + get_mock("u1", "g3"), + get_mock("u1", "g4"), + ]]) + .into_connection(); + + set_user_groups(&db, "u1", vec!["g1", "g2", "g3", "g4"]) + .await + .unwrap(); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "user_group_membership"."user_id", "user_group_membership"."group_id" FROM "user_group_membership" WHERE "user_group_membership"."user_id" = $1"#, + ["u1".into()] + ),] + ); + } +} diff --git a/src/identity/backends/sql/user_option.rs b/src/identity/backends/sql/user_option.rs index 3cec8016..1abc5eee 100644 --- a/src/identity/backends/sql/user_option.rs +++ b/src/identity/backends/sql/user_option.rs @@ -12,24 +12,12 @@ // // SPDX-License-Identifier: Apache-2.0 -use sea_orm::DatabaseConnection; -use sea_orm::entity::*; -use sea_orm::query::*; - -use crate::db::entity::{prelude::UserOption as DbUserOptions, user_option}; -use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; +use crate::db::entity::user_option; use crate::identity::types::*; -pub async fn get>( - db: &DatabaseConnection, - user_id: S, -) -> Result, IdentityDatabaseError> { - DbUserOptions::find() - .filter(user_option::Column::UserId.eq(user_id.as_ref())) - .all(db) - .await - .map_err(|err| db_err(err, "fetching options of the user")) -} +mod list; + +pub use list::list_by_user_id; impl FromIterator for UserOptions { fn from_iter>(iter: I) -> Self { diff --git a/src/identity/backends/sql/user_option/list.rs b/src/identity/backends/sql/user_option/list.rs new file mode 100644 index 00000000..493c65e6 --- /dev/null +++ b/src/identity/backends/sql/user_option/list.rs @@ -0,0 +1,31 @@ +// 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 sea_orm::query::*; + +use crate::db::entity::{prelude::UserOption as DbUserOptions, user_option}; +use crate::identity::backends::sql::{IdentityDatabaseError, db_err}; + +pub async fn list_by_user_id>( + db: &DatabaseConnection, + user_id: S, +) -> Result, IdentityDatabaseError> { + DbUserOptions::find() + .filter(user_option::Column::UserId.eq(user_id.as_ref())) + .all(db) + .await + .map_err(|err| db_err(err, "fetching options of the user")) +} diff --git a/src/identity/mock.rs b/src/identity/mock.rs new file mode 100644 index 00000000..d3f56149 --- /dev/null +++ b/src/identity/mock.rs @@ -0,0 +1,196 @@ +// 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 mockall::mock; +use std::collections::HashSet; +use webauthn_rs::prelude::{Passkey, PasskeyAuthentication, PasskeyRegistration}; + +use crate::auth::AuthenticatedInfo; +use crate::config::Config; +use crate::identity::IdentityApi; +use crate::identity::error::IdentityProviderError; +use crate::identity::types::{ + Group, GroupCreate, GroupListParameters, UserCreate, UserListParameters, + UserPasswordAuthRequest, UserResponse, WebauthnCredential, +}; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManager; + +mock! { + pub IdentityProvider { + pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; + } + + #[async_trait] + impl IdentityApi for IdentityProvider { + async fn authenticate_by_password( + &self, + state: &ServiceState, + auth: UserPasswordAuthRequest, + ) -> Result; + + async fn list_users( + &self, + state: &ServiceState, + params: &UserListParameters, + ) -> Result, IdentityProviderError>; + + async fn get_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError>; + + async fn find_federated_user<'a>( + &self, + state: &ServiceState, + idp_id: &'a str, + unique_id: &'a str, + ) -> Result, IdentityProviderError>; + + async fn create_user( + &self, + state: &ServiceState, + user: UserCreate, + ) -> Result; + + async fn delete_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + async fn list_groups( + &self, + state: &ServiceState, + params: &GroupListParameters, + ) -> Result, IdentityProviderError>; + + async fn get_group<'a>( + &self, + state: &ServiceState, + group_id: &'a str, + ) -> Result, IdentityProviderError>; + + async fn create_group( + &self, + state: &ServiceState, + group: GroupCreate, + ) -> Result; + + async fn delete_group<'a>( + &self, + state: &ServiceState, + group_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + async fn list_groups_of_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError>; + + async fn add_user_to_group<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + async fn add_users_to_groups<'a>( + &self, + state: &ServiceState, + memberships: Vec<(&'a str, &'a str)> + ) -> Result<(), IdentityProviderError>; + + async fn remove_user_from_group<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + async fn remove_user_from_groups<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + ) -> Result<(), IdentityProviderError>; + + async fn set_user_groups<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + ) -> Result<(), IdentityProviderError>; + + async fn list_user_webauthn_credentials<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError>; + + async fn create_user_webauthn_credential<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + passkey: &Passkey, + description: Option<&'a str> + ) -> Result; + + async fn save_user_webauthn_credential_registration_state<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + auth_state: PasskeyRegistration, + ) -> Result<(), IdentityProviderError>; + + async fn save_user_webauthn_credential_authentication_state<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + auth_state: PasskeyAuthentication, + ) -> Result<(), IdentityProviderError>; + + async fn get_user_webauthn_credential_registration_state<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError>; + + async fn get_user_webauthn_credential_authentication_state<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError>; + + async fn delete_user_webauthn_credential_registration_state<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + async fn delete_user_webauthn_credential_authentication_state<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result<(), IdentityProviderError>; + } + + impl Clone for IdentityProvider { + fn clone(&self) -> Self; + } + +} diff --git a/src/identity/mod.rs b/src/identity/mod.rs index 0bd7f2cb..6df0187b 100644 --- a/src/identity/mod.rs +++ b/src/identity/mod.rs @@ -13,367 +13,38 @@ // SPDX-License-Identifier: Apache-2.0 use async_trait::async_trait; -#[cfg(test)] -use mockall::mock; use std::collections::HashSet; use uuid::Uuid; use webauthn_rs::prelude::{Passkey, PasskeyAuthentication, PasskeyRegistration}; pub mod backends; pub mod error; +#[cfg(test)] +pub mod mock; pub mod password_hashing; -pub(crate) mod types; +pub mod types; +#[cfg(test)] +pub use mock::MockIdentityProvider; use crate::auth::AuthenticatedInfo; use crate::config::Config; -use crate::identity::backends::sql::SqlBackend; +use crate::identity::backends::{IdentityBackend, sql::SqlBackend}; use crate::identity::error::IdentityProviderError; use crate::identity::types::{ - Group, GroupCreate, GroupListParameters, IdentityBackend, UserCreate, UserListParameters, + Group, GroupCreate, GroupListParameters, UserCreate, UserListParameters, UserPasswordAuthRequest, UserResponse, WebauthnCredential, }; use crate::keystone::ServiceState; use crate::plugin_manager::PluginManager; use crate::resource::{ResourceApi, error::ResourceProviderError}; +pub use types::IdentityApi; + #[derive(Clone, Debug)] pub struct IdentityProvider { backend_driver: Box, } -#[async_trait] -pub trait IdentityApi: Send + Sync + Clone { - async fn authenticate_by_password( - &self, - state: &ServiceState, - auth: UserPasswordAuthRequest, - ) -> Result; - - async fn list_users( - &self, - state: &ServiceState, - params: &UserListParameters, - ) -> Result, IdentityProviderError>; - - async fn get_user<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError>; - - async fn find_federated_user<'a>( - &self, - state: &ServiceState, - idp_id: &'a str, - unique_id: &'a str, - ) -> Result, IdentityProviderError>; - - async fn create_user( - &self, - state: &ServiceState, - user: UserCreate, - ) -> Result; - - async fn delete_user<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - async fn list_groups( - &self, - state: &ServiceState, - params: &GroupListParameters, - ) -> Result, IdentityProviderError>; - - async fn get_group<'a>( - &self, - state: &ServiceState, - group_id: &'a str, - ) -> Result, IdentityProviderError>; - - async fn create_group( - &self, - state: &ServiceState, - group: GroupCreate, - ) -> Result; - - async fn delete_group<'a>( - &self, - state: &ServiceState, - group_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - /// List groups the user is a member of. - async fn list_groups_of_user<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError>; - - /// Add the user to the single group. - async fn add_user_to_group<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - /// Add user group memberships as specified by (uid, gid) tuples. - async fn add_users_to_groups<'a>( - &self, - state: &ServiceState, - memberships: Vec<(&'a str, &'a str)>, - ) -> Result<(), IdentityProviderError>; - - /// Remove the user from the single group. - async fn remove_user_from_group<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - /// Remove the user from specified groups. - async fn remove_user_from_groups<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_ids: HashSet<&'a str>, - ) -> Result<(), IdentityProviderError>; - - /// Set group memberships of the user. - async fn set_user_groups<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_ids: HashSet<&'a str>, - ) -> Result<(), IdentityProviderError>; - - async fn list_user_webauthn_credentials<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError>; - - /// Create passkey. - async fn create_user_webauthn_credential<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - passkey: &Passkey, - description: Option<&'a str>, - ) -> Result; - - async fn save_user_webauthn_credential_registration_state<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - state: PasskeyRegistration, - ) -> Result<(), IdentityProviderError>; - - async fn save_user_webauthn_credential_authentication_state<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - state: PasskeyAuthentication, - ) -> Result<(), IdentityProviderError>; - - async fn get_user_webauthn_credential_registration_state<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError>; - - async fn get_user_webauthn_credential_authentication_state<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError>; - - /// Delete passkey registration state of a user - async fn delete_user_webauthn_credential_registration_state<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - /// Delete passkey registration state of a user - async fn delete_user_webauthn_credential_authentication_state<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result<(), IdentityProviderError>; -} - -#[cfg(test)] -mock! { - pub IdentityProvider { - pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; - } - - #[async_trait] - impl IdentityApi for IdentityProvider { - async fn authenticate_by_password( - &self, - state: &ServiceState, - auth: UserPasswordAuthRequest, - ) -> Result; - - async fn list_users( - &self, - state: &ServiceState, - params: &UserListParameters, - ) -> Result, IdentityProviderError>; - - async fn get_user<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError>; - - async fn find_federated_user<'a>( - &self, - state: &ServiceState, - idp_id: &'a str, - unique_id: &'a str, - ) -> Result, IdentityProviderError>; - - async fn create_user( - &self, - state: &ServiceState, - user: UserCreate, - ) -> Result; - - async fn delete_user<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - async fn list_groups( - &self, - state: &ServiceState, - params: &GroupListParameters, - ) -> Result, IdentityProviderError>; - - async fn get_group<'a>( - &self, - state: &ServiceState, - group_id: &'a str, - ) -> Result, IdentityProviderError>; - - async fn create_group( - &self, - state: &ServiceState, - group: GroupCreate, - ) -> Result; - - async fn delete_group<'a>( - &self, - state: &ServiceState, - group_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - async fn list_groups_of_user<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError>; - - async fn add_user_to_group<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - async fn add_users_to_groups<'a>( - &self, - state: &ServiceState, - memberships: Vec<(&'a str, &'a str)> - ) -> Result<(), IdentityProviderError>; - - async fn remove_user_from_group<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - async fn remove_user_from_groups<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_ids: HashSet<&'a str>, - ) -> Result<(), IdentityProviderError>; - - async fn set_user_groups<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_ids: HashSet<&'a str>, - ) -> Result<(), IdentityProviderError>; - - async fn list_user_webauthn_credentials<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError>; - - async fn create_user_webauthn_credential<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - passkey: &Passkey, - description: Option<&'a str> - ) -> Result; - - async fn save_user_webauthn_credential_registration_state<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - auth_state: PasskeyRegistration, - ) -> Result<(), IdentityProviderError>; - - async fn save_user_webauthn_credential_authentication_state<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - auth_state: PasskeyAuthentication, - ) -> Result<(), IdentityProviderError>; - - async fn get_user_webauthn_credential_registration_state<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError>; - - async fn get_user_webauthn_credential_authentication_state<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError>; - - async fn delete_user_webauthn_credential_registration_state<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - async fn delete_user_webauthn_credential_authentication_state<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result<(), IdentityProviderError>; - } - - impl Clone for IdentityProvider { - fn clone(&self) -> Self; - } - -} - impl IdentityProvider { pub fn new( config: &Config, diff --git a/src/identity/types.rs b/src/identity/types.rs index 911715b1..b3f90057 100644 --- a/src/identity/types.rs +++ b/src/identity/types.rs @@ -18,43 +18,34 @@ pub mod group; pub mod user; use async_trait::async_trait; -use dyn_clone::DynClone; use webauthn_rs::prelude::{Passkey, PasskeyAuthentication, PasskeyRegistration}; use crate::auth::AuthenticatedInfo; -use crate::config::Config; use crate::identity::IdentityProviderError; pub use crate::identity::types::group::{Group, GroupCreate, GroupListParameters}; pub use crate::identity::types::user::*; use crate::keystone::ServiceState; #[async_trait] -pub trait IdentityBackend: DynClone + Send + Sync + std::fmt::Debug { - /// Set config. - fn set_config(&mut self, config: Config); - - /// Authenticate a user by a password. +pub trait IdentityApi: Send + Sync + Clone { async fn authenticate_by_password( &self, state: &ServiceState, auth: UserPasswordAuthRequest, ) -> Result; - /// List Users. async fn list_users( &self, state: &ServiceState, params: &UserListParameters, - ) -> Result, IdentityProviderError>; + ) -> Result, IdentityProviderError>; - /// Get single user by ID. async fn get_user<'a>( &self, state: &ServiceState, user_id: &'a str, ) -> Result, IdentityProviderError>; - /// Find federated user by IDP and Unique ID. async fn find_federated_user<'a>( &self, state: &ServiceState, @@ -62,56 +53,50 @@ pub trait IdentityBackend: DynClone + Send + Sync + std::fmt::Debug { unique_id: &'a str, ) -> Result, IdentityProviderError>; - /// Create user. async fn create_user( &self, state: &ServiceState, user: UserCreate, ) -> Result; - /// Delete user. async fn delete_user<'a>( &self, state: &ServiceState, user_id: &'a str, ) -> Result<(), IdentityProviderError>; - /// List groups. async fn list_groups( &self, state: &ServiceState, params: &GroupListParameters, - ) -> Result, IdentityProviderError>; + ) -> Result, IdentityProviderError>; - /// Get single group by ID. async fn get_group<'a>( &self, state: &ServiceState, group_id: &'a str, ) -> Result, IdentityProviderError>; - /// Create group. async fn create_group( &self, state: &ServiceState, group: GroupCreate, ) -> Result; - /// Delete group by ID. async fn delete_group<'a>( &self, state: &ServiceState, group_id: &'a str, ) -> Result<(), IdentityProviderError>; - /// List groups a user is member of. + /// List groups the user is a member of. async fn list_groups_of_user<'a>( &self, state: &ServiceState, user_id: &'a str, - ) -> Result, IdentityProviderError>; + ) -> Result, IdentityProviderError>; - /// Add the user to the group. + /// Add the user to the single group. async fn add_user_to_group<'a>( &self, state: &ServiceState, @@ -119,14 +104,14 @@ pub trait IdentityBackend: DynClone + Send + Sync + std::fmt::Debug { group_id: &'a str, ) -> Result<(), IdentityProviderError>; - /// Add user group membership relations. + /// Add user group memberships as specified by (uid, gid) tuples. async fn add_users_to_groups<'a>( &self, state: &ServiceState, memberships: Vec<(&'a str, &'a str)>, ) -> Result<(), IdentityProviderError>; - /// Remove the user from the group. + /// Remove the user from the single group. async fn remove_user_from_group<'a>( &self, state: &ServiceState, @@ -134,7 +119,7 @@ pub trait IdentityBackend: DynClone + Send + Sync + std::fmt::Debug { group_id: &'a str, ) -> Result<(), IdentityProviderError>; - /// Remove the user from multiple groups. + /// Remove the user from specified groups. async fn remove_user_from_groups<'a>( &self, state: &ServiceState, @@ -142,7 +127,7 @@ pub trait IdentityBackend: DynClone + Send + Sync + std::fmt::Debug { group_ids: HashSet<&'a str>, ) -> Result<(), IdentityProviderError>; - /// Set group memberships for the user. + /// Set group memberships of the user. async fn set_user_groups<'a>( &self, state: &ServiceState, @@ -150,12 +135,11 @@ pub trait IdentityBackend: DynClone + Send + Sync + std::fmt::Debug { group_ids: HashSet<&'a str>, ) -> Result<(), IdentityProviderError>; - /// List user passkeys. async fn list_user_webauthn_credentials<'a>( &self, state: &ServiceState, user_id: &'a str, - ) -> Result, IdentityProviderError>; + ) -> Result, IdentityProviderError>; /// Create passkey. async fn create_user_webauthn_credential<'a>( @@ -166,49 +150,43 @@ pub trait IdentityBackend: DynClone + Send + Sync + std::fmt::Debug { description: Option<&'a str>, ) -> Result; - /// Save passkey registration state. - async fn create_user_webauthn_credential_registration_state<'a>( + async fn save_user_webauthn_credential_registration_state<'a>( &self, state: &ServiceState, user_id: &'a str, state: PasskeyRegistration, ) -> Result<(), IdentityProviderError>; - /// Save passkey auth state. - async fn create_user_webauthn_credential_authentication_state<'a>( + async fn save_user_webauthn_credential_authentication_state<'a>( &self, state: &ServiceState, user_id: &'a str, state: PasskeyAuthentication, ) -> Result<(), IdentityProviderError>; - /// Get passkey registration state. async fn get_user_webauthn_credential_registration_state<'a>( &self, state: &ServiceState, user_id: &'a str, ) -> Result, IdentityProviderError>; - /// Get passkey authentication state. async fn get_user_webauthn_credential_authentication_state<'a>( &self, state: &ServiceState, user_id: &'a str, ) -> Result, IdentityProviderError>; - /// Delete passkey registration state of a user. + /// Delete passkey registration state of a user async fn delete_user_webauthn_credential_registration_state<'a>( &self, state: &ServiceState, user_id: &'a str, ) -> Result<(), IdentityProviderError>; - /// Delete passkey authentication state of a user. + /// Delete passkey registration state of a user async fn delete_user_webauthn_credential_authentication_state<'a>( &self, state: &ServiceState, user_id: &'a str, ) -> Result<(), IdentityProviderError>; } - -dyn_clone::clone_trait_object!(IdentityBackend); diff --git a/src/plugin_manager.rs b/src/plugin_manager.rs index e644ed47..46f8090b 100644 --- a/src/plugin_manager.rs +++ b/src/plugin_manager.rs @@ -17,7 +17,7 @@ use std::collections::HashMap; use crate::assignment::backend::AssignmentBackend; use crate::catalog::backends::CatalogBackend; use crate::federation::types::FederationBackend; -use crate::identity::types::IdentityBackend; +use crate::identity::backends::IdentityBackend; use crate::resource::types::ResourceBackend; use crate::revoke::backend::RevokeBackend;