server: Add a Uuid attribute to every user and group

This commit is contained in:
Valentin Tolmer 2022-06-10 18:23:16 +02:00 committed by nitnelave
parent cbde363fde
commit c72c1fdf2c
9 changed files with 679 additions and 315 deletions

86
Cargo.lock generated
View File

@ -501,6 +501,15 @@ dependencies = [
"generic-array", "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]] [[package]]
name = "boolinator" name = "boolinator"
version = "2.4.0" version = "2.4.0"
@ -541,7 +550,7 @@ dependencies = [
"rand 0.7.3", "rand 0.7.3",
"serde", "serde",
"serde_json", "serde_json",
"uuid", "uuid 0.8.2",
] ]
[[package]] [[package]]
@ -837,6 +846,16 @@ dependencies = [
"subtle", "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]] [[package]]
name = "crypto-mac" name = "crypto-mac"
version = "0.10.1" version = "0.10.1"
@ -870,7 +889,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"digest", "digest 0.9.0",
"rand_core 0.5.1", "rand_core 0.5.1",
"serde", "serde",
"subtle", "subtle",
@ -992,6 +1011,16 @@ dependencies = [
"generic-array", "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]] [[package]]
name = "dirs" name = "dirs"
version = "4.0.0" version = "4.0.0"
@ -1539,7 +1568,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b"
dependencies = [ dependencies = [
"digest", "digest 0.9.0",
"hmac 0.11.0", "hmac 0.11.0",
] ]
@ -1550,7 +1579,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
dependencies = [ dependencies = [
"crypto-mac 0.10.1", "crypto-mac 0.10.1",
"digest", "digest 0.9.0",
] ]
[[package]] [[package]]
@ -1560,7 +1589,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
dependencies = [ dependencies = [
"crypto-mac 0.11.1", "crypto-mac 0.11.1",
"digest", "digest 0.9.0",
] ]
[[package]] [[package]]
@ -1764,7 +1793,7 @@ dependencies = [
"smartstring", "smartstring",
"static_assertions", "static_assertions",
"url", "url",
"uuid", "uuid 0.8.2",
] ]
[[package]] [[package]]
@ -1806,7 +1835,7 @@ checksum = "86e46349d67dc03bdbdb28da0337a355a53ca1d5156452722c36fe21d0e6389b"
dependencies = [ dependencies = [
"base64", "base64",
"crypto-mac 0.10.1", "crypto-mac 0.10.1",
"digest", "digest 0.9.0",
"hmac 0.10.1", "hmac 0.10.1",
"serde", "serde",
"serde_json", "serde_json",
@ -2002,6 +2031,7 @@ dependencies = [
"tracing-forest", "tracing-forest",
"tracing-log", "tracing-log",
"tracing-subscriber", "tracing-subscriber",
"uuid 1.1.1",
] ]
[[package]] [[package]]
@ -2035,7 +2065,7 @@ version = "0.3.0-alpha.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"curve25519-dalek", "curve25519-dalek",
"digest", "digest 0.9.0",
"generic-array", "generic-array",
"getrandom 0.2.3", "getrandom 0.2.3",
"opaque-ke", "opaque-ke",
@ -2115,11 +2145,20 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15"
dependencies = [ dependencies = [
"block-buffer", "block-buffer 0.9.0",
"digest", "digest 0.9.0",
"opaque-debug", "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]] [[package]]
name = "memchr" name = "memchr"
version = "2.4.1" version = "2.4.1"
@ -2404,7 +2443,7 @@ checksum = "26772682ba4fa69f11ae6e4af8bc83946372981ff31a026648d4acb2553c9ee8"
dependencies = [ dependencies = [
"base64", "base64",
"curve25519-dalek", "curve25519-dalek",
"digest", "digest 0.9.0",
"displaydoc", "displaydoc",
"generic-array", "generic-array",
"generic-bytes", "generic-bytes",
@ -2938,7 +2977,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c2603e2823634ab331437001b411b9ed11660fbc4066f3908c84a9439260d" checksum = "e05c2603e2823634ab331437001b411b9ed11660fbc4066f3908c84a9439260d"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"digest", "digest 0.9.0",
"lazy_static", "lazy_static",
"num-bigint-dig", "num-bigint-dig",
"num-integer", "num-integer",
@ -3175,10 +3214,10 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
dependencies = [ dependencies = [
"block-buffer", "block-buffer 0.9.0",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"cpufeatures", "cpufeatures",
"digest", "digest 0.9.0",
"opaque-debug", "opaque-debug",
] ]
@ -3194,10 +3233,10 @@ version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [ dependencies = [
"block-buffer", "block-buffer 0.9.0",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"cpufeatures", "cpufeatures",
"digest", "digest 0.9.0",
"opaque-debug", "opaque-debug",
] ]
@ -3337,7 +3376,7 @@ dependencies = [
"chrono", "chrono",
"crc", "crc",
"crossbeam-queue", "crossbeam-queue",
"digest", "digest 0.9.0",
"dirs", "dirs",
"either", "either",
"flume", "flume",
@ -3355,7 +3394,7 @@ dependencies = [
"libc", "libc",
"libsqlite3-sys", "libsqlite3-sys",
"log", "log",
"md-5", "md-5 0.9.1",
"memchr", "memchr",
"num-bigint", "num-bigint",
"once_cell", "once_cell",
@ -3759,7 +3798,7 @@ dependencies = [
"futures", "futures",
"tracing", "tracing",
"tracing-futures", "tracing-futures",
"uuid", "uuid 0.8.2",
] ]
[[package]] [[package]]
@ -3955,6 +3994,15 @@ dependencies = [
"getrandom 0.2.3", "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]] [[package]]
name = "validator" name = "validator"
version = "0.14.0" version = "0.14.0"

View File

@ -17,54 +17,53 @@ anyhow = "*"
async-trait = "0.1" async-trait = "0.1"
base64 = "0.13" base64 = "0.13"
bincode = "1.3" bincode = "1.3"
chrono = { version = "*", features = [ "serde" ]}
clap = { version = "3.1.15", features = [ "std", "color", "suggestions", "derive", "env" ] }
cron = "*" cron = "*"
derive_builder = "0.10.2" derive_builder = "0.10.2"
futures = "*" futures = "*"
futures-util = "*" futures-util = "*"
hmac = "0.10" hmac = "0.10"
http = "*" http = "*"
itertools = "0.10.1"
juniper = "0.15.6"
juniper_actix = "0.4.0"
jwt = "0.13" jwt = "0.13"
ldap3_server = ">=0.1.9" ldap3_server = ">=0.1.9"
lldap_auth = { path = "../auth" }
log = "*" log = "*"
orion = "0.16"
native-tls = "0.2.10" native-tls = "0.2.10"
orion = "0.16"
serde = "*" serde = "*"
serde_json = "1" serde_json = "1"
sha2 = "0.9" sha2 = "0.9"
sqlx-core = "0.5.11" sqlx-core = "0.5.11"
thiserror = "*" thiserror = "*"
time = "0.2" time = "0.2"
tokio = { version = "1.13.1", features = ["full"] }
tokio-native-tls = "0.3" tokio-native-tls = "0.3"
tokio-util = "0.6.3"
tokio-stream = "*" tokio-stream = "*"
tokio-util = "0.6.3"
tracing = "*"
tracing-actix-web = "0.4.0-beta.7" tracing-actix-web = "0.4.0-beta.7"
tracing-attributes = "^0.1.21" tracing-attributes = "^0.1.21"
tracing-log = "*" 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] [dependencies.chrono]
version = "0.6" features = ["serde"]
version = "*"
[dependencies.clap]
features = ["std", "color", "suggestions", "derive", "env"]
version = "3.1.15"
[dependencies.figment]
features = ["env", "toml"]
version = "*"
[dependencies.tracing-subscriber] [dependencies.tracing-subscriber]
version = "0.3" version = "0.3"
features = ["env-filter", "tracing-log"] features = ["env-filter", "tracing-log"]
[dependencies.lettre] [dependencies.lettre]
features = ["builder", "serde", "smtp-transport", "tokio1-native-tls", "tokio1"]
version = "0.10.0-rc.3" version = "0.10.0-rc.3"
features = [
"builder",
"serde",
"smtp-transport",
"tokio1-native-tls",
"tokio1",
]
[dependencies.sqlx] [dependencies.sqlx]
version = "0.5.11" version = "0.5.11"
@ -78,6 +77,9 @@ features = [
"sqlite", "sqlite",
] ]
[dependencies.lldap_auth]
path = "../auth"
[dependencies.sea-query] [dependencies.sea-query]
version = "^0.25" version = "^0.25"
features = ["with-chrono", "sqlx-sqlite"] features = ["with-chrono", "sqlx-sqlite"]
@ -86,24 +88,32 @@ features = ["with-chrono", "sqlx-sqlite"]
version = "*" version = "*"
features = ["with-chrono", "sqlx-sqlite", "sqlx-any"] features = ["with-chrono", "sqlx-sqlite", "sqlx-any"]
[dependencies.figment] [dependencies.opaque-ke]
features = ["env", "toml"] version = "0.6"
[dependencies.openssl-sys]
features = ["vendored"]
version = "*" version = "*"
[dependencies.rand]
features = ["small_rng", "getrandom"]
version = "0.8"
[dependencies.secstr] [dependencies.secstr]
features = ["serde"] features = ["serde"]
version = "*" version = "*"
[dependencies.openssl-sys] [dependencies.tokio]
features = ["vendored"] features = ["full"]
version = "1.13.1"
[dependencies.uuid]
features = ["v3"]
version = "*" version = "*"
[dependencies.tracing-forest] [dependencies.tracing-forest]
features = ["smallvec", "chrono", "tokio"] features = ["smallvec", "chrono", "tokio"]
version = "^0.1.4" version = "^0.1.4"
[dependencies.tracing]
version = "*"
[dev-dependencies] [dev-dependencies]
mockall = "0.9.1" mockall = "0.9.1"

View File

@ -3,9 +3,57 @@ use async_trait::async_trait;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet; use std::collections::HashSet;
#[derive(PartialEq, Eq, Clone, Debug, Default, Serialize, Deserialize)] #[derive(
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] 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<chrono::Utc>) -> 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<Self> {
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")] #[serde(from = "String")]
#[sqlx(transparent)]
pub struct UserId(String); pub struct UserId(String);
impl UserId { impl UserId {
@ -34,8 +82,7 @@ impl From<String> for UserId {
} }
} }
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
pub struct User { pub struct User {
pub user_id: UserId, pub user_id: UserId,
pub email: String, pub email: String,
@ -44,18 +91,22 @@ pub struct User {
pub last_name: String, pub last_name: String,
// pub avatar: ?, // pub avatar: ?,
pub creation_date: chrono::DateTime<chrono::Utc>, pub creation_date: chrono::DateTime<chrono::Utc>,
pub uuid: Uuid,
} }
#[cfg(test)]
impl Default for User { impl Default for User {
fn default() -> Self { fn default() -> Self {
use chrono::TimeZone; use chrono::TimeZone;
let epoch = chrono::Utc.timestamp(0, 0);
User { User {
user_id: UserId::default(), user_id: UserId::default(),
email: String::new(), email: String::new(),
display_name: String::new(), display_name: String::new(),
first_name: String::new(), first_name: String::new(),
last_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 struct Group {
pub id: GroupId, pub id: GroupId,
pub display_name: String, pub display_name: String,
pub creation_date: chrono::DateTime<chrono::Utc>,
pub uuid: Uuid,
pub users: Vec<UserId>, pub users: Vec<UserId>,
} }
@ -92,6 +145,7 @@ pub enum GroupRequestFilter {
Or(Vec<GroupRequestFilter>), Or(Vec<GroupRequestFilter>),
Not(Box<GroupRequestFilter>), Not(Box<GroupRequestFilter>),
DisplayName(String), DisplayName(String),
Uuid(Uuid),
GroupId(GroupId), GroupId(GroupId),
// Check if the group contains a user identified by uid. // Check if the group contains a user identified by uid.
Member(UserId), Member(UserId),
@ -128,16 +182,22 @@ pub trait LoginHandler: Clone + Send {
async fn bind(&self, request: BindRequest) -> Result<()>; 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); pub struct GroupId(pub i32);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::FromRow)] #[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<chrono::Utc>,
pub uuid: Uuid,
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct UserAndGroups { pub struct UserAndGroups {
pub user: User, pub user: User,
pub groups: Option<Vec<GroupIdAndName>>, pub groups: Option<Vec<GroupDetails>>,
} }
#[async_trait] #[async_trait]
@ -149,7 +209,7 @@ pub trait BackendHandler: Clone + Send {
) -> Result<Vec<UserAndGroups>>; ) -> Result<Vec<UserAndGroups>>;
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>; async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>;
async fn get_user_details(&self, user_id: &UserId) -> Result<User>; async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupIdAndName>; async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>;
async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
async fn update_group(&self, request: UpdateGroupRequest) -> 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 delete_group(&self, group_id: GroupId) -> Result<()>;
async fn add_user_to_group(&self, user_id: &UserId, 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 remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>;
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupIdAndName>>; async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>>;
} }
#[cfg(test)] #[cfg(test)]
@ -172,14 +232,14 @@ mockall::mock! {
async fn list_users(&self, filters: Option<UserRequestFilter>, get_groups: bool) -> Result<Vec<UserAndGroups>>; async fn list_users(&self, filters: Option<UserRequestFilter>, get_groups: bool) -> Result<Vec<UserAndGroups>>;
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>; async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>;
async fn get_user_details(&self, user_id: &UserId) -> Result<User>; async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupIdAndName>; async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>;
async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>;
async fn delete_user(&self, user_id: &UserId) -> Result<()>; async fn delete_user(&self, user_id: &UserId) -> Result<()>;
async fn create_group(&self, group_name: &str) -> Result<GroupId>; async fn create_group(&self, group_name: &str) -> Result<GroupId>;
async fn delete_group(&self, group_id: GroupId) -> Result<()>; async fn delete_group(&self, group_id: GroupId) -> Result<()>;
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupIdAndName>>; async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>>;
async fn add_user_to_group(&self, user_id: &UserId, 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 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<()>; 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)
);
}
}

View File

@ -98,6 +98,7 @@ fn get_group_filter_expr(filter: GroupRequestFilter) -> SimpleExpr {
Not(f) => Expr::not(Expr::expr(get_group_filter_expr(*f))), Not(f) => Expr::not(Expr::expr(get_group_filter_expr(*f))),
DisplayName(name) => Expr::col((Groups::Table, Groups::DisplayName)).eq(name), DisplayName(name) => Expr::col((Groups::Table, Groups::DisplayName)).eq(name),
GroupId(id) => Expr::col((Groups::Table, Groups::GroupId)).eq(id.0), 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)) // WHERE (group_id in (SELECT group_id FROM memberships WHERE user_id = user))
Member(user) => Expr::col((Memberships::Table, Memberships::GroupId)).in_subquery( Member(user) => Expr::col((Memberships::Table, Memberships::GroupId)).in_subquery(
Query::select() Query::select()
@ -126,7 +127,8 @@ impl BackendHandler for SqlBackendHandler {
.column(Users::FirstName) .column(Users::FirstName)
.column(Users::LastName) .column(Users::LastName)
.column(Users::Avatar) .column(Users::Avatar)
.column(Users::CreationDate) .column((Users::Table, Users::CreationDate))
.column((Users::Table, Users::Uuid))
.from(Users::Table) .from(Users::Table)
.order_by((Users::Table, Users::UserId), Order::Asc) .order_by((Users::Table, Users::UserId), Order::Asc)
.to_owned(); .to_owned();
@ -151,6 +153,14 @@ impl BackendHandler for SqlBackendHandler {
Expr::col((Groups::Table, Groups::DisplayName)), Expr::col((Groups::Table, Groups::DisplayName)),
Alias::new("group_display_name"), 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); .order_by(Alias::new("group_display_name"), Order::Asc);
} }
if let Some(filter) = filters { if let Some(filter) = filters {
@ -189,13 +199,14 @@ impl BackendHandler for SqlBackendHandler {
user: User::from_row(rows.peek().unwrap()).unwrap(), user: User::from_row(rows.peek().unwrap()).unwrap(),
groups: if get_groups { groups: if get_groups {
Some( Some(
rows.map(|row| { rows.map(|row| GroupDetails {
GroupIdAndName( group_id: row.get::<GroupId, _>(&*Groups::GroupId.to_string()),
GroupId(row.get::<i32, _>(&*Groups::GroupId.to_string())), display_name: row.get::<String, _>("group_display_name"),
row.get::<String, _>("group_display_name"), creation_date: row
) .get::<chrono::DateTime<chrono::Utc>, _>("group_creation_date"),
uuid: row.get::<Uuid, _>("group_uuid"),
}) })
.filter(|g| !g.1.is_empty()) .filter(|g| !g.display_name.is_empty())
.collect(), .collect(),
) )
} else { } else {
@ -213,6 +224,8 @@ impl BackendHandler for SqlBackendHandler {
let mut query_builder = Query::select() let mut query_builder = Query::select()
.column((Groups::Table, Groups::GroupId)) .column((Groups::Table, Groups::GroupId))
.column(Groups::DisplayName) .column(Groups::DisplayName)
.column(Groups::CreationDate)
.column(Groups::Uuid)
.column(Memberships::UserId) .column(Memberships::UserId)
.from(Groups::Table) .from(Groups::Table)
.left_join( .left_join(
@ -245,20 +258,17 @@ impl BackendHandler for SqlBackendHandler {
let mut groups = Vec::new(); let mut groups = Vec::new();
// The rows are returned sorted by display_name, equivalent to group_id. We group them by // 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. // 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) .fetch_all(&self.sql_pool)
.await? .await?
.into_iter() .into_iter()
.group_by(|row| { .group_by(|row| GroupDetails::from_row(row).unwrap())
(
GroupId(row.get::<i32, _>(&*Groups::GroupId.to_string())),
row.get::<String, _>(&*Groups::DisplayName.to_string()),
)
})
{ {
groups.push(Group { groups.push(Group {
id: group_id, id: group_details.group_id,
display_name, display_name: group_details.display_name,
creation_date: group_details.creation_date,
uuid: group_details.uuid,
users: rows users: rows
.map(|row| row.get::<UserId, _>(&*Memberships::UserId.to_string())) .map(|row| row.get::<UserId, _>(&*Memberships::UserId.to_string()))
// If a group has no users, an empty string is returned because of the left // 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::LastName)
.column(Users::Avatar) .column(Users::Avatar)
.column(Users::CreationDate) .column(Users::CreationDate)
.column(Users::Uuid)
.from(Users::Table) .from(Users::Table)
.cond_where(Expr::col(Users::UserId).eq(user_id)) .cond_where(Expr::col(Users::UserId).eq(user_id))
.build_sqlx(DbQueryBuilder {}); .build_sqlx(DbQueryBuilder {});
@ -292,29 +303,31 @@ impl BackendHandler for SqlBackendHandler {
} }
#[instrument(skip_all, level = "debug", ret, err)] #[instrument(skip_all, level = "debug", ret, err)]
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupIdAndName> { async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails> {
debug!(?group_id); debug!(?group_id);
let (query, values) = Query::select() let (query, values) = Query::select()
.column(Groups::GroupId) .column(Groups::GroupId)
.column(Groups::DisplayName) .column(Groups::DisplayName)
.column(Groups::CreationDate)
.column(Groups::Uuid)
.from(Groups::Table) .from(Groups::Table)
.cond_where(Expr::col(Groups::GroupId).eq(group_id)) .cond_where(Expr::col(Groups::GroupId).eq(group_id))
.build_sqlx(DbQueryBuilder {}); .build_sqlx(DbQueryBuilder {});
debug!(%query); debug!(%query);
Ok( Ok(query_as_with::<_, GroupDetails, _>(&query, values)
query_as_with::<_, GroupIdAndName, _>(query.as_str(), values) .fetch_one(&self.sql_pool)
.fetch_one(&self.sql_pool) .await?)
.await?,
)
} }
#[instrument(skip_all, level = "debug", ret, err)] #[instrument(skip_all, level = "debug", ret, err)]
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupIdAndName>> { async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>> {
debug!(?user_id); debug!(?user_id);
let (query, values) = Query::select() let (query, values) = Query::select()
.column((Groups::Table, Groups::GroupId)) .column((Groups::Table, Groups::GroupId))
.column(Groups::DisplayName) .column(Groups::DisplayName)
.column(Groups::CreationDate)
.column(Groups::Uuid)
.from(Groups::Table) .from(Groups::Table)
.inner_join( .inner_join(
Memberships::Table, Memberships::Table,
@ -325,17 +338,10 @@ impl BackendHandler for SqlBackendHandler {
.build_sqlx(DbQueryBuilder {}); .build_sqlx(DbQueryBuilder {});
debug!(%query); debug!(%query);
query_with(query.as_str(), values) query_as_with::<_, GroupDetails, _>(&query, values)
// Extract the group id from the row.
.map(|row: DbRow| {
GroupIdAndName(
row.get::<GroupId, _>(&*Groups::GroupId.to_string()),
row.get::<String, _>(&*Groups::DisplayName.to_string()),
)
})
.fetch(&self.sql_pool) .fetch(&self.sql_pool)
// Collect the vector of rows, each potentially an error. // Collect the vector of rows, each potentially an error.
.collect::<Vec<sqlx::Result<GroupIdAndName>>>() .collect::<Vec<sqlx::Result<GroupDetails>>>()
.await .await
.into_iter() .into_iter()
// Transform it into a single result (the first error if any), and group the group_ids // 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::FirstName,
Users::LastName, Users::LastName,
Users::CreationDate, 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() let (query, values) = Query::insert()
.into_table(Users::Table) .into_table(Users::Table)
.columns(columns) .columns(columns)
.values_panic(vec![ .values_panic(values)
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(),
])
.build_sqlx(DbQueryBuilder {}); .build_sqlx(DbQueryBuilder {});
debug!(%query); debug!(%query);
query_with(query.as_str(), values) query_with(query.as_str(), values)
@ -445,10 +456,19 @@ impl BackendHandler for SqlBackendHandler {
#[instrument(skip_all, level = "debug", ret, err)] #[instrument(skip_all, level = "debug", ret, err)]
async fn create_group(&self, group_name: &str) -> Result<GroupId> { async fn create_group(&self, group_name: &str) -> Result<GroupId> {
debug!(?group_name); debug!(?group_name);
let now = chrono::Utc::now();
let (query, values) = Query::insert() let (query, values) = Query::insert()
.into_table(Groups::Table) .into_table(Groups::Table)
.columns(vec![Groups::DisplayName]) .columns(vec![
.values_panic(vec![group_name.into()]) 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 {}); .build_sqlx(DbQueryBuilder {});
debug!(%query); debug!(%query);
query_with(query.as_str(), values) query_with(query.as_str(), values)
@ -707,7 +727,7 @@ mod tests {
u.groups u.groups
.unwrap() .unwrap()
.into_iter() .into_iter()
.map(|g| g.0) .map(|g| g.group_id)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
) )
}) })
@ -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::<Vec<_>>(),
)
})
.collect::<Vec<_>>();
for (user_date, groups) in users {
for group_date in groups {
assert_ne!(user_date, group_date);
}
}
}
} }
#[tokio::test] #[tokio::test]
@ -738,62 +781,33 @@ mod tests {
insert_membership(&handler, group_1, "patrick").await; insert_membership(&handler, group_1, "patrick").await;
insert_membership(&handler, group_2, "patrick").await; insert_membership(&handler, group_2, "patrick").await;
insert_membership(&handler, group_2, "John").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::<Vec<_>>()
};
assert_eq!(get_group_ids(None).await, vec![group_1, group_3, group_2]);
assert_eq!( assert_eq!(
handler.list_groups(None).await.unwrap(), get_group_ids(Some(GroupRequestFilter::Or(vec![
vec![ GroupRequestFilter::DisplayName("Empty Group".to_string()),
Group { GroupRequestFilter::Member(UserId::new("bob")),
id: group_1, ])))
display_name: "Best Group".to_string(), .await,
users: vec![UserId::new("bob"), UserId::new("patrick")] vec![group_1, group_3]
},
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")]
},
]
); );
assert_eq!( assert_eq!(
handler get_group_ids(Some(GroupRequestFilter::And(vec![
.list_groups(Some(GroupRequestFilter::Or(vec![ GroupRequestFilter::Not(Box::new(GroupRequestFilter::DisplayName(
GroupRequestFilter::DisplayName("Empty Group".to_string()), "value".to_string()
GroupRequestFilter::Member(UserId::new("bob")), ))),
]))) GroupRequestFilter::GroupId(group_1),
.await ])))
.unwrap(), .await,
vec![ vec![group_1]
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")]
}]
); );
} }
@ -846,26 +860,20 @@ mod tests {
insert_membership(&handler, group_1, "bob").await; insert_membership(&handler, group_1, "bob").await;
insert_membership(&handler, group_1, "patrick").await; insert_membership(&handler, group_1, "patrick").await;
insert_membership(&handler, group_2, "patrick").await; insert_membership(&handler, group_2, "patrick").await;
let mut bob_groups = HashSet::new(); let get_group_ids = |user: &'static str| async {
bob_groups.insert(GroupIdAndName(group_1, "Group1".to_string())); let mut groups = handler
let mut patrick_groups = HashSet::new(); .get_user_groups(&UserId::new(user))
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"))
.await .await
.unwrap(), .unwrap()
patrick_groups .into_iter()
); .map(|g| g.group_id)
assert_eq!( .collect::<Vec<_>>();
handler.get_user_groups(&UserId::new("John")).await.unwrap(), groups.sort_by(|g1, g2| g1.0.cmp(&g2.0));
HashSet::new() 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] #[tokio::test]

View File

@ -1,5 +1,7 @@
use super::handler::{GroupId, UserId}; use super::handler::{GroupId, UserId, Uuid};
use sea_query::*; use sea_query::*;
use sqlx::Row;
use tracing::warn;
pub type Pool = sqlx::sqlite::SqlitePool; pub type Pool = sqlx::sqlite::SqlitePool;
pub type PoolOptions = sqlx::sqlite::SqlitePoolOptions; pub type PoolOptions = sqlx::sqlite::SqlitePoolOptions;
@ -12,56 +14,6 @@ impl From<GroupId> for Value {
} }
} }
impl<DB> sqlx::Type<DB> for GroupId
where
DB: sqlx::Database,
i32: sqlx::Type<DB>,
{
fn type_info() -> <DB as sqlx::Database>::TypeInfo {
<i32 as sqlx::Type<DB>>::type_info()
}
fn compatible(ty: &<DB as sqlx::Database>::TypeInfo) -> bool {
<i32 as sqlx::Type<DB>>::compatible(ty)
}
}
impl<'r, DB> sqlx::Decode<'r, DB> for GroupId
where
DB: sqlx::Database,
i32: sqlx::Decode<'r, DB>,
{
fn decode(
value: <DB as sqlx::database::HasValueRef<'r>>::ValueRef,
) -> Result<Self, Box<dyn std::error::Error + Sync + Send + 'static>> {
<i32 as sqlx::Decode<'r, DB>>::decode(value).map(GroupId)
}
}
impl<DB> sqlx::Type<DB> for UserId
where
DB: sqlx::Database,
String: sqlx::Type<DB>,
{
fn type_info() -> <DB as sqlx::Database>::TypeInfo {
<String as sqlx::Type<DB>>::type_info()
}
fn compatible(ty: &<DB as sqlx::Database>::TypeInfo) -> bool {
<String as sqlx::Type<DB>>::compatible(ty)
}
}
impl<'r, DB> sqlx::Decode<'r, DB> for UserId
where
DB: sqlx::Database,
String: sqlx::Decode<'r, DB>,
{
fn decode(
value: <DB as sqlx::database::HasValueRef<'r>>::ValueRef,
) -> Result<Self, Box<dyn std::error::Error + Sync + Send + 'static>> {
<String as sqlx::Decode<'r, DB>>::decode(value).map(|s| UserId::new(&s))
}
}
impl From<UserId> for sea_query::Value { impl From<UserId> for sea_query::Value {
fn from(user_id: UserId) -> Self { fn from(user_id: UserId) -> Self {
user_id.into_string().into() user_id.into_string().into()
@ -74,6 +26,18 @@ impl From<&UserId> for sea_query::Value {
} }
} }
impl From<Uuid> 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)] #[derive(Iden)]
pub enum Users { pub enum Users {
Table, Table,
@ -87,6 +51,7 @@ pub enum Users {
PasswordHash, PasswordHash,
TotpSecret, TotpSecret,
MfaType, MfaType,
Uuid,
} }
#[derive(Iden)] #[derive(Iden)]
@ -94,6 +59,8 @@ pub enum Groups {
Table, Table,
GroupId, GroupId,
DisplayName, DisplayName,
CreationDate,
Uuid,
} }
#[derive(Iden)] #[derive(Iden)]
@ -103,6 +70,19 @@ pub enum Memberships {
GroupId, GroupId,
} }
async fn column_exists(pool: &Pool, table_name: &str, column_name: &str) -> sqlx::Result<bool> {
// 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::<i32, _>("col_count")
> 0)
}
pub async fn init_table(pool: &Pool) -> sqlx::Result<()> { 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 // SQLite needs this pragma to be turned on. Other DB might not understand this, so ignore the
// error. // error.
@ -130,6 +110,7 @@ pub async fn init_table(pool: &Pool) -> sqlx::Result<()> {
.col(ColumnDef::new(Users::PasswordHash).binary()) .col(ColumnDef::new(Users::PasswordHash).binary())
.col(ColumnDef::new(Users::TotpSecret).string_len(64)) .col(ColumnDef::new(Users::TotpSecret).string_len(64))
.col(ColumnDef::new(Users::MfaType).string_len(64)) .col(ColumnDef::new(Users::MfaType).string_len(64))
.col(ColumnDef::new(Users::Uuid).string_len(36).not_null())
.to_string(DbQueryBuilder {}), .to_string(DbQueryBuilder {}),
) )
.execute(pool) .execute(pool)
@ -151,11 +132,141 @@ pub async fn init_table(pool: &Pool) -> sqlx::Result<()> {
.unique_key() .unique_key()
.not_null(), .not_null(),
) )
.col(ColumnDef::new(Users::CreationDate).date_time().not_null())
.col(ColumnDef::new(Users::Uuid).string_len(36).not_null())
.to_string(DbQueryBuilder {}), .to_string(DbQueryBuilder {}),
) )
.execute(pool) .execute(pool)
.await?; .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::<String, _>(&*Groups::DisplayName.to_string()),
&row.get::<chrono::DateTime<chrono::Utc>, _>(
&*Groups::CreationDate.to_string(),
),
)
.into(),
)
.and_where(
Expr::col(Groups::GroupId)
.eq(row.get::<GroupId, _>(&*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::<UserId, _>(&*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::<chrono::DateTime<chrono::Utc>, _>(
&*Users::CreationDate.to_string(),
),
)
.into(),
)
.and_where(Expr::col(Users::UserId).eq(user_id))
.to_string(DbQueryBuilder {}),
)
.execute(pool)
.await?;
}
}
sqlx::query( sqlx::query(
&Table::create() &Table::create()
.table(Memberships::Table) .table(Memberships::Table)
@ -196,13 +307,13 @@ mod tests {
use chrono::prelude::*; use chrono::prelude::*;
use sqlx::{Column, Row}; use sqlx::{Column, Row};
#[actix_rt::test] #[tokio::test]
async fn test_init_table() { async fn test_init_table() {
let sql_pool = PoolOptions::new().connect("sqlite::memory:").await.unwrap(); let sql_pool = PoolOptions::new().connect("sqlite::memory:").await.unwrap();
init_table(&sql_pool).await.unwrap(); init_table(&sql_pool).await.unwrap();
sqlx::query(r#"INSERT INTO users sqlx::query(r#"INSERT INTO users
(user_id, email, display_name, first_name, last_name, creation_date, password_hash) (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")"#).execute(&sql_pool).await.unwrap(); 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 = let row =
sqlx::query(r#"SELECT display_name, creation_date FROM users WHERE user_id = "bôb""#) sqlx::query(r#"SELECT display_name, creation_date FROM users WHERE user_id = "bôb""#)
.fetch_one(&sql_pool) .fetch_one(&sql_pool)
@ -216,10 +327,49 @@ mod tests {
); );
} }
#[actix_rt::test] #[tokio::test]
async fn test_already_init_table() { async fn test_already_init_table() {
let sql_pool = PoolOptions::new().connect("sqlite::memory:").await.unwrap(); let sql_pool = PoolOptions::new().connect("sqlite::memory:").await.unwrap();
init_table(&sql_pool).await.unwrap(); init_table(&sql_pool).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, _>("uuid"))
.collect::<Vec<_>>(),
vec![crate::uuid!("a02eaf13-48a7-30f6-a3d4-040ff7c52b04")]
);
}
} }

View File

@ -25,7 +25,7 @@ use lldap_auth::{login, password_reset, registration, JWTClaims};
use crate::{ use crate::{
domain::{ domain::{
error::DomainError, error::DomainError,
handler::{BackendHandler, BindRequest, GroupIdAndName, LoginHandler, UserId}, handler::{BackendHandler, BindRequest, GroupDetails, LoginHandler, UserId},
opaque_handler::OpaqueHandler, opaque_handler::OpaqueHandler,
}, },
infra::{ infra::{
@ -37,12 +37,12 @@ use crate::{
type Token<S> = jwt::Token<jwt::Header, JWTClaims, S>; type Token<S> = jwt::Token<jwt::Header, JWTClaims, S>;
type SignedToken = Token<jwt::token::Signed>; type SignedToken = Token<jwt::token::Signed>;
fn create_jwt(key: &Hmac<Sha512>, user: String, groups: HashSet<GroupIdAndName>) -> SignedToken { fn create_jwt(key: &Hmac<Sha512>, user: String, groups: HashSet<GroupDetails>) -> SignedToken {
let claims = JWTClaims { let claims = JWTClaims {
exp: Utc::now() + chrono::Duration::days(1), exp: Utc::now() + chrono::Duration::days(1),
iat: Utc::now(), iat: Utc::now(),
user, user,
groups: groups.into_iter().map(|g| g.1).collect(), groups: groups.into_iter().map(|g| g.display_name).collect(),
}; };
let header = jwt::Header { let header = jwt::Header {
algorithm: jwt::AlgorithmType::Hs512, algorithm: jwt::AlgorithmType::Hs512,

View File

@ -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 juniper::{graphql_object, FieldResult, GraphQLInputObject};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{debug, debug_span, Instrument}; use tracing::{debug, debug_span, Instrument};
@ -184,6 +184,7 @@ pub struct User<Handler: BackendHandler> {
_phantom: std::marker::PhantomData<Box<Handler>>, _phantom: std::marker::PhantomData<Box<Handler>>,
} }
#[cfg(test)]
impl<Handler: BackendHandler> Default for User<Handler> { impl<Handler: BackendHandler> Default for User<Handler> {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -257,6 +258,7 @@ impl<Handler: BackendHandler> From<DomainUserAndGroups> for User<Handler> {
pub struct Group<Handler: BackendHandler> { pub struct Group<Handler: BackendHandler> {
group_id: i32, group_id: i32,
display_name: String, display_name: String,
creation_date: chrono::DateTime<chrono::Utc>,
members: Option<Vec<String>>, members: Option<Vec<String>>,
_phantom: std::marker::PhantomData<Box<Handler>>, _phantom: std::marker::PhantomData<Box<Handler>>,
} }
@ -291,11 +293,12 @@ impl<Handler: BackendHandler + Sync> Group<Handler> {
} }
} }
impl<Handler: BackendHandler> From<GroupIdAndName> for Group<Handler> { impl<Handler: BackendHandler> From<GroupDetails> for Group<Handler> {
fn from(group_id_and_name: GroupIdAndName) -> Self { fn from(group_details: GroupDetails) -> Self {
Self { Self {
group_id: group_id_and_name.0 .0, group_id: group_details.group_id.0,
display_name: group_id_and_name.1, display_name: group_details.display_name,
creation_date: group_details.creation_date,
members: None, members: None,
_phantom: std::marker::PhantomData, _phantom: std::marker::PhantomData,
} }
@ -307,6 +310,7 @@ impl<Handler: BackendHandler> From<DomainGroup> for Group<Handler> {
Self { Self {
group_id: group.id.0, group_id: group.id.0,
display_name: group.display_name, display_name: group.display_name,
creation_date: group.creation_date,
members: Some(group.users.into_iter().map(UserId::into_string).collect()), members: Some(group.users.into_iter().map(UserId::into_string).collect()),
_phantom: std::marker::PhantomData, _phantom: std::marker::PhantomData,
} }
@ -320,6 +324,7 @@ mod tests {
domain::handler::{MockTestBackendHandler, UserRequestFilter}, domain::handler::{MockTestBackendHandler, UserRequestFilter},
infra::auth_service::ValidationResults, infra::auth_service::ValidationResults,
}; };
use chrono::TimeZone;
use juniper::{ use juniper::{
execute, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, execute, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType,
RootNode, Variables, RootNode, Variables,
@ -361,7 +366,12 @@ mod tests {
}) })
}); });
let mut groups = HashSet::new(); 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() mock.expect_get_user_groups()
.with(eq(UserId::new("bob"))) .with(eq(UserId::new("bob")))
.return_once(|_| Ok(groups)); .return_once(|_| Ok(groups));

View File

@ -1,8 +1,8 @@
use crate::{ use crate::{
domain::{ domain::{
handler::{ handler::{
BackendHandler, BindRequest, Group, GroupIdAndName, GroupRequestFilter, LoginHandler, BackendHandler, BindRequest, Group, GroupDetails, GroupRequestFilter, LoginHandler,
User, UserId, UserRequestFilter, User, UserId, UserRequestFilter, Uuid,
}, },
opaque_handler::OpaqueHandler, opaque_handler::OpaqueHandler,
}, },
@ -153,7 +153,7 @@ fn get_user_attribute(
attribute: &str, attribute: &str,
dn: &str, dn: &str,
base_dn_str: &str, base_dn_str: &str,
groups: Option<&[GroupIdAndName]>, groups: Option<&[GroupDetails]>,
ignored_user_attributes: &[String], ignored_user_attributes: &[String],
) -> Result<Option<Vec<String>>> { ) -> Result<Option<Vec<String>>> {
let attribute = attribute.to_ascii_lowercase(); let attribute = attribute.to_ascii_lowercase();
@ -166,13 +166,19 @@ fn get_user_attribute(
], ],
"dn" | "distinguishedname" => vec![dn.to_string()], "dn" | "distinguishedname" => vec![dn.to_string()],
"uid" => vec![user.user_id.to_string()], "uid" => vec![user.user_id.to_string()],
"entryuuid" => vec![user.uuid.to_string()],
"mail" => vec![user.email.clone()], "mail" => vec![user.email.clone()],
"givenname" => vec![user.first_name.clone()], "givenname" => vec![user.first_name.clone()],
"sn" => vec![user.last_name.clone()], "sn" => vec![user.last_name.clone()],
"memberof" => groups "memberof" => groups
.into_iter() .into_iter()
.flatten() .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(), .collect(),
"cn" | "displayname" => vec![user.display_name.clone()], "cn" | "displayname" => vec![user.display_name.clone()],
"createtimestamp" | "modifytimestamp" => vec![user.creation_date.to_rfc3339()], "createtimestamp" | "modifytimestamp" => vec![user.creation_date.to_rfc3339()],
@ -233,7 +239,7 @@ fn make_ldap_search_user_result_entry(
user: User, user: User,
base_dn_str: &str, base_dn_str: &str,
attributes: &[String], attributes: &[String],
groups: Option<&[GroupIdAndName]>, groups: Option<&[GroupDetails]>,
ignored_user_attributes: &[String], ignored_user_attributes: &[String],
) -> Result<LdapSearchResultEntry> { ) -> Result<LdapSearchResultEntry> {
let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str); 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 group.display_name, base_dn_str
)], )],
"cn" | "uid" => vec![group.display_name.clone()], "cn" | "uid" => vec![group.display_name.clone()],
"entryuuid" => vec![group.uuid.to_string()],
"member" | "uniquemember" => group "member" | "uniquemember" => group
.users .users
.iter() .iter()
@ -363,24 +370,18 @@ fn is_subtree(subtree: &[(String, String)], base_tree: &[(String, String)]) -> b
true true
} }
fn map_field(field: &str) -> Result<String> { fn map_field(field: &str) -> Result<&'static str> {
assert!(field == field.to_ascii_lowercase()); assert!(field == field.to_ascii_lowercase());
Ok(if field == "uid" { Ok(match field {
"user_id".to_string() "uid" => "user_id",
} else if field == "mail" { "mail" => "email",
"email".to_string() "cn" | "displayname" => "display_name",
} else if field == "cn" || field == "displayname" { "givenname" => "first_name",
"display_name".to_string() "sn" => "last_name",
} else if field == "givenname" { "avatar" => "avatar",
"first_name".to_string() "creationdate" | "createtimestamp" | "modifytimestamp" => "creation_date",
} else if field == "sn" { "entryuuid" => "uuid",
"last_name".to_string() _ => bail!("Unknown field: {}", field),
} else if field == "avatar" {
"avatar".to_string()
} else if field == "creationdate" || field == "createtimestamp" || field == "modifytimestamp" {
"creation_date".to_string()
} else {
bail!("Unknown field: {}", field);
}) })
} }
@ -499,7 +500,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
let is_in_group = |name| { let is_in_group = |name| {
user_groups user_groups
.as_ref() .as_ref()
.map(|groups| groups.iter().any(|g| g.1 == name)) .map(|groups| groups.iter().any(|g| g.display_name == name))
.unwrap_or(false) .unwrap_or(false)
}; };
self.user_info = Some(( self.user_info = Some((
@ -845,29 +846,36 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
LdapFilter::Equality(field, value) => { LdapFilter::Equality(field, value) => {
let field = &field.to_ascii_lowercase(); let field = &field.to_ascii_lowercase();
let value = &value.to_ascii_lowercase(); let value = &value.to_ascii_lowercase();
if field == "member" || field == "uniquemember" { match field.as_str() {
let user_name = get_user_id_from_distinguished_name( "member" | "uniquemember" => {
value, let user_name = get_user_id_from_distinguished_name(
&self.base_dn, value,
&self.base_dn_str, &self.base_dn,
)?; &self.base_dn_str,
Ok(GroupRequestFilter::Member(user_name)) )?;
} else if field == "objectclass" { Ok(GroupRequestFilter::Member(user_name))
if value == "groupofuniquenames" || value == "groupofnames" {
Ok(GroupRequestFilter::And(vec![]))
} else {
Ok(GroupRequestFilter::Not(Box::new(GroupRequestFilter::And(
vec![],
))))
} }
} else { "objectclass" => {
let mapped_field = map_field(field); if value == "groupofuniquenames" || value == "groupofnames" {
if mapped_field.is_ok() Ok(GroupRequestFilter::And(vec![]))
&& (mapped_field.as_ref().unwrap() == "display_name" } else {
|| mapped_field.as_ref().unwrap() == "user_id") Ok(GroupRequestFilter::Not(Box::new(GroupRequestFilter::And(
{ vec![],
Ok(GroupRequestFilter::DisplayName(value.clone())) ))))
} else { }
}
_ => {
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) { if !self.ignored_group_attributes.contains(field) {
warn!( warn!(
r#"Ignoring unknown group attribute "{:?}" in filter.\n\ r#"Ignoring unknown group attribute "{:?}" in filter.\n\
@ -928,32 +936,37 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
))), ))),
LdapFilter::Equality(field, value) => { LdapFilter::Equality(field, value) => {
let field = &field.to_ascii_lowercase(); let field = &field.to_ascii_lowercase();
if field == "memberof" { match field.as_str() {
let group_name = get_group_id_from_distinguished_name( "memberof" => {
&value.to_ascii_lowercase(), let group_name = get_group_id_from_distinguished_name(
&self.base_dn, &value.to_ascii_lowercase(),
&self.base_dn_str, &self.base_dn,
)?; &self.base_dn_str,
Ok(UserRequestFilter::MemberOf(group_name)) )?;
} else if field == "objectclass" { Ok(UserRequestFilter::MemberOf(group_name))
if value == "person"
|| value == "inetOrgPerson"
|| value == "posixAccount"
|| value == "mailAccount"
{
Ok(UserRequestFilter::And(vec![]))
} else {
Ok(UserRequestFilter::Not(Box::new(UserRequestFilter::And(
vec![],
))))
} }
} else { "objectclass" => {
match map_field(field) { 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) => { Ok(field) => {
if field == "user_id" { if field == "user_id" {
Ok(UserRequestFilter::UserId(UserId::new(value))) Ok(UserRequestFilter::UserId(UserId::new(value)))
} else { } else {
Ok(UserRequestFilter::Equality(field, value.clone())) Ok(UserRequestFilter::Equality(
field.to_string(),
value.clone(),
))
} }
} }
Err(_) => { Err(_) => {
@ -968,7 +981,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
vec![], vec![],
)))) ))))
} }
} },
} }
} }
LdapFilter::Present(field) => { LdapFilter::Present(field) => {
@ -990,8 +1003,12 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::domain::{error::Result, handler::*, opaque_handler::*}; use crate::{
domain::{error::Result, handler::*, opaque_handler::*},
uuid,
};
use async_trait::async_trait; use async_trait::async_trait;
use chrono::TimeZone;
use ldap3_server::proto::{LdapDerefAliases, LdapSearchScope}; use ldap3_server::proto::{LdapDerefAliases, LdapSearchScope};
use mockall::predicate::eq; use mockall::predicate::eq;
use std::collections::HashSet; use std::collections::HashSet;
@ -1011,8 +1028,8 @@ mod tests {
async fn list_users(&self, filters: Option<UserRequestFilter>, get_groups: bool) -> Result<Vec<UserAndGroups>>; async fn list_users(&self, filters: Option<UserRequestFilter>, get_groups: bool) -> Result<Vec<UserAndGroups>>;
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>; async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>;
async fn get_user_details(&self, user_id: &UserId) -> Result<User>; async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupIdAndName>; async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>;
async fn get_user_groups(&self, user: &UserId) -> Result<HashSet<GroupIdAndName>>; async fn get_user_groups(&self, user: &UserId) -> Result<HashSet<GroupDetails>>;
async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>;
@ -1079,7 +1096,12 @@ mod tests {
.with(eq(UserId::new("test"))) .with(eq(UserId::new("test")))
.return_once(|_| { .return_once(|_| {
let mut set = HashSet::new(); 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) Ok(set)
}); });
let mut ldap_handler = let mut ldap_handler =
@ -1155,7 +1177,12 @@ mod tests {
.with(eq(UserId::new("test"))) .with(eq(UserId::new("test")))
.return_once(|_| { .return_once(|_| {
let mut set = HashSet::new(); 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) Ok(set)
}); });
let mut ldap_handler = let mut ldap_handler =
@ -1237,7 +1264,12 @@ mod tests {
user_id: UserId::new("bob"), user_id: UserId::new("bob"),
..Default::default() ..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; let mut ldap_handler = setup_bound_readonly_handler(mock).await;
@ -1386,6 +1418,7 @@ mod tests {
display_name: "Bôb Böbberson".to_string(), display_name: "Bôb Böbberson".to_string(),
first_name: "Bôb".to_string(), first_name: "Bôb".to_string(),
last_name: "Böbberson".to_string(), last_name: "Böbberson".to_string(),
uuid: uuid!("698e1d5f-7a40-3151-8745-b9b8a37839da"),
..Default::default() ..Default::default()
}, },
groups: None, groups: None,
@ -1397,6 +1430,7 @@ mod tests {
display_name: "Jimminy Cricket".to_string(), display_name: "Jimminy Cricket".to_string(),
first_name: "Jim".to_string(), first_name: "Jim".to_string(),
last_name: "Cricket".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), creation_date: Utc.ymd(2014, 7, 8).and_hms(9, 10, 11),
}, },
groups: None, groups: None,
@ -1415,6 +1449,7 @@ mod tests {
"sn", "sn",
"cn", "cn",
"createTimestamp", "createTimestamp",
"entryUuid",
], ],
); );
assert_eq!( assert_eq!(
@ -1459,7 +1494,11 @@ mod tests {
LdapPartialAttribute { LdapPartialAttribute {
atype: "createTimestamp".to_string(), atype: "createTimestamp".to_string(),
vals: vec!["1970-01-01T00:00:00+00:00".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 { LdapOp::SearchResultEntry(LdapSearchResultEntry {
@ -1501,7 +1540,11 @@ mod tests {
LdapPartialAttribute { LdapPartialAttribute {
atype: "createTimestamp".to_string(), atype: "createTimestamp".to_string(),
vals: vec!["2014-07-08T09:10:11+00:00".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(), make_search_success(),
@ -1520,12 +1563,16 @@ mod tests {
Group { Group {
id: GroupId(1), id: GroupId(1),
display_name: "group_1".to_string(), display_name: "group_1".to_string(),
creation_date: chrono::Utc.timestamp(42, 42),
users: vec![UserId::new("bob"), UserId::new("john")], users: vec![UserId::new("bob"), UserId::new("john")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
}, },
Group { Group {
id: GroupId(3), id: GroupId(3),
display_name: "BestGroup".to_string(), display_name: "BestGroup".to_string(),
creation_date: chrono::Utc.timestamp(42, 42),
users: vec![UserId::new("john")], users: vec![UserId::new("john")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
}, },
]) ])
}); });
@ -1533,7 +1580,7 @@ mod tests {
let request = make_search_request( let request = make_search_request(
"ou=groups,dc=example,dc=cOm", "ou=groups,dc=example,dc=cOm",
LdapFilter::And(vec![]), LdapFilter::And(vec![]),
vec!["objectClass", "dn", "cn", "uniqueMember"], vec!["objectClass", "dn", "cn", "uniqueMember", "entryUuid"],
); );
assert_eq!( assert_eq!(
ldap_handler.do_search_or_dse(&request).await, ldap_handler.do_search_or_dse(&request).await,
@ -1560,6 +1607,10 @@ mod tests {
"uid=john,ou=people,dc=example,dc=com".to_string(), "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 { LdapOp::SearchResultEntry(LdapSearchResultEntry {
@ -1581,6 +1632,10 @@ mod tests {
atype: "uniqueMember".to_string(), atype: "uniqueMember".to_string(),
vals: vec!["uid=john,ou=people,dc=example,dc=com".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(), make_search_success(),
@ -1609,7 +1664,9 @@ mod tests {
Ok(vec![Group { Ok(vec![Group {
display_name: "group_1".to_string(), display_name: "group_1".to_string(),
id: GroupId(1), id: GroupId(1),
creation_date: chrono::Utc.timestamp(42, 42),
users: vec![], users: vec![],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
}]) }])
}); });
let mut ldap_handler = setup_bound_admin_handler(mock).await; let mut ldap_handler = setup_bound_admin_handler(mock).await;
@ -1658,7 +1715,9 @@ mod tests {
Ok(vec![Group { Ok(vec![Group {
display_name: "group_1".to_string(), display_name: "group_1".to_string(),
id: GroupId(1), id: GroupId(1),
creation_date: chrono::Utc.timestamp(42, 42),
users: vec![], users: vec![],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
}]) }])
}); });
let mut ldap_handler = setup_bound_admin_handler(mock).await; let mut ldap_handler = setup_bound_admin_handler(mock).await;
@ -1932,7 +1991,9 @@ mod tests {
Ok(vec![Group { Ok(vec![Group {
id: GroupId(1), id: GroupId(1),
display_name: "group_1".to_string(), display_name: "group_1".to_string(),
creation_date: chrono::Utc.timestamp(42, 42),
users: vec![UserId::new("bob"), UserId::new("john")], 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; let mut ldap_handler = setup_bound_admin_handler(mock).await;
@ -1989,7 +2050,6 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn test_search_wildcards() { async fn test_search_wildcards() {
use chrono::TimeZone;
let mut mock = MockTestBackendHandler::new(); let mut mock = MockTestBackendHandler::new();
mock.expect_list_users().returning(|_, _| { mock.expect_list_users().returning(|_, _| {
@ -2011,7 +2071,9 @@ mod tests {
Ok(vec![Group { Ok(vec![Group {
id: GroupId(1), id: GroupId(1),
display_name: "group_1".to_string(), display_name: "group_1".to_string(),
creation_date: chrono::Utc.timestamp(42, 42),
users: vec![UserId::new("bob"), UserId::new("john")], 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; let mut ldap_handler = setup_bound_admin_handler(mock).await;

View File

@ -38,8 +38,8 @@ mockall::mock! {
async fn list_users(&self, filters: Option<UserRequestFilter>, get_groups: bool) -> Result<Vec<UserAndGroups>>; async fn list_users(&self, filters: Option<UserRequestFilter>, get_groups: bool) -> Result<Vec<UserAndGroups>>;
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>; async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>;
async fn get_user_details(&self, user_id: &UserId) -> Result<User>; async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupIdAndName>; async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>;
async fn get_user_groups(&self, user: &UserId) -> Result<HashSet<GroupIdAndName>>; async fn get_user_groups(&self, user: &UserId) -> Result<HashSet<GroupDetails>>;
async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>;