Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/api/v3/user/types.rs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is fine in the concept, but we are not going to expose this in the api v3 (so far). I suggest first you focus only on the backend/provider part. In the separate PR you can then start relying on that, but only in the v4 api (or at least we can have a separate discussion in the dedicated PR)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove this part of the change - no api changes for now

Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ impl From<UserListParameters> for identity_types::UserListParameters {
domain_id: value.domain_id,
name: value.name,
unique_id: value.unique_id,
// limit: value.limit,
..Default::default() // limit: value.limit,
}
}
}
203 changes: 191 additions & 12 deletions src/identity/backend/sql/user/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,27 +60,28 @@ pub async fn list(
let db_users: Vec<db_user::Model> = user_select.all(db).await.context("fetching users data")?;
let count_of_users_selected = db_users.len();

let user_type = params.user_type.unwrap_or(UserType::All);
let (user_opts, local_users, nonlocal_users, federated_users) = tokio::join!(
db_users.load_many(DbUserOption, db),
// Load local users when requested, otherwise return empty results list
async {
if true {
if user_type == UserType::Local || user_type == UserType::All {
db_users.load_one(local_user_select, db).await
} else {
Ok(vec![None; count_of_users_selected])
}
},
// Load nonlocal users when requested
async {
if true {
if user_type == UserType::NonLocal || user_type == UserType::All {
db_users.load_one(nonlocal_user_select, db).await
} else {
Ok(vec![None; count_of_users_selected])
}
},
// Load federated users when requested
async {
if true {
if user_type == UserType::Federated || user_type == UserType::All {
db_users.load_many(federated_user_select, db).await
} else {
Ok(vec![Vec::new(); count_of_users_selected])
Expand All @@ -92,15 +93,19 @@ pub async fn list(

// For local users fetch passwords to determine password expiration
let local_users_passwords: Vec<Option<Vec<db_password::Model>>> =
local_user::load_local_users_passwords(
db,
locals
.iter()
.cloned()
.map(|u| u.map(|x| x.id))
.collect::<Vec<_>>(),
)
.await?;
if user_type == UserType::Local || user_type == UserType::All {
local_user::load_local_users_passwords(
db,
locals
.iter()
.cloned()
.map(|u| u.map(|x| x.id))
.collect::<Vec<_>>(),
)
.await?
} else {
vec![None; count_of_users_selected]
};

// Determine the date for which users with the last activity earlier than are
// determined as inactive.
Expand Down Expand Up @@ -232,4 +237,178 @@ mod tests {
);
}
}

#[tokio::test]
async fn test_list_local_only() {
let db = MockDatabase::new(DatabaseBackend::Postgres)
.append_query_results([vec![
// local user
get_user_mock("1"),
// nonlocal user
get_user_mock("2"),
// federated user
get_user_mock("3"),
// a "bad" user with no user detail records
get_user_mock("4"),
]])
.append_query_results([[get_user_options_mock("1", &UserOptions::default())]
.into_iter()
.flatten()])
.append_query_results([vec![get_local_user_mock("1")]])
.append_query_results([vec![db_password::Model::default()]])
.into_connection();

let config = Config::default();
let res = list(
&config,
&db,
&UserListParameters {
user_type: Some(UserType::Local),
..Default::default()
},
)
.await
.unwrap();
assert_eq!(res.len(), 1, "1 local user found");
for (l,r) in db.into_transaction_log().iter().zip([
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""#,
[]
),
Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"SELECT "user_option"."user_id", "user_option"."option_id", "user_option"."option_value" FROM "user_option" WHERE "user_option"."user_id" IN ($1, $2, $3, $4)"#,
[]
),
Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"SELECT "local_user"."id", "local_user"."user_id", "local_user"."domain_id", "local_user"."name", "local_user"."failed_auth_count", "local_user"."failed_auth_at" FROM "local_user" WHERE ("local_user"."user_id", "local_user"."domain_id") IN (($1, $2), ($3, $4), ($5, $6), ($7, $8))"#,
[]
),
Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"SELECT "password"."id", "password"."local_user_id", "password"."self_service", "password"."created_at", "password"."expires_at", "password"."password_hash", "password"."created_at_int", "password"."expires_at_int" FROM "password" WHERE "password"."local_user_id" IN ($1) ORDER BY "password"."created_at_int" DESC"#,
[]
),
]) {
assert_eq!(
l.statements().iter().map(|x| x.sql.clone()).collect::<Vec<_>>(),
r.statements().iter().map(|x| x.sql.clone()).collect::<Vec<_>>()
);
}
}

#[tokio::test]

async fn test_list_nonlocal_only() {
let db = MockDatabase::new(DatabaseBackend::Postgres)
.append_query_results([vec![
// local user
get_user_mock("1"),
// nonlocal user
get_user_mock("2"),
// federated user
get_user_mock("3"),
// a "bad" user with no user detail records
get_user_mock("4"),
]])
.append_query_results([[get_user_options_mock("2", &UserOptions::default())]
.into_iter()
.flatten()])
.append_query_results([vec![get_nonlocal_user_mock("2")]])
.into_connection();

let config = Config::default();
let res = list(
&config,
&db,
&UserListParameters {
user_type: Some(UserType::NonLocal),
..Default::default()
},
)
.await
.unwrap();
assert_eq!(res.len(), 1, "1 nonlocal user found");

for (l,r) in db.into_transaction_log().iter().zip([
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""#,
[]
),
Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"SELECT "user_option"."user_id", "user_option"."option_id", "user_option"."option_value" FROM "user_option" WHERE "user_option"."user_id" IN ($1, $2, $3, $4)"#,
[]
),
Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"SELECT "nonlocal_user"."domain_id", "nonlocal_user"."name", "nonlocal_user"."user_id" FROM "nonlocal_user" WHERE ("nonlocal_user"."user_id", "nonlocal_user"."domain_id") IN (($1, $2), ($3, $4), ($5, $6), ($7, $8))"#,
[]
),
]) {
assert_eq!(
l.statements().iter().map(|x| x.sql.clone()).collect::<Vec<_>>(),
r.statements().iter().map(|x| x.sql.clone()).collect::<Vec<_>>()
);
}
}

