From fae0f6c15cd54051c90fed1555b89407ce6c5ab2 Mon Sep 17 00:00:00 2001 From: konac-hamza Date: Sun, 21 Dec 2025 23:44:09 +0300 Subject: [PATCH 1/2] feat: Extend backend driver to list only local users --- src/api/v3/user/types.rs | 1 + src/identity/backends/sql.rs | 15 +++ src/identity/backends/sql/local_user.rs | 2 + src/identity/backends/sql/local_user/list.rs | 98 ++++++++++++++++++++ src/identity/types/user.rs | 22 +++++ 5 files changed, 138 insertions(+) create mode 100644 src/identity/backends/sql/local_user/list.rs diff --git a/src/api/v3/user/types.rs b/src/api/v3/user/types.rs index a15bdb0..dc15652 100644 --- a/src/api/v3/user/types.rs +++ b/src/api/v3/user/types.rs @@ -353,6 +353,7 @@ impl From for identity_types::UserListParameters { domain_id: value.domain_id, name: value.name, unique_id: value.unique_id, + user_type: None, // limit: value.limit, } } diff --git a/src/identity/backends/sql.rs b/src/identity/backends/sql.rs index 9d19eba..2413ca3 100644 --- a/src/identity/backends/sql.rs +++ b/src/identity/backends/sql.rs @@ -64,6 +64,21 @@ impl IdentityBackend for SqlBackend { state: &ServiceState, params: &UserListParameters, ) -> Result, IdentityProviderError> { + // Determine user type to call appropriate listing function + if let Some(val) = ¶ms.user_type { + match val { + UserType::Local => { + return Ok(local_user::list(&self.config, &state.db, params).await?); + } + UserType::NonLocal => { + // return Ok(nonlocal_user::list(&self.config, &state.db, params).await?); + } + UserType::Federated => { + // return Ok(federated_user::list(&self.config, &state.db, params).await?); + } + UserType::All => { /* continue to general listing */ } + } + } Ok(user::list(&self.config, &state.db, params).await?) } diff --git a/src/identity/backends/sql/local_user.rs b/src/identity/backends/sql/local_user.rs index 131102c..7936a2b 100644 --- a/src/identity/backends/sql/local_user.rs +++ b/src/identity/backends/sql/local_user.rs @@ -17,9 +17,11 @@ use crate::identity::types::*; mod create; mod get; +mod list; mod load; mod set; +pub use list::list; pub use load::load_local_user_with_passwords; pub use load::load_local_users_passwords; pub use set::reset_failed_auth; diff --git a/src/identity/backends/sql/local_user/list.rs b/src/identity/backends/sql/local_user/list.rs new file mode 100644 index 0000000..f56be5f --- /dev/null +++ b/src/identity/backends/sql/local_user/list.rs @@ -0,0 +1,98 @@ +// 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::local_user; +use crate::config::Config; +use crate::db::entity::{ + local_user as db_local_user, password as db_password, + prelude::{LocalUser, User as DbUser}, + user as db_user, +}; +use crate::error::DbContextExt; +use crate::identity::backends::error::IdentityDatabaseError; +use crate::identity::types::*; + +pub async fn list( + conf: &Config, + db: &DatabaseConnection, + params: &UserListParameters, +) -> Result, IdentityDatabaseError> { + // Prepare basic selects for users and local_users only + let mut user_select = DbUser::find(); + let mut local_user_select = LocalUser::find(); + + // Apply filters to the user table + if let Some(domain_id) = ¶ms.domain_id { + user_select = user_select.filter(db_user::Column::DomainId.eq(domain_id)); + } + + // Apply filters to the local_user table + if let Some(name) = ¶ms.name { + local_user_select = local_user_select.filter(db_local_user::Column::Name.eq(name)); + } + + // Fetch users from the user table + let db_users: Vec = user_select.all(db).await.context("fetching users data")?; + + // Load related local_user data + let local_users = db_users + .load_one(local_user_select, db) + .await + .context("fetching local users data")?; + + // Load passwords for local users + let local_users_passwords: Vec>> = + local_user::load_local_users_passwords( + db, + local_users.iter().cloned().map(|u| u.map(|x| x.id)), + ) + .await?; + + let last_activity_cutof_date = conf.get_user_last_activity_cutof_date(); + + let mut results: Vec = Vec::new(); + + // Iterate over users and build responses + for (u, (local, passwords)) in db_users.into_iter().zip( + local_users + .into_iter() + .zip(local_users_passwords.into_iter()), + ) { + // Skip users without local_user data + let Some(local) = local else { + continue; + }; + + // Build the user response + let mut user_builder = UserResponseBuilder::default(); + user_builder.merge_user_data( + &u, + &UserOptions::default(), // Local users don't have user options + last_activity_cutof_date.as_ref(), + ); + user_builder.merge_local_user_data(&local); + + if let Some(pass) = passwords { + user_builder.merge_passwords_data(pass.into_iter()); + } + + results.push(user_builder.build()?); + } + + Ok(results) +} diff --git a/src/identity/types/user.rs b/src/identity/types/user.rs index a65db7e..07cd97e 100644 --- a/src/identity/types/user.rs +++ b/src/identity/types/user.rs @@ -204,6 +204,28 @@ pub struct UserListParameters { #[builder(default)] #[validate(length(max = 64))] pub unique_id: Option, + /// Filter users by User Type (local, federated, nonlocal, all). + #[builder(default)] + #[serde(default, rename = "type")] + pub user_type: Option, +} + +/// User type for filtering. +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Hash, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum UserType { + /// Local users only (with passwords). + Local, + + /// Federated users only (authenticated via external IdP). + Federated, + + /// Non-local users (users without local authentication). + NonLocal, + + /// All users (default behavior). + #[default] + All, } /// User password information. From 4a434aac5f9c955c5c904e24e3e0d89c4ddabab1 Mon Sep 17 00:00:00 2001 From: konac-hamza Date: Tue, 23 Dec 2025 23:50:35 +0300 Subject: [PATCH 2/2] core: Extend list method to handle all types --- src/api/v3/user/types.rs | 3 +- src/identity/backends/sql.rs | 21 +- src/identity/backends/sql/local_user.rs | 2 - src/identity/backends/sql/local_user/list.rs | 98 -------- src/identity/backends/sql/user/list.rs | 245 +++++++++++++------ 5 files changed, 182 insertions(+), 187 deletions(-) delete mode 100644 src/identity/backends/sql/local_user/list.rs diff --git a/src/api/v3/user/types.rs b/src/api/v3/user/types.rs index dc15652..481c489 100644 --- a/src/api/v3/user/types.rs +++ b/src/api/v3/user/types.rs @@ -353,8 +353,7 @@ impl From for identity_types::UserListParameters { domain_id: value.domain_id, name: value.name, unique_id: value.unique_id, - user_type: None, - // limit: value.limit, + ..Default::default() // limit: value.limit, } } } diff --git a/src/identity/backends/sql.rs b/src/identity/backends/sql.rs index 2413ca3..d338c51 100644 --- a/src/identity/backends/sql.rs +++ b/src/identity/backends/sql.rs @@ -64,22 +64,13 @@ impl IdentityBackend for SqlBackend { state: &ServiceState, params: &UserListParameters, ) -> Result, IdentityProviderError> { - // Determine user type to call appropriate listing function - if let Some(val) = ¶ms.user_type { - match val { - UserType::Local => { - return Ok(local_user::list(&self.config, &state.db, params).await?); - } - UserType::NonLocal => { - // return Ok(nonlocal_user::list(&self.config, &state.db, params).await?); - } - UserType::Federated => { - // return Ok(federated_user::list(&self.config, &state.db, params).await?); - } - UserType::All => { /* continue to general listing */ } - } + // Create a modified copy if user_type is None + let mut modified_params = params.clone(); + if modified_params.user_type.is_none() { + modified_params.user_type = Some(UserType::All); } - Ok(user::list(&self.config, &state.db, params).await?) + + Ok(user::list(&self.config, &state.db, &modified_params).await?) } /// Get single user by ID diff --git a/src/identity/backends/sql/local_user.rs b/src/identity/backends/sql/local_user.rs index 7936a2b..131102c 100644 --- a/src/identity/backends/sql/local_user.rs +++ b/src/identity/backends/sql/local_user.rs @@ -17,11 +17,9 @@ use crate::identity::types::*; mod create; mod get; -mod list; mod load; mod set; -pub use list::list; pub use load::load_local_user_with_passwords; pub use load::load_local_users_passwords; pub use set::reset_failed_auth; diff --git a/src/identity/backends/sql/local_user/list.rs b/src/identity/backends/sql/local_user/list.rs deleted file mode 100644 index f56be5f..0000000 --- a/src/identity/backends/sql/local_user/list.rs +++ /dev/null @@ -1,98 +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 sea_orm::DatabaseConnection; -use sea_orm::entity::*; -use sea_orm::query::*; - -use super::super::local_user; -use crate::config::Config; -use crate::db::entity::{ - local_user as db_local_user, password as db_password, - prelude::{LocalUser, User as DbUser}, - user as db_user, -}; -use crate::error::DbContextExt; -use crate::identity::backends::error::IdentityDatabaseError; -use crate::identity::types::*; - -pub async fn list( - conf: &Config, - db: &DatabaseConnection, - params: &UserListParameters, -) -> Result, IdentityDatabaseError> { - // Prepare basic selects for users and local_users only - let mut user_select = DbUser::find(); - let mut local_user_select = LocalUser::find(); - - // Apply filters to the user table - if let Some(domain_id) = ¶ms.domain_id { - user_select = user_select.filter(db_user::Column::DomainId.eq(domain_id)); - } - - // Apply filters to the local_user table - if let Some(name) = ¶ms.name { - local_user_select = local_user_select.filter(db_local_user::Column::Name.eq(name)); - } - - // Fetch users from the user table - let db_users: Vec = user_select.all(db).await.context("fetching users data")?; - - // Load related local_user data - let local_users = db_users - .load_one(local_user_select, db) - .await - .context("fetching local users data")?; - - // Load passwords for local users - let local_users_passwords: Vec>> = - local_user::load_local_users_passwords( - db, - local_users.iter().cloned().map(|u| u.map(|x| x.id)), - ) - .await?; - - let last_activity_cutof_date = conf.get_user_last_activity_cutof_date(); - - let mut results: Vec = Vec::new(); - - // Iterate over users and build responses - for (u, (local, passwords)) in db_users.into_iter().zip( - local_users - .into_iter() - .zip(local_users_passwords.into_iter()), - ) { - // Skip users without local_user data - let Some(local) = local else { - continue; - }; - - // Build the user response - let mut user_builder = UserResponseBuilder::default(); - user_builder.merge_user_data( - &u, - &UserOptions::default(), // Local users don't have user options - last_activity_cutof_date.as_ref(), - ); - user_builder.merge_local_user_data(&local); - - if let Some(pass) = passwords { - user_builder.merge_passwords_data(pass.into_iter()); - } - - results.push(user_builder.build()?); - } - - Ok(results) -} diff --git a/src/identity/backends/sql/user/list.rs b/src/identity/backends/sql/user/list.rs index 01c7154..fc66916 100644 --- a/src/identity/backends/sql/user/list.rs +++ b/src/identity/backends/sql/user/list.rs @@ -35,93 +35,198 @@ pub async fn list( ) -> 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 = user_select.all(db).await.context("fetching users")?; + + if db_users.is_empty() { + return Ok(vec![]); } - let db_users: Vec = user_select.all(db).await.context("fetching users data")?; + let user_type = params.user_type.unwrap_or(UserType::All); + let last_activity_cutof_date = conf.get_user_last_activity_cutof_date(); - 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 mut results: Vec = Vec::new(); - let locals = local_users.context("fetching local users data")?; + match user_type { + UserType::Local => { + let mut local_user_select = LocalUser::find(); - let local_users_passwords: Vec>> = - local_user::load_local_users_passwords(db, locals.iter().cloned().map(|u| u.map(|x| x.id))) - .await?; + if let Some(name) = ¶ms.name { + local_user_select = local_user_select.filter(db_local_user::Column::Name.eq(name)); + } - let last_activity_cutof_date = conf.get_user_last_activity_cutof_date(); + let (user_opts, local_users) = tokio::join!( + db_users.load_many(UserOption, db), + db_users.load_one(local_user_select, db) + ); + let locals = local_users.context("fetching local users data")?; - let mut results: Vec = Vec::new(); - for (u, (o, (l, (p, (n, f))))) in db_users.into_iter().zip( - user_opts.context("fetching user options")?.into_iter().zip( - locals.into_iter().zip( - local_users_passwords.into_iter().zip( + let local_users_passwords: Vec>> = + local_user::load_local_users_passwords( + db, + locals.iter().cloned().map(|u| u.map(|x| x.id)), + ) + .await?; + + for ((u, opts), (l, p)) in db_users + .into_iter() + .zip(user_opts.context("fetching user options")?.into_iter()) + .zip(locals.into_iter().zip(local_users_passwords.into_iter())) + { + if let Some(local) = l { + let mut user_builder = UserResponseBuilder::default(); + user_builder.merge_user_data( + &u, + &UserOptions::from_iter(opts), + last_activity_cutof_date.as_ref(), + ); + user_builder.merge_local_user_data(&local); + if let Some(pass) = p { + user_builder.merge_passwords_data(pass.into_iter()); + } + results.push(user_builder.build()?); + } + } + } + UserType::NonLocal => { + let mut nonlocal_user_select = NonlocalUser::find(); + if let Some(name) = ¶ms.name { + nonlocal_user_select = + nonlocal_user_select.filter(db_nonlocal_user::Column::Name.eq(name)); + } + + let (user_opts, nonlocal_users) = tokio::join!( + db_users.load_many(UserOption, db), + db_users.load_one(nonlocal_user_select, db) + ); + + for ((u, opts), n) in db_users + .into_iter() + .zip(user_opts.context("fetching user options")?.into_iter()) + .zip( nonlocal_users .context("fetching nonlocal users data")? - .into_iter() - .zip( - federated_users - .context("fetching federated users data")? - .into_iter(), + .into_iter(), + ) + { + if let Some(nonlocal) = n { + let mut user_builder = UserResponseBuilder::default(); + user_builder.merge_user_data( + &u, + &UserOptions::from_iter(opts), + last_activity_cutof_date.as_ref(), + ); + user_builder.merge_nonlocal_user_data(&nonlocal); + results.push(user_builder.build()?); + } + } + } + UserType::Federated => { + let mut federated_user_select = db_federated_user::Entity::find(); + if let Some(name) = ¶ms.name { + federated_user_select = + federated_user_select.filter(db_federated_user::Column::DisplayName.eq(name)); + } + + let (user_opts, federated_users) = tokio::join!( + db_users.load_many(UserOption, db), + db_users.load_many(federated_user_select, db) + ); + for ((u, opts), f) in db_users + .into_iter() + .zip(user_opts.context("fetching user options")?.into_iter()) + .zip( + federated_users + .context("fetching federated users data")? + .into_iter(), + ) + { + if !f.is_empty() { + let mut user_builder = UserResponseBuilder::default(); + user_builder.merge_user_data( + &u, + &UserOptions::from_iter(opts), + last_activity_cutof_date.as_ref(), + ); + user_builder.merge_federated_user_data(f); + results.push(user_builder.build()?); + } + } + } + UserType::All => { + let mut local_user_select = LocalUser::find(); + let mut nonlocal_user_select = NonlocalUser::find(); + let mut federated_user_select = db_federated_user::Entity::find(); + 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 (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.context("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?; + + for (u, (o, (l, (p, (n, f))))) in db_users.into_iter().zip( + user_opts.context("fetching user options")?.into_iter().zip( + locals.into_iter().zip( + local_users_passwords.into_iter().zip( + nonlocal_users + .context("fetching nonlocal users data")? + .into_iter() + .zip( + federated_users + .context("fetching federated users data")? + .into_iter(), + ), ), + ), ), - ), - ), - ) { - if l.is_none() && n.is_none() && f.is_empty() { - continue; - } + ) { + if l.is_none() && n.is_none() && f.is_empty() { + continue; + } - let mut user_builder = UserResponseBuilder::default(); - user_builder.merge_user_data( - &u, - &UserOptions::from_iter(o), - last_activity_cutof_date.as_ref(), - ); - //user_builder.merge_options(&UserOptions::from_iter(o)); - if let Some(local) = l { - user_builder.merge_local_user_data(&local); - if let Some(pass) = p { - user_builder.merge_passwords_data(pass.into_iter()); + let mut user_builder = UserResponseBuilder::default(); + user_builder.merge_user_data( + &u, + &UserOptions::from_iter(o), + last_activity_cutof_date.as_ref(), + ); + //user_builder.merge_options(&UserOptions::from_iter(o)); + if let Some(local) = l { + user_builder.merge_local_user_data(&local); + if let Some(pass) = p { + user_builder.merge_passwords_data(pass.into_iter()); + } + } else if let Some(nonlocal) = n { + user_builder.merge_nonlocal_user_data(&nonlocal); + } else if !f.is_empty() { + user_builder.merge_federated_user_data(f); + } else { + return Err(IdentityDatabaseError::MalformedUser(u.id))?; + }; + results.push(user_builder.build()?); } - } else if let Some(nonlocal) = n { - user_builder.merge_nonlocal_user_data(&nonlocal); - } else if !f.is_empty() { - user_builder.merge_federated_user_data(f); - } 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) }