diff --git a/Cargo.lock b/Cargo.lock index 3a29a85..92dbf2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -501,6 +501,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + [[package]] name = "boolinator" version = "2.4.0" @@ -541,7 +550,7 @@ dependencies = [ "rand 0.7.3", "serde", "serde_json", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -837,6 +846,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "crypto-mac" version = "0.10.1" @@ -870,7 +889,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder", - "digest", + "digest 0.9.0", "rand_core 0.5.1", "serde", "subtle", @@ -992,6 +1011,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1539,7 +1568,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" dependencies = [ - "digest", + "digest 0.9.0", "hmac 0.11.0", ] @@ -1550,7 +1579,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" dependencies = [ "crypto-mac 0.10.1", - "digest", + "digest 0.9.0", ] [[package]] @@ -1560,7 +1589,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ "crypto-mac 0.11.1", - "digest", + "digest 0.9.0", ] [[package]] @@ -1764,7 +1793,7 @@ dependencies = [ "smartstring", "static_assertions", "url", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -1806,7 +1835,7 @@ checksum = "86e46349d67dc03bdbdb28da0337a355a53ca1d5156452722c36fe21d0e6389b" dependencies = [ "base64", "crypto-mac 0.10.1", - "digest", + "digest 0.9.0", "hmac 0.10.1", "serde", "serde_json", @@ -2002,6 +2031,7 @@ dependencies = [ "tracing-forest", "tracing-log", "tracing-subscriber", + "uuid 1.1.1", ] [[package]] @@ -2035,7 +2065,7 @@ version = "0.3.0-alpha.1" dependencies = [ "chrono", "curve25519-dalek", - "digest", + "digest 0.9.0", "generic-array", "getrandom 0.2.3", "opaque-ke", @@ -2115,11 +2145,20 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.9.0", + "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "md-5" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658646b21e0b72f7866c7038ab086d3d5e1cd6271f060fd37defb241949d0582" +dependencies = [ + "digest 0.10.3", +] + [[package]] name = "memchr" version = "2.4.1" @@ -2404,7 +2443,7 @@ checksum = "26772682ba4fa69f11ae6e4af8bc83946372981ff31a026648d4acb2553c9ee8" dependencies = [ "base64", "curve25519-dalek", - "digest", + "digest 0.9.0", "displaydoc", "generic-array", "generic-bytes", @@ -2938,7 +2977,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e05c2603e2823634ab331437001b411b9ed11660fbc4066f3908c84a9439260d" dependencies = [ "byteorder", - "digest", + "digest 0.9.0", "lazy_static", "num-bigint-dig", "num-integer", @@ -3175,10 +3214,10 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if 1.0.0", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] @@ -3194,10 +3233,10 @@ version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if 1.0.0", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] @@ -3337,7 +3376,7 @@ dependencies = [ "chrono", "crc", "crossbeam-queue", - "digest", + "digest 0.9.0", "dirs", "either", "flume", @@ -3355,7 +3394,7 @@ dependencies = [ "libc", "libsqlite3-sys", "log", - "md-5", + "md-5 0.9.1", "memchr", "num-bigint", "once_cell", @@ -3759,7 +3798,7 @@ dependencies = [ "futures", "tracing", "tracing-futures", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -3955,6 +3994,15 @@ dependencies = [ "getrandom 0.2.3", ] +[[package]] +name = "uuid" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6d5d669b51467dcf7b2f1a796ce0f955f05f01cafda6c19d6e95f730df29238" +dependencies = [ + "md-5 0.10.1", +] + [[package]] name = "validator" version = "0.14.0" diff --git a/server/Cargo.toml b/server/Cargo.toml index fd4aad2..e30c77a 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -17,54 +17,53 @@ anyhow = "*" async-trait = "0.1" base64 = "0.13" bincode = "1.3" -chrono = { version = "*", features = [ "serde" ]} -clap = { version = "3.1.15", features = [ "std", "color", "suggestions", "derive", "env" ] } cron = "*" derive_builder = "0.10.2" futures = "*" futures-util = "*" hmac = "0.10" http = "*" +itertools = "0.10.1" +juniper = "0.15.6" +juniper_actix = "0.4.0" jwt = "0.13" ldap3_server = ">=0.1.9" -lldap_auth = { path = "../auth" } log = "*" -orion = "0.16" native-tls = "0.2.10" +orion = "0.16" serde = "*" serde_json = "1" sha2 = "0.9" sqlx-core = "0.5.11" thiserror = "*" time = "0.2" -tokio = { version = "1.13.1", features = ["full"] } tokio-native-tls = "0.3" -tokio-util = "0.6.3" tokio-stream = "*" +tokio-util = "0.6.3" +tracing = "*" tracing-actix-web = "0.4.0-beta.7" tracing-attributes = "^0.1.21" tracing-log = "*" -rand = { version = "0.8", features = ["small_rng", "getrandom"] } -juniper_actix = "0.4.0" -juniper = "0.15.6" -itertools = "0.10.1" -[dependencies.opaque-ke] -version = "0.6" +[dependencies.chrono] +features = ["serde"] +version = "*" + +[dependencies.clap] +features = ["std", "color", "suggestions", "derive", "env"] +version = "3.1.15" + +[dependencies.figment] +features = ["env", "toml"] +version = "*" [dependencies.tracing-subscriber] version = "0.3" features = ["env-filter", "tracing-log"] [dependencies.lettre] +features = ["builder", "serde", "smtp-transport", "tokio1-native-tls", "tokio1"] version = "0.10.0-rc.3" -features = [ - "builder", - "serde", - "smtp-transport", - "tokio1-native-tls", - "tokio1", -] [dependencies.sqlx] version = "0.5.11" @@ -78,6 +77,9 @@ features = [ "sqlite", ] +[dependencies.lldap_auth] +path = "../auth" + [dependencies.sea-query] version = "^0.25" features = ["with-chrono", "sqlx-sqlite"] @@ -86,24 +88,32 @@ features = ["with-chrono", "sqlx-sqlite"] version = "*" features = ["with-chrono", "sqlx-sqlite", "sqlx-any"] -[dependencies.figment] -features = ["env", "toml"] +[dependencies.opaque-ke] +version = "0.6" + +[dependencies.openssl-sys] +features = ["vendored"] version = "*" +[dependencies.rand] +features = ["small_rng", "getrandom"] +version = "0.8" + [dependencies.secstr] features = ["serde"] version = "*" -[dependencies.openssl-sys] -features = ["vendored"] +[dependencies.tokio] +features = ["full"] +version = "1.13.1" + +[dependencies.uuid] +features = ["v3"] version = "*" [dependencies.tracing-forest] features = ["smallvec", "chrono", "tokio"] version = "^0.1.4" -[dependencies.tracing] -version = "*" - [dev-dependencies] mockall = "0.9.1" diff --git a/server/src/domain/handler.rs b/server/src/domain/handler.rs index 0efc41f..b01c292 100644 --- a/server/src/domain/handler.rs +++ b/server/src/domain/handler.rs @@ -3,9 +3,57 @@ use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::collections::HashSet; -#[derive(PartialEq, Eq, Clone, Debug, Default, Serialize, Deserialize)] -#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +#[derive( + PartialEq, Hash, Eq, Clone, Debug, Default, Serialize, Deserialize, sqlx::FromRow, sqlx::Type, +)] +#[serde(try_from = "&str")] +#[sqlx(transparent)] +pub struct Uuid(String); + +impl Uuid { + pub fn from_name_and_date(name: &str, creation_date: &chrono::DateTime) -> Self { + Uuid( + uuid::Uuid::new_v3( + &uuid::Uuid::NAMESPACE_X500, + &[name.as_bytes(), creation_date.to_rfc3339().as_bytes()].concat(), + ) + .to_string(), + ) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn into_string(self) -> String { + self.0 + } +} + +impl<'a> std::convert::TryFrom<&'a str> for Uuid { + type Error = anyhow::Error; + fn try_from(s: &'a str) -> anyhow::Result { + Ok(Uuid(uuid::Uuid::parse_str(s)?.to_string())) + } +} + +impl std::string::ToString for Uuid { + fn to_string(&self) -> String { + self.0.clone() + } +} + +#[cfg(test)] +#[macro_export] +macro_rules! uuid { + ($s:literal) => { + crate::domain::handler::Uuid::try_from($s).unwrap() + }; +} + +#[derive(PartialEq, Eq, Clone, Debug, Default, Serialize, Deserialize, sqlx::Type)] #[serde(from = "String")] +#[sqlx(transparent)] pub struct UserId(String); impl UserId { @@ -34,8 +82,7 @@ impl From for UserId { } } -#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct User { pub user_id: UserId, pub email: String, @@ -44,18 +91,22 @@ pub struct User { pub last_name: String, // pub avatar: ?, pub creation_date: chrono::DateTime, + pub uuid: Uuid, } +#[cfg(test)] impl Default for User { fn default() -> Self { use chrono::TimeZone; + let epoch = chrono::Utc.timestamp(0, 0); User { user_id: UserId::default(), email: String::new(), display_name: String::new(), first_name: String::new(), last_name: String::new(), - creation_date: chrono::Utc.timestamp(0, 0), + creation_date: epoch, + uuid: Uuid::from_name_and_date("", &epoch), } } } @@ -64,6 +115,8 @@ impl Default for User { pub struct Group { pub id: GroupId, pub display_name: String, + pub creation_date: chrono::DateTime, + pub uuid: Uuid, pub users: Vec, } @@ -92,6 +145,7 @@ pub enum GroupRequestFilter { Or(Vec), Not(Box), DisplayName(String), + Uuid(Uuid), GroupId(GroupId), // Check if the group contains a user identified by uid. Member(UserId), @@ -128,16 +182,22 @@ pub trait LoginHandler: Clone + Send { async fn bind(&self, request: BindRequest) -> Result<()>; } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] +#[sqlx(transparent)] pub struct GroupId(pub i32); #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::FromRow)] -pub struct GroupIdAndName(pub GroupId, pub String); +pub struct GroupDetails { + pub group_id: GroupId, + pub display_name: String, + pub creation_date: chrono::DateTime, + pub uuid: Uuid, +} #[derive(Debug, Clone, PartialEq)] pub struct UserAndGroups { pub user: User, - pub groups: Option>, + pub groups: Option>, } #[async_trait] @@ -149,7 +209,7 @@ pub trait BackendHandler: Clone + Send { ) -> Result>; async fn list_groups(&self, filters: Option) -> Result>; async fn get_user_details(&self, user_id: &UserId) -> Result; - async fn get_group_details(&self, group_id: GroupId) -> Result; + async fn get_group_details(&self, group_id: GroupId) -> Result; async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; @@ -158,7 +218,7 @@ pub trait BackendHandler: Clone + Send { async fn delete_group(&self, group_id: GroupId) -> Result<()>; async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>; async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>; - async fn get_user_groups(&self, user_id: &UserId) -> Result>; + async fn get_user_groups(&self, user_id: &UserId) -> Result>; } #[cfg(test)] @@ -172,14 +232,14 @@ mockall::mock! { async fn list_users(&self, filters: Option, get_groups: bool) -> Result>; async fn list_groups(&self, filters: Option) -> Result>; async fn get_user_details(&self, user_id: &UserId) -> Result; - async fn get_group_details(&self, group_id: GroupId) -> Result; + async fn get_group_details(&self, group_id: GroupId) -> Result; async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; async fn delete_user(&self, user_id: &UserId) -> Result<()>; async fn create_group(&self, group_name: &str) -> Result; async fn delete_group(&self, group_id: GroupId) -> Result<()>; - async fn get_user_groups(&self, user_id: &UserId) -> Result>; + async fn get_user_groups(&self, user_id: &UserId) -> Result>; async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>; async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>; } @@ -188,3 +248,19 @@ mockall::mock! { async fn bind(&self, request: BindRequest) -> Result<()>; } } + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_uuid_time() { + use chrono::prelude::*; + let user_id = "bob"; + let date1 = Utc.ymd(2014, 7, 8).and_hms(9, 10, 11); + let date2 = Utc.ymd(2014, 7, 8).and_hms(9, 10, 12); + assert_ne!( + Uuid::from_name_and_date(user_id, &date1), + Uuid::from_name_and_date(user_id, &date2) + ); + } +} diff --git a/server/src/domain/sql_backend_handler.rs b/server/src/domain/sql_backend_handler.rs index 27510d1..c808d14 100644 --- a/server/src/domain/sql_backend_handler.rs +++ b/server/src/domain/sql_backend_handler.rs @@ -98,6 +98,7 @@ fn get_group_filter_expr(filter: GroupRequestFilter) -> SimpleExpr { Not(f) => Expr::not(Expr::expr(get_group_filter_expr(*f))), DisplayName(name) => Expr::col((Groups::Table, Groups::DisplayName)).eq(name), GroupId(id) => Expr::col((Groups::Table, Groups::GroupId)).eq(id.0), + Uuid(uuid) => Expr::col((Groups::Table, Groups::Uuid)).eq(uuid.to_string()), // WHERE (group_id in (SELECT group_id FROM memberships WHERE user_id = user)) Member(user) => Expr::col((Memberships::Table, Memberships::GroupId)).in_subquery( Query::select() @@ -126,7 +127,8 @@ impl BackendHandler for SqlBackendHandler { .column(Users::FirstName) .column(Users::LastName) .column(Users::Avatar) - .column(Users::CreationDate) + .column((Users::Table, Users::CreationDate)) + .column((Users::Table, Users::Uuid)) .from(Users::Table) .order_by((Users::Table, Users::UserId), Order::Asc) .to_owned(); @@ -151,6 +153,14 @@ impl BackendHandler for SqlBackendHandler { Expr::col((Groups::Table, Groups::DisplayName)), Alias::new("group_display_name"), ) + .expr_as( + Expr::col((Groups::Table, Groups::CreationDate)), + sea_query::Alias::new("group_creation_date"), + ) + .expr_as( + Expr::col((Groups::Table, Groups::Uuid)), + sea_query::Alias::new("group_uuid"), + ) .order_by(Alias::new("group_display_name"), Order::Asc); } if let Some(filter) = filters { @@ -189,13 +199,14 @@ impl BackendHandler for SqlBackendHandler { user: User::from_row(rows.peek().unwrap()).unwrap(), groups: if get_groups { Some( - rows.map(|row| { - GroupIdAndName( - GroupId(row.get::(&*Groups::GroupId.to_string())), - row.get::("group_display_name"), - ) + rows.map(|row| GroupDetails { + group_id: row.get::(&*Groups::GroupId.to_string()), + display_name: row.get::("group_display_name"), + creation_date: row + .get::, _>("group_creation_date"), + uuid: row.get::("group_uuid"), }) - .filter(|g| !g.1.is_empty()) + .filter(|g| !g.display_name.is_empty()) .collect(), ) } else { @@ -213,6 +224,8 @@ impl BackendHandler for SqlBackendHandler { let mut query_builder = Query::select() .column((Groups::Table, Groups::GroupId)) .column(Groups::DisplayName) + .column(Groups::CreationDate) + .column(Groups::Uuid) .column(Memberships::UserId) .from(Groups::Table) .left_join( @@ -245,20 +258,17 @@ impl BackendHandler for SqlBackendHandler { let mut groups = Vec::new(); // The rows are returned sorted by display_name, equivalent to group_id. We group them by // this key which gives us one element (`rows`) per group. - for ((group_id, display_name), rows) in &query_with(query.as_str(), values) + for (group_details, rows) in &query_with(&query, values) .fetch_all(&self.sql_pool) .await? .into_iter() - .group_by(|row| { - ( - GroupId(row.get::(&*Groups::GroupId.to_string())), - row.get::(&*Groups::DisplayName.to_string()), - ) - }) + .group_by(|row| GroupDetails::from_row(row).unwrap()) { groups.push(Group { - id: group_id, - display_name, + id: group_details.group_id, + display_name: group_details.display_name, + creation_date: group_details.creation_date, + uuid: group_details.uuid, users: rows .map(|row| row.get::(&*Memberships::UserId.to_string())) // If a group has no users, an empty string is returned because of the left @@ -281,6 +291,7 @@ impl BackendHandler for SqlBackendHandler { .column(Users::LastName) .column(Users::Avatar) .column(Users::CreationDate) + .column(Users::Uuid) .from(Users::Table) .cond_where(Expr::col(Users::UserId).eq(user_id)) .build_sqlx(DbQueryBuilder {}); @@ -292,29 +303,31 @@ impl BackendHandler for SqlBackendHandler { } #[instrument(skip_all, level = "debug", ret, err)] - async fn get_group_details(&self, group_id: GroupId) -> Result { + async fn get_group_details(&self, group_id: GroupId) -> Result { debug!(?group_id); let (query, values) = Query::select() .column(Groups::GroupId) .column(Groups::DisplayName) + .column(Groups::CreationDate) + .column(Groups::Uuid) .from(Groups::Table) .cond_where(Expr::col(Groups::GroupId).eq(group_id)) .build_sqlx(DbQueryBuilder {}); debug!(%query); - Ok( - query_as_with::<_, GroupIdAndName, _>(query.as_str(), values) - .fetch_one(&self.sql_pool) - .await?, - ) + Ok(query_as_with::<_, GroupDetails, _>(&query, values) + .fetch_one(&self.sql_pool) + .await?) } #[instrument(skip_all, level = "debug", ret, err)] - async fn get_user_groups(&self, user_id: &UserId) -> Result> { + async fn get_user_groups(&self, user_id: &UserId) -> Result> { debug!(?user_id); let (query, values) = Query::select() .column((Groups::Table, Groups::GroupId)) .column(Groups::DisplayName) + .column(Groups::CreationDate) + .column(Groups::Uuid) .from(Groups::Table) .inner_join( Memberships::Table, @@ -325,17 +338,10 @@ impl BackendHandler for SqlBackendHandler { .build_sqlx(DbQueryBuilder {}); debug!(%query); - query_with(query.as_str(), values) - // Extract the group id from the row. - .map(|row: DbRow| { - GroupIdAndName( - row.get::(&*Groups::GroupId.to_string()), - row.get::(&*Groups::DisplayName.to_string()), - ) - }) + query_as_with::<_, GroupDetails, _>(&query, values) .fetch(&self.sql_pool) // Collect the vector of rows, each potentially an error. - .collect::>>() + .collect::>>() .await .into_iter() // Transform it into a single result (the first error if any), and group the group_ids @@ -355,18 +361,23 @@ impl BackendHandler for SqlBackendHandler { Users::FirstName, Users::LastName, Users::CreationDate, + Users::Uuid, + ]; + let now = chrono::Utc::now(); + let uuid = Uuid::from_name_and_date(request.user_id.as_str(), &now); + let values = vec![ + request.user_id.into(), + request.email.into(), + request.display_name.unwrap_or_default().into(), + request.first_name.unwrap_or_default().into(), + request.last_name.unwrap_or_default().into(), + now.naive_utc().into(), + uuid.into(), ]; let (query, values) = Query::insert() .into_table(Users::Table) .columns(columns) - .values_panic(vec![ - request.user_id.into(), - request.email.into(), - request.display_name.unwrap_or_default().into(), - request.first_name.unwrap_or_default().into(), - request.last_name.unwrap_or_default().into(), - chrono::Utc::now().naive_utc().into(), - ]) + .values_panic(values) .build_sqlx(DbQueryBuilder {}); debug!(%query); query_with(query.as_str(), values) @@ -445,10 +456,19 @@ impl BackendHandler for SqlBackendHandler { #[instrument(skip_all, level = "debug", ret, err)] async fn create_group(&self, group_name: &str) -> Result { debug!(?group_name); + let now = chrono::Utc::now(); let (query, values) = Query::insert() .into_table(Groups::Table) - .columns(vec![Groups::DisplayName]) - .values_panic(vec![group_name.into()]) + .columns(vec![ + Groups::DisplayName, + Groups::CreationDate, + Groups::Uuid, + ]) + .values_panic(vec![ + group_name.into(), + now.naive_utc().into(), + Uuid::from_name_and_date(group_name, &now).into(), + ]) .build_sqlx(DbQueryBuilder {}); debug!(%query); query_with(query.as_str(), values) @@ -707,7 +727,7 @@ mod tests { u.groups .unwrap() .into_iter() - .map(|g| g.0) + .map(|g| g.group_id) .collect::>(), ) }) @@ -721,6 +741,29 @@ mod tests { ] ); } + { + let users = handler + .list_users(None, true) + .await + .unwrap() + .into_iter() + .map(|u| { + ( + u.user.creation_date, + u.groups + .unwrap() + .into_iter() + .map(|g| g.creation_date) + .collect::>(), + ) + }) + .collect::>(); + for (user_date, groups) in users { + for group_date in groups { + assert_ne!(user_date, group_date); + } + } + } } #[tokio::test] @@ -738,62 +781,33 @@ mod tests { insert_membership(&handler, group_1, "patrick").await; insert_membership(&handler, group_2, "patrick").await; insert_membership(&handler, group_2, "John").await; + let get_group_ids = |filter| async { + handler + .list_groups(filter) + .await + .unwrap() + .into_iter() + .map(|g| g.id) + .collect::>() + }; + assert_eq!(get_group_ids(None).await, vec![group_1, group_3, group_2]); assert_eq!( - handler.list_groups(None).await.unwrap(), - vec![ - Group { - id: group_1, - display_name: "Best Group".to_string(), - users: vec![UserId::new("bob"), UserId::new("patrick")] - }, - Group { - id: group_3, - display_name: "Empty Group".to_string(), - users: vec![] - }, - Group { - id: group_2, - display_name: "Worst Group".to_string(), - users: vec![UserId::new("john"), UserId::new("patrick")] - }, - ] + get_group_ids(Some(GroupRequestFilter::Or(vec![ + GroupRequestFilter::DisplayName("Empty Group".to_string()), + GroupRequestFilter::Member(UserId::new("bob")), + ]))) + .await, + vec![group_1, group_3] ); assert_eq!( - handler - .list_groups(Some(GroupRequestFilter::Or(vec![ - GroupRequestFilter::DisplayName("Empty Group".to_string()), - GroupRequestFilter::Member(UserId::new("bob")), - ]))) - .await - .unwrap(), - vec![ - Group { - id: group_1, - display_name: "Best Group".to_string(), - users: vec![UserId::new("bob"), UserId::new("patrick")] - }, - Group { - id: group_3, - display_name: "Empty Group".to_string(), - users: vec![] - }, - ] - ); - assert_eq!( - handler - .list_groups(Some(GroupRequestFilter::And(vec![ - GroupRequestFilter::Not(Box::new(GroupRequestFilter::DisplayName( - "value".to_string() - ))), - GroupRequestFilter::GroupId(group_1), - ]))) - .await - .unwrap(), - vec![Group { - id: group_1, - display_name: "Best Group".to_string(), - users: vec![UserId::new("bob"), UserId::new("patrick")] - }] + get_group_ids(Some(GroupRequestFilter::And(vec![ + GroupRequestFilter::Not(Box::new(GroupRequestFilter::DisplayName( + "value".to_string() + ))), + GroupRequestFilter::GroupId(group_1), + ]))) + .await, + vec![group_1] ); } @@ -846,26 +860,20 @@ mod tests { insert_membership(&handler, group_1, "bob").await; insert_membership(&handler, group_1, "patrick").await; insert_membership(&handler, group_2, "patrick").await; - let mut bob_groups = HashSet::new(); - bob_groups.insert(GroupIdAndName(group_1, "Group1".to_string())); - let mut patrick_groups = HashSet::new(); - patrick_groups.insert(GroupIdAndName(group_1, "Group1".to_string())); - patrick_groups.insert(GroupIdAndName(group_2, "Group2".to_string())); - assert_eq!( - handler.get_user_groups(&UserId::new("bob")).await.unwrap(), - bob_groups - ); - assert_eq!( - handler - .get_user_groups(&UserId::new("patrick")) + let get_group_ids = |user: &'static str| async { + let mut groups = handler + .get_user_groups(&UserId::new(user)) .await - .unwrap(), - patrick_groups - ); - assert_eq!( - handler.get_user_groups(&UserId::new("John")).await.unwrap(), - HashSet::new() - ); + .unwrap() + .into_iter() + .map(|g| g.group_id) + .collect::>(); + groups.sort_by(|g1, g2| g1.0.cmp(&g2.0)); + groups + }; + assert_eq!(get_group_ids("bob").await, vec![group_1]); + assert_eq!(get_group_ids("patrick").await, vec![group_1, group_2]); + assert_eq!(get_group_ids("John").await, vec![]); } #[tokio::test] diff --git a/server/src/domain/sql_tables.rs b/server/src/domain/sql_tables.rs index ae644ef..6b64865 100644 --- a/server/src/domain/sql_tables.rs +++ b/server/src/domain/sql_tables.rs @@ -1,5 +1,7 @@ -use super::handler::{GroupId, UserId}; +use super::handler::{GroupId, UserId, Uuid}; use sea_query::*; +use sqlx::Row; +use tracing::warn; pub type Pool = sqlx::sqlite::SqlitePool; pub type PoolOptions = sqlx::sqlite::SqlitePoolOptions; @@ -12,56 +14,6 @@ impl From for Value { } } -impl sqlx::Type for GroupId -where - DB: sqlx::Database, - i32: sqlx::Type, -{ - fn type_info() -> ::TypeInfo { - >::type_info() - } - fn compatible(ty: &::TypeInfo) -> bool { - >::compatible(ty) - } -} - -impl<'r, DB> sqlx::Decode<'r, DB> for GroupId -where - DB: sqlx::Database, - i32: sqlx::Decode<'r, DB>, -{ - fn decode( - value: >::ValueRef, - ) -> Result> { - >::decode(value).map(GroupId) - } -} - -impl sqlx::Type for UserId -where - DB: sqlx::Database, - String: sqlx::Type, -{ - fn type_info() -> ::TypeInfo { - >::type_info() - } - fn compatible(ty: &::TypeInfo) -> bool { - >::compatible(ty) - } -} - -impl<'r, DB> sqlx::Decode<'r, DB> for UserId -where - DB: sqlx::Database, - String: sqlx::Decode<'r, DB>, -{ - fn decode( - value: >::ValueRef, - ) -> Result> { - >::decode(value).map(|s| UserId::new(&s)) - } -} - impl From for sea_query::Value { fn from(user_id: UserId) -> Self { user_id.into_string().into() @@ -74,6 +26,18 @@ impl From<&UserId> for sea_query::Value { } } +impl From for sea_query::Value { + fn from(uuid: Uuid) -> Self { + uuid.as_str().into() + } +} + +impl From<&Uuid> for sea_query::Value { + fn from(uuid: &Uuid) -> Self { + uuid.as_str().into() + } +} + #[derive(Iden)] pub enum Users { Table, @@ -87,6 +51,7 @@ pub enum Users { PasswordHash, TotpSecret, MfaType, + Uuid, } #[derive(Iden)] @@ -94,6 +59,8 @@ pub enum Groups { Table, GroupId, DisplayName, + CreationDate, + Uuid, } #[derive(Iden)] @@ -103,6 +70,19 @@ pub enum Memberships { GroupId, } +async fn column_exists(pool: &Pool, table_name: &str, column_name: &str) -> sqlx::Result { + // Sqlite specific + let query = format!( + "SELECT COUNT(*) AS col_count FROM pragma_table_info('{}') WHERE name = '{}'", + table_name, column_name + ); + Ok(sqlx::query(&query) + .fetch_one(pool) + .await? + .get::("col_count") + > 0) +} + pub async fn init_table(pool: &Pool) -> sqlx::Result<()> { // SQLite needs this pragma to be turned on. Other DB might not understand this, so ignore the // error. @@ -130,6 +110,7 @@ pub async fn init_table(pool: &Pool) -> sqlx::Result<()> { .col(ColumnDef::new(Users::PasswordHash).binary()) .col(ColumnDef::new(Users::TotpSecret).string_len(64)) .col(ColumnDef::new(Users::MfaType).string_len(64)) + .col(ColumnDef::new(Users::Uuid).string_len(36).not_null()) .to_string(DbQueryBuilder {}), ) .execute(pool) @@ -151,11 +132,141 @@ pub async fn init_table(pool: &Pool) -> sqlx::Result<()> { .unique_key() .not_null(), ) + .col(ColumnDef::new(Users::CreationDate).date_time().not_null()) + .col(ColumnDef::new(Users::Uuid).string_len(36).not_null()) .to_string(DbQueryBuilder {}), ) .execute(pool) .await?; + // If the creation_date column doesn't exist, add it. + if !column_exists( + pool, + &*Groups::Table.to_string(), + &*Groups::CreationDate.to_string(), + ) + .await? + { + warn!("`creation_date` column not found in `groups`, creating it"); + sqlx::query( + &Table::alter() + .table(Groups::Table) + .add_column( + ColumnDef::new(Groups::CreationDate) + .date_time() + .not_null() + .default(chrono::Utc::now().naive_utc()), + ) + .to_string(DbQueryBuilder {}), + ) + .execute(pool) + .await?; + } + + // If the uuid column doesn't exist, add it. + if !column_exists( + pool, + &*Groups::Table.to_string(), + &*Groups::Uuid.to_string(), + ) + .await? + { + warn!("`uuid` column not found in `groups`, creating it"); + sqlx::query( + &Table::alter() + .table(Groups::Table) + .add_column( + ColumnDef::new(Groups::Uuid) + .string_len(36) + .not_null() + .default(""), + ) + .to_string(DbQueryBuilder {}), + ) + .execute(pool) + .await?; + for row in sqlx::query( + &Query::select() + .from(Groups::Table) + .column(Groups::GroupId) + .column(Groups::DisplayName) + .column(Groups::CreationDate) + .to_string(DbQueryBuilder {}), + ) + .fetch_all(pool) + .await? + { + sqlx::query( + &Query::update() + .table(Groups::Table) + .value( + Groups::Uuid, + Uuid::from_name_and_date( + &row.get::(&*Groups::DisplayName.to_string()), + &row.get::, _>( + &*Groups::CreationDate.to_string(), + ), + ) + .into(), + ) + .and_where( + Expr::col(Groups::GroupId) + .eq(row.get::(&*Groups::GroupId.to_string())), + ) + .to_string(DbQueryBuilder {}), + ) + .execute(pool) + .await?; + } + } + + if !column_exists(pool, &*Users::Table.to_string(), &*Users::Uuid.to_string()).await? { + warn!("`uuid` column not found in `users`, creating it"); + sqlx::query( + &Table::alter() + .table(Users::Table) + .add_column( + ColumnDef::new(Users::Uuid) + .string_len(36) + .not_null() + .default(""), + ) + .to_string(DbQueryBuilder {}), + ) + .execute(pool) + .await?; + for row in sqlx::query( + &Query::select() + .from(Users::Table) + .column(Users::UserId) + .column(Users::CreationDate) + .to_string(DbQueryBuilder {}), + ) + .fetch_all(pool) + .await? + { + let user_id = row.get::(&*Users::UserId.to_string()); + sqlx::query( + &Query::update() + .table(Users::Table) + .value( + Users::Uuid, + Uuid::from_name_and_date( + user_id.as_str(), + &row.get::, _>( + &*Users::CreationDate.to_string(), + ), + ) + .into(), + ) + .and_where(Expr::col(Users::UserId).eq(user_id)) + .to_string(DbQueryBuilder {}), + ) + .execute(pool) + .await?; + } + } + sqlx::query( &Table::create() .table(Memberships::Table) @@ -196,13 +307,13 @@ mod tests { use chrono::prelude::*; use sqlx::{Column, Row}; - #[actix_rt::test] + #[tokio::test] async fn test_init_table() { let sql_pool = PoolOptions::new().connect("sqlite::memory:").await.unwrap(); init_table(&sql_pool).await.unwrap(); sqlx::query(r#"INSERT INTO users - (user_id, email, display_name, first_name, last_name, creation_date, password_hash) - VALUES ("bôb", "böb@bob.bob", "Bob Bobbersön", "Bob", "Bobberson", "1970-01-01 00:00:00", "bob00")"#).execute(&sql_pool).await.unwrap(); + (user_id, email, display_name, first_name, last_name, creation_date, password_hash, uuid) + VALUES ("bôb", "böb@bob.bob", "Bob Bobbersön", "Bob", "Bobberson", "1970-01-01 00:00:00", "bob00", "abc")"#).execute(&sql_pool).await.unwrap(); let row = sqlx::query(r#"SELECT display_name, creation_date FROM users WHERE user_id = "bôb""#) .fetch_one(&sql_pool) @@ -216,10 +327,49 @@ mod tests { ); } - #[actix_rt::test] + #[tokio::test] async fn test_already_init_table() { let sql_pool = PoolOptions::new().connect("sqlite::memory:").await.unwrap(); init_table(&sql_pool).await.unwrap(); init_table(&sql_pool).await.unwrap(); } + + #[tokio::test] + async fn test_migrate_tables() { + // Test that we add the column creation_date to groups and uuid to users and groups. + let sql_pool = PoolOptions::new().connect("sqlite::memory:").await.unwrap(); + sqlx::query(r#"CREATE TABLE users ( user_id TEXT , creation_date TEXT);"#) + .execute(&sql_pool) + .await + .unwrap(); + sqlx::query( + r#"INSERT INTO users (user_id, creation_date) + VALUES ("bôb", "1970-01-01 00:00:00")"#, + ) + .execute(&sql_pool) + .await + .unwrap(); + sqlx::query(r#"CREATE TABLE groups ( group_id int, display_name TEXT );"#) + .execute(&sql_pool) + .await + .unwrap(); + init_table(&sql_pool).await.unwrap(); + sqlx::query( + r#"INSERT INTO groups (group_id, display_name, creation_date, uuid) + VALUES (3, "test", "1970-01-01 00:00:00", "abc")"#, + ) + .execute(&sql_pool) + .await + .unwrap(); + assert_eq!( + sqlx::query(r#"SELECT uuid FROM users"#) + .fetch_all(&sql_pool) + .await + .unwrap() + .into_iter() + .map(|row| row.get::("uuid")) + .collect::>(), + vec![crate::uuid!("a02eaf13-48a7-30f6-a3d4-040ff7c52b04")] + ); + } } diff --git a/server/src/infra/auth_service.rs b/server/src/infra/auth_service.rs index b6d4869..e96814e 100644 --- a/server/src/infra/auth_service.rs +++ b/server/src/infra/auth_service.rs @@ -25,7 +25,7 @@ use lldap_auth::{login, password_reset, registration, JWTClaims}; use crate::{ domain::{ error::DomainError, - handler::{BackendHandler, BindRequest, GroupIdAndName, LoginHandler, UserId}, + handler::{BackendHandler, BindRequest, GroupDetails, LoginHandler, UserId}, opaque_handler::OpaqueHandler, }, infra::{ @@ -37,12 +37,12 @@ use crate::{ type Token = jwt::Token; type SignedToken = Token; -fn create_jwt(key: &Hmac, user: String, groups: HashSet) -> SignedToken { +fn create_jwt(key: &Hmac, user: String, groups: HashSet) -> SignedToken { let claims = JWTClaims { exp: Utc::now() + chrono::Duration::days(1), iat: Utc::now(), user, - groups: groups.into_iter().map(|g| g.1).collect(), + groups: groups.into_iter().map(|g| g.display_name).collect(), }; let header = jwt::Header { algorithm: jwt::AlgorithmType::Hs512, diff --git a/server/src/infra/graphql/query.rs b/server/src/infra/graphql/query.rs index 0747eb1..799cc35 100644 --- a/server/src/infra/graphql/query.rs +++ b/server/src/infra/graphql/query.rs @@ -1,4 +1,4 @@ -use crate::domain::handler::{BackendHandler, GroupId, GroupIdAndName, UserId}; +use crate::domain::handler::{BackendHandler, GroupDetails, GroupId, UserId}; use juniper::{graphql_object, FieldResult, GraphQLInputObject}; use serde::{Deserialize, Serialize}; use tracing::{debug, debug_span, Instrument}; @@ -184,6 +184,7 @@ pub struct User { _phantom: std::marker::PhantomData>, } +#[cfg(test)] impl Default for User { fn default() -> Self { Self { @@ -257,6 +258,7 @@ impl From for User { pub struct Group { group_id: i32, display_name: String, + creation_date: chrono::DateTime, members: Option>, _phantom: std::marker::PhantomData>, } @@ -291,11 +293,12 @@ impl Group { } } -impl From for Group { - fn from(group_id_and_name: GroupIdAndName) -> Self { +impl From for Group { + fn from(group_details: GroupDetails) -> Self { Self { - group_id: group_id_and_name.0 .0, - display_name: group_id_and_name.1, + group_id: group_details.group_id.0, + display_name: group_details.display_name, + creation_date: group_details.creation_date, members: None, _phantom: std::marker::PhantomData, } @@ -307,6 +310,7 @@ impl From for Group { Self { group_id: group.id.0, display_name: group.display_name, + creation_date: group.creation_date, members: Some(group.users.into_iter().map(UserId::into_string).collect()), _phantom: std::marker::PhantomData, } @@ -320,6 +324,7 @@ mod tests { domain::handler::{MockTestBackendHandler, UserRequestFilter}, infra::auth_service::ValidationResults, }; + use chrono::TimeZone; use juniper::{ execute, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, RootNode, Variables, @@ -361,7 +366,12 @@ mod tests { }) }); let mut groups = HashSet::new(); - groups.insert(GroupIdAndName(GroupId(3), "Bobbersons".to_string())); + groups.insert(GroupDetails { + group_id: GroupId(3), + display_name: "Bobbersons".to_string(), + creation_date: chrono::Utc.timestamp_nanos(42), + uuid: crate::uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), + }); mock.expect_get_user_groups() .with(eq(UserId::new("bob"))) .return_once(|_| Ok(groups)); diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index a488b51..7464745 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -1,8 +1,8 @@ use crate::{ domain::{ handler::{ - BackendHandler, BindRequest, Group, GroupIdAndName, GroupRequestFilter, LoginHandler, - User, UserId, UserRequestFilter, + BackendHandler, BindRequest, Group, GroupDetails, GroupRequestFilter, LoginHandler, + User, UserId, UserRequestFilter, Uuid, }, opaque_handler::OpaqueHandler, }, @@ -153,7 +153,7 @@ fn get_user_attribute( attribute: &str, dn: &str, base_dn_str: &str, - groups: Option<&[GroupIdAndName]>, + groups: Option<&[GroupDetails]>, ignored_user_attributes: &[String], ) -> Result>> { let attribute = attribute.to_ascii_lowercase(); @@ -166,13 +166,19 @@ fn get_user_attribute( ], "dn" | "distinguishedname" => vec![dn.to_string()], "uid" => vec![user.user_id.to_string()], + "entryuuid" => vec![user.uuid.to_string()], "mail" => vec![user.email.clone()], "givenname" => vec![user.first_name.clone()], "sn" => vec![user.last_name.clone()], "memberof" => groups .into_iter() .flatten() - .map(|id_and_name| format!("uid={},ou=groups,{}", &id_and_name.1, base_dn_str)) + .map(|id_and_name| { + format!( + "uid={},ou=groups,{}", + &id_and_name.display_name, base_dn_str + ) + }) .collect(), "cn" | "displayname" => vec![user.display_name.clone()], "createtimestamp" | "modifytimestamp" => vec![user.creation_date.to_rfc3339()], @@ -233,7 +239,7 @@ fn make_ldap_search_user_result_entry( user: User, base_dn_str: &str, attributes: &[String], - groups: Option<&[GroupIdAndName]>, + groups: Option<&[GroupDetails]>, ignored_user_attributes: &[String], ) -> Result { let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str); @@ -278,6 +284,7 @@ fn get_group_attribute( group.display_name, base_dn_str )], "cn" | "uid" => vec![group.display_name.clone()], + "entryuuid" => vec![group.uuid.to_string()], "member" | "uniquemember" => group .users .iter() @@ -363,24 +370,18 @@ fn is_subtree(subtree: &[(String, String)], base_tree: &[(String, String)]) -> b true } -fn map_field(field: &str) -> Result { +fn map_field(field: &str) -> Result<&'static str> { assert!(field == field.to_ascii_lowercase()); - Ok(if field == "uid" { - "user_id".to_string() - } else if field == "mail" { - "email".to_string() - } else if field == "cn" || field == "displayname" { - "display_name".to_string() - } else if field == "givenname" { - "first_name".to_string() - } else if field == "sn" { - "last_name".to_string() - } else if field == "avatar" { - "avatar".to_string() - } else if field == "creationdate" || field == "createtimestamp" || field == "modifytimestamp" { - "creation_date".to_string() - } else { - bail!("Unknown field: {}", field); + Ok(match field { + "uid" => "user_id", + "mail" => "email", + "cn" | "displayname" => "display_name", + "givenname" => "first_name", + "sn" => "last_name", + "avatar" => "avatar", + "creationdate" | "createtimestamp" | "modifytimestamp" => "creation_date", + "entryuuid" => "uuid", + _ => bail!("Unknown field: {}", field), }) } @@ -499,7 +500,7 @@ impl LdapHandler LdapHandler { let field = &field.to_ascii_lowercase(); let value = &value.to_ascii_lowercase(); - if field == "member" || field == "uniquemember" { - let user_name = get_user_id_from_distinguished_name( - value, - &self.base_dn, - &self.base_dn_str, - )?; - Ok(GroupRequestFilter::Member(user_name)) - } else if field == "objectclass" { - if value == "groupofuniquenames" || value == "groupofnames" { - Ok(GroupRequestFilter::And(vec![])) - } else { - Ok(GroupRequestFilter::Not(Box::new(GroupRequestFilter::And( - vec![], - )))) + match field.as_str() { + "member" | "uniquemember" => { + let user_name = get_user_id_from_distinguished_name( + value, + &self.base_dn, + &self.base_dn_str, + )?; + Ok(GroupRequestFilter::Member(user_name)) } - } else { - let mapped_field = map_field(field); - if mapped_field.is_ok() - && (mapped_field.as_ref().unwrap() == "display_name" - || mapped_field.as_ref().unwrap() == "user_id") - { - Ok(GroupRequestFilter::DisplayName(value.clone())) - } else { + "objectclass" => { + if value == "groupofuniquenames" || value == "groupofnames" { + Ok(GroupRequestFilter::And(vec![])) + } else { + Ok(GroupRequestFilter::Not(Box::new(GroupRequestFilter::And( + vec![], + )))) + } + } + _ => { + match map_field(field) { + Ok("display_name") | Ok("user_id") => { + return Ok(GroupRequestFilter::DisplayName(value.clone())); + } + Ok("uuid") => { + return Ok(GroupRequestFilter::Uuid(Uuid::try_from( + value.as_str(), + )?)); + } + _ => (), + }; if !self.ignored_group_attributes.contains(field) { warn!( r#"Ignoring unknown group attribute "{:?}" in filter.\n\ @@ -928,32 +936,37 @@ impl LdapHandler { let field = &field.to_ascii_lowercase(); - if field == "memberof" { - let group_name = get_group_id_from_distinguished_name( - &value.to_ascii_lowercase(), - &self.base_dn, - &self.base_dn_str, - )?; - Ok(UserRequestFilter::MemberOf(group_name)) - } else if field == "objectclass" { - if value == "person" - || value == "inetOrgPerson" - || value == "posixAccount" - || value == "mailAccount" - { - Ok(UserRequestFilter::And(vec![])) - } else { - Ok(UserRequestFilter::Not(Box::new(UserRequestFilter::And( - vec![], - )))) + match field.as_str() { + "memberof" => { + let group_name = get_group_id_from_distinguished_name( + &value.to_ascii_lowercase(), + &self.base_dn, + &self.base_dn_str, + )?; + Ok(UserRequestFilter::MemberOf(group_name)) } - } else { - match map_field(field) { + "objectclass" => { + if value == "person" + || value == "inetOrgPerson" + || value == "posixAccount" + || value == "mailAccount" + { + Ok(UserRequestFilter::And(vec![])) + } else { + Ok(UserRequestFilter::Not(Box::new(UserRequestFilter::And( + vec![], + )))) + } + } + _ => match map_field(field) { Ok(field) => { if field == "user_id" { Ok(UserRequestFilter::UserId(UserId::new(value))) } else { - Ok(UserRequestFilter::Equality(field, value.clone())) + Ok(UserRequestFilter::Equality( + field.to_string(), + value.clone(), + )) } } Err(_) => { @@ -968,7 +981,7 @@ impl LdapHandler { @@ -990,8 +1003,12 @@ impl LdapHandler, get_groups: bool) -> Result>; async fn list_groups(&self, filters: Option) -> Result>; async fn get_user_details(&self, user_id: &UserId) -> Result; - async fn get_group_details(&self, group_id: GroupId) -> Result; - async fn get_user_groups(&self, user: &UserId) -> Result>; + async fn get_group_details(&self, group_id: GroupId) -> Result; + async fn get_user_groups(&self, user: &UserId) -> Result>; async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; @@ -1079,7 +1096,12 @@ mod tests { .with(eq(UserId::new("test"))) .return_once(|_| { let mut set = HashSet::new(); - set.insert(GroupIdAndName(GroupId(42), group)); + set.insert(GroupDetails { + group_id: GroupId(42), + display_name: group, + creation_date: chrono::Utc.timestamp(42, 42), + uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), + }); Ok(set) }); let mut ldap_handler = @@ -1155,7 +1177,12 @@ mod tests { .with(eq(UserId::new("test"))) .return_once(|_| { let mut set = HashSet::new(); - set.insert(GroupIdAndName(GroupId(42), "lldap_admin".to_string())); + set.insert(GroupDetails { + group_id: GroupId(42), + display_name: "lldap_admin".to_string(), + creation_date: chrono::Utc.timestamp(42, 42), + uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), + }); Ok(set) }); let mut ldap_handler = @@ -1237,7 +1264,12 @@ mod tests { user_id: UserId::new("bob"), ..Default::default() }, - groups: Some(vec![GroupIdAndName(GroupId(42), "rockstars".to_string())]), + groups: Some(vec![GroupDetails { + group_id: GroupId(42), + display_name: "rockstars".to_string(), + creation_date: chrono::Utc.timestamp(42, 42), + uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), + }]), }]) }); let mut ldap_handler = setup_bound_readonly_handler(mock).await; @@ -1386,6 +1418,7 @@ mod tests { display_name: "Bôb Böbberson".to_string(), first_name: "Bôb".to_string(), last_name: "Böbberson".to_string(), + uuid: uuid!("698e1d5f-7a40-3151-8745-b9b8a37839da"), ..Default::default() }, groups: None, @@ -1397,6 +1430,7 @@ mod tests { display_name: "Jimminy Cricket".to_string(), first_name: "Jim".to_string(), last_name: "Cricket".to_string(), + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), creation_date: Utc.ymd(2014, 7, 8).and_hms(9, 10, 11), }, groups: None, @@ -1415,6 +1449,7 @@ mod tests { "sn", "cn", "createTimestamp", + "entryUuid", ], ); assert_eq!( @@ -1459,7 +1494,11 @@ mod tests { LdapPartialAttribute { atype: "createTimestamp".to_string(), vals: vec!["1970-01-01T00:00:00+00:00".to_string()] - } + }, + LdapPartialAttribute { + atype: "entryUuid".to_string(), + vals: vec!["698e1d5f-7a40-3151-8745-b9b8a37839da".to_string()] + }, ], }), LdapOp::SearchResultEntry(LdapSearchResultEntry { @@ -1501,7 +1540,11 @@ mod tests { LdapPartialAttribute { atype: "createTimestamp".to_string(), vals: vec!["2014-07-08T09:10:11+00:00".to_string()] - } + }, + LdapPartialAttribute { + atype: "entryUuid".to_string(), + vals: vec!["04ac75e0-2900-3e21-926c-2f732c26b3fc".to_string()] + }, ], }), make_search_success(), @@ -1520,12 +1563,16 @@ mod tests { Group { id: GroupId(1), display_name: "group_1".to_string(), + creation_date: chrono::Utc.timestamp(42, 42), users: vec![UserId::new("bob"), UserId::new("john")], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), }, Group { id: GroupId(3), display_name: "BestGroup".to_string(), + creation_date: chrono::Utc.timestamp(42, 42), users: vec![UserId::new("john")], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), }, ]) }); @@ -1533,7 +1580,7 @@ mod tests { let request = make_search_request( "ou=groups,dc=example,dc=cOm", LdapFilter::And(vec![]), - vec!["objectClass", "dn", "cn", "uniqueMember"], + vec!["objectClass", "dn", "cn", "uniqueMember", "entryUuid"], ); assert_eq!( ldap_handler.do_search_or_dse(&request).await, @@ -1560,6 +1607,10 @@ mod tests { "uid=john,ou=people,dc=example,dc=com".to_string(), ] }, + LdapPartialAttribute { + atype: "entryUuid".to_string(), + vals: vec!["04ac75e0-2900-3e21-926c-2f732c26b3fc".to_string()], + }, ], }), LdapOp::SearchResultEntry(LdapSearchResultEntry { @@ -1581,6 +1632,10 @@ mod tests { atype: "uniqueMember".to_string(), vals: vec!["uid=john,ou=people,dc=example,dc=com".to_string()] }, + LdapPartialAttribute { + atype: "entryUuid".to_string(), + vals: vec!["04ac75e0-2900-3e21-926c-2f732c26b3fc".to_string()], + }, ], }), make_search_success(), @@ -1609,7 +1664,9 @@ mod tests { Ok(vec![Group { display_name: "group_1".to_string(), id: GroupId(1), + creation_date: chrono::Utc.timestamp(42, 42), users: vec![], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), }]) }); let mut ldap_handler = setup_bound_admin_handler(mock).await; @@ -1658,7 +1715,9 @@ mod tests { Ok(vec![Group { display_name: "group_1".to_string(), id: GroupId(1), + creation_date: chrono::Utc.timestamp(42, 42), users: vec![], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), }]) }); let mut ldap_handler = setup_bound_admin_handler(mock).await; @@ -1932,7 +1991,9 @@ mod tests { Ok(vec![Group { id: GroupId(1), display_name: "group_1".to_string(), + creation_date: chrono::Utc.timestamp(42, 42), users: vec![UserId::new("bob"), UserId::new("john")], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), }]) }); let mut ldap_handler = setup_bound_admin_handler(mock).await; @@ -1989,7 +2050,6 @@ mod tests { } #[tokio::test] async fn test_search_wildcards() { - use chrono::TimeZone; let mut mock = MockTestBackendHandler::new(); mock.expect_list_users().returning(|_, _| { @@ -2011,7 +2071,9 @@ mod tests { Ok(vec![Group { id: GroupId(1), display_name: "group_1".to_string(), + creation_date: chrono::Utc.timestamp(42, 42), users: vec![UserId::new("bob"), UserId::new("john")], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), }]) }); let mut ldap_handler = setup_bound_admin_handler(mock).await; diff --git a/server/src/infra/tcp_backend_handler.rs b/server/src/infra/tcp_backend_handler.rs index c6f8d4d..093ec2b 100644 --- a/server/src/infra/tcp_backend_handler.rs +++ b/server/src/infra/tcp_backend_handler.rs @@ -38,8 +38,8 @@ mockall::mock! { async fn list_users(&self, filters: Option, get_groups: bool) -> Result>; async fn list_groups(&self, filters: Option) -> Result>; async fn get_user_details(&self, user_id: &UserId) -> Result; - async fn get_group_details(&self, group_id: GroupId) -> Result; - async fn get_user_groups(&self, user: &UserId) -> Result>; + async fn get_group_details(&self, group_id: GroupId) -> Result; + async fn get_user_groups(&self, user: &UserId) -> Result>; async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>;