#[tokio::test]
async fn test_list_federated_only() {
let db = MockDatabase::new(DatabaseBackend::Postgres)
.append_query_results([vec![
// local user
get_user_mock("1"),
// nonlocal user
get_user_mock("2"),
// federated user
get_user_mock("3"),
// a "bad" user with no user detail records
get_user_mock("4"),
]])
.append_query_results([[get_user_options_mock("3", &UserOptions::default())]
.into_iter()
.flatten()])
.append_query_results([vec![get_federated_user_mock("3")]])
.into_connection();

let config = Config::default();
let res = list(
&config,
&db,
&UserListParameters {
user_type: Some(UserType::Federated),
..Default::default()
},
)
.await
.unwrap();
assert_eq!(res.len(), 1, "1 federated user found");

for (l,r) in db.into_transaction_log().iter().zip([
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""#,
[]
),
Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"SELECT "user_option"."user_id", "user_option"."option_id", "user_option"."option_value" FROM "user_option" WHERE "user_option"."user_id" IN ($1, $2, $3, $4)"#,
[]
),
Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"SELECT "federated_user"."id", "federated_user"."user_id", "federated_user"."idp_id", "federated_user"."protocol_id", "federated_user"."unique_id", "federated_user"."display_name" FROM "federated_user" WHERE "federated_user"."user_id" IN ($1, $2, $3, $4)"#,
[]
),
]) {
assert_eq!(
l.statements().iter().map(|x| x.sql.clone()).collect::<Vec<_>>(),
r.statements().iter().map(|x| x.sql.clone()).collect::<Vec<_>>()
);
}
}
}
22 changes: 22 additions & 0 deletions src/identity/types/user.rs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine

Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,28 @@ pub struct UserListParameters {
#[builder(default)]
#[validate(length(max = 64))]
pub unique_id: Option<String>,
/// Filter users by User Type (local, federated, nonlocal, all).
#[builder(default)]
#[serde(default, rename = "type")]
pub user_type: Option<UserType>,
}

/// 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.
Expand Down
Loading