diff --git a/crates/cli/src/modules/account.rs b/crates/cli/src/modules/account.rs index aa487a9f..c03881e4 100644 --- a/crates/cli/src/modules/account.rs +++ b/crates/cli/src/modules/account.rs @@ -46,7 +46,6 @@ impl AccountCommands { member_of, } => { let principal = Principal { - id: None, typ: if is_admin.unwrap_or_default() { Type::Superuser } else { @@ -54,12 +53,12 @@ impl AccountCommands { } .into(), quota, - used_quota: None, name: name.clone().into(), secrets: vec![sha512_crypt::hash(password).unwrap()], emails: addresses.unwrap_or_default(), member_of: member_of.unwrap_or_default(), description, + ..Default::default() }; let account_id = client .http_request::(Method::POST, "/admin/principal", Some(principal)) @@ -104,11 +103,15 @@ impl AccountCommands { if let Some(is_admin) = is_admin { changes.push(PrincipalUpdate::set( PrincipalField::Type, - PrincipalValue::Type(if is_admin { - Type::Superuser - } else { - Type::Individual - }), + PrincipalValue::String( + if is_admin { + Type::Superuser + } else { + Type::Individual + } + .to_string() + .to_ascii_lowercase(), + ), )); } if let Some(addresses) = addresses { @@ -255,37 +258,48 @@ impl Client { Cell::new(&name), ])); } - let is_list = if let Some(typ) = principal.typ { + if let Some(typ) = principal.typ { table.add_row(Row::new(vec![ Cell::new("Type").with_style(Attr::Bold), Cell::new(&typ.to_string()), ])); - matches!(typ, Type::List) - } else { - false - }; + } if let Some(description) = principal.description { table.add_row(Row::new(vec![ Cell::new("Description").with_style(Attr::Bold), Cell::new(&description), ])); } - if let Some(quota) = principal.quota { - table.add_row(Row::new(vec![ - Cell::new("Quota").with_style(Attr::Bold), - Cell::new("a.to_string()), - ])); + if matches!( + principal.typ, + Some(Type::Individual | Type::Superuser | Type::Group) + ) { + if let Some(quota) = principal.quota { + table.add_row(Row::new(vec![ + Cell::new("Quota").with_style(Attr::Bold), + if quota != 0 { + Cell::new("a.to_string()) + } else { + Cell::new("Unlimited") + }, + ])); + } + if let Some(used_quota) = principal.used_quota { + table.add_row(Row::new(vec![ + Cell::new("Used Quota").with_style(Attr::Bold), + Cell::new(&used_quota.to_string()), + ])); + } } - if let Some(used_quota) = principal.used_quota { + if !principal.members.is_empty() { table.add_row(Row::new(vec![ - Cell::new("Used Quota").with_style(Attr::Bold), - Cell::new(&used_quota.to_string()), + Cell::new("Members").with_style(Attr::Bold), + Cell::new(&principal.members.join(", ")), ])); } if !principal.member_of.is_empty() { table.add_row(Row::new(vec![ - Cell::new(if is_list { "List members" } else { "Member of" }) - .with_style(Attr::Bold), + Cell::new("Member of").with_style(Attr::Bold), Cell::new(&principal.member_of.join(", ")), ])); } diff --git a/crates/cli/src/modules/cli.rs b/crates/cli/src/modules/cli.rs index 67e757d0..c8dee697 100644 --- a/crates/cli/src/modules/cli.rs +++ b/crates/cli/src/modules/cli.rs @@ -273,9 +273,9 @@ pub enum GroupCommands { /// Description #[clap(short, long)] description: Option, - /// Groups that this group is a member of + /// Group members #[clap(short, long)] - member_of: Option>, + members: Option>, }, /// Update an existing group @@ -291,25 +291,25 @@ pub enum GroupCommands { description: Option, /// Update groups that this group is a member of #[clap(short, long)] - member_of: Option>, + members: Option>, }, - /// Add a group to other groups - AddToGroup { + /// Add members to a group + AddMembers { /// Group name name: String, /// Groups to add #[clap(required = true)] - member_of: Vec, + members: Vec, }, - /// Remove a group account from groups - RemoveFromGroup { + /// Remove members from a group + RemoveMembers { /// Group name name: String, /// Groups to remove #[clap(required = true)] - member_of: Vec, + members: Vec, }, /// Display an existing group diff --git a/crates/cli/src/modules/group.rs b/crates/cli/src/modules/group.rs index 7e61c98a..539ae373 100644 --- a/crates/cli/src/modules/group.rs +++ b/crates/cli/src/modules/group.rs @@ -21,6 +21,8 @@ * for more details. */ +use std::vec; + use reqwest::Method; use serde_json::Value; @@ -38,22 +40,30 @@ impl GroupCommands { name, email, description, - member_of, + members, } => { let principal = Principal { - id: None, typ: Some(Type::Group), - quota: None, - used_quota: None, name: name.clone().into(), - secrets: vec![], emails: email.map(|e| vec![e]).unwrap_or_default(), - member_of: member_of.unwrap_or_default(), description, + ..Default::default() }; let account_id = client .http_request::(Method::POST, "/admin/principal", Some(principal)) .await; + if let Some(members) = members { + client + .http_request::( + Method::PATCH, + &format!("/admin/principal/{name}"), + Some(vec![PrincipalUpdate::set( + PrincipalField::Members, + PrincipalValue::StringList(members), + )]), + ) + .await; + } eprintln!("Successfully created group {name:?} with id {account_id}."); } GroupCommands::Update { @@ -61,7 +71,7 @@ impl GroupCommands { new_name, email, description, - member_of, + members, } => { let mut changes = Vec::new(); if let Some(new_name) = new_name { @@ -76,10 +86,10 @@ impl GroupCommands { PrincipalValue::StringList(vec![email]), )); } - if let Some(member_of) = member_of { + if let Some(members) = members { changes.push(PrincipalUpdate::set( - PrincipalField::MemberOf, - PrincipalValue::StringList(member_of), + PrincipalField::Members, + PrincipalValue::StringList(members), )); } if let Some(description) = description { @@ -102,17 +112,17 @@ impl GroupCommands { eprintln!("No changes to apply."); } } - GroupCommands::AddToGroup { name, member_of } => { + GroupCommands::AddMembers { name, members } => { client .http_request::( Method::PATCH, &format!("/admin/principal/{name}"), Some( - member_of + members .into_iter() .map(|group| { PrincipalUpdate::add_item( - PrincipalField::MemberOf, + PrincipalField::Members, PrincipalValue::String(group), ) }) @@ -122,17 +132,17 @@ impl GroupCommands { .await; eprintln!("Successfully updated group {name:?}."); } - GroupCommands::RemoveFromGroup { name, member_of } => { + GroupCommands::RemoveMembers { name, members } => { client .http_request::( Method::PATCH, &format!("/admin/principal/{name}"), Some( - member_of + members .into_iter() .map(|group| { PrincipalUpdate::remove_item( - PrincipalField::MemberOf, + PrincipalField::Members, PrincipalValue::String(group), ) }) diff --git a/crates/cli/src/modules/list.rs b/crates/cli/src/modules/list.rs index 6b2c241b..88be2711 100644 --- a/crates/cli/src/modules/list.rs +++ b/crates/cli/src/modules/list.rs @@ -21,6 +21,8 @@ * for more details. */ +use std::vec; + use reqwest::Method; use serde_json::Value; @@ -41,19 +43,27 @@ impl ListCommands { members, } => { let principal = Principal { - id: None, typ: Some(Type::List), - quota: None, - used_quota: None, name: name.clone().into(), - secrets: vec![], emails: vec![email], - member_of: members.unwrap_or_default(), description, + ..Default::default() }; let account_id = client .http_request::(Method::POST, "/admin/principal", Some(principal)) .await; + if let Some(members) = members { + client + .http_request::( + Method::PATCH, + &format!("/admin/principal/{name}"), + Some(vec![PrincipalUpdate::set( + PrincipalField::Members, + PrincipalValue::StringList(members), + )]), + ) + .await; + } eprintln!("Successfully created mailing list {name:?} with id {account_id}."); } ListCommands::Update { @@ -78,7 +88,7 @@ impl ListCommands { } if let Some(members) = members { changes.push(PrincipalUpdate::set( - PrincipalField::MemberOf, + PrincipalField::Members, PrincipalValue::StringList(members), )); } @@ -112,7 +122,7 @@ impl ListCommands { .into_iter() .map(|group| { PrincipalUpdate::add_item( - PrincipalField::MemberOf, + PrincipalField::Members, PrincipalValue::String(group), ) }) @@ -132,7 +142,7 @@ impl ListCommands { .into_iter() .map(|group| { PrincipalUpdate::remove_item( - PrincipalField::MemberOf, + PrincipalField::Members, PrincipalValue::String(group), ) }) diff --git a/crates/cli/src/modules/mod.rs b/crates/cli/src/modules/mod.rs index 1cf6e797..b4f22d9d 100644 --- a/crates/cli/src/modules/mod.rs +++ b/crates/cli/src/modules/mod.rs @@ -25,10 +25,7 @@ use std::{collections::HashMap, fmt::Display, io::Read}; use jmap_client::{ client::Client, - principal::{ - query::{self}, - Property, - }, + principal::query::{self}, }; use serde::{Deserialize, Serialize}; @@ -73,6 +70,10 @@ pub struct Principal { #[serde(rename = "memberOf")] pub member_of: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(rename = "members")] + pub members: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, } @@ -112,6 +113,8 @@ pub enum PrincipalField { Emails, #[serde(rename = "memberOf")] MemberOf, + #[serde(rename = "members")] + Members, } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -137,7 +140,6 @@ pub enum PrincipalValue { String(String), StringList(Vec), Integer(u32), - Type(Type), } impl PrincipalUpdate { @@ -198,27 +200,6 @@ trait TableName { fn table_name(&self) -> &'static str; } -impl TableName for Property { - fn table_name(&self) -> &'static str { - match self { - Property::Id => "Id", - Property::Type => "Type", - Property::Name => "Name", - Property::Description => "Description", - Property::Email => "E-mail", - Property::Timezone => "Timezone", - Property::Capabilities => "Capabilities", - Property::Aliases => "Aliases", - Property::Secret => "Secret", - Property::DKIM => "DKIM", - Property::Quota => "Quota", - Property::Picture => "Picture", - Property::Members => "Members", - Property::ACL => "ACL", - } - } -} - pub fn read_file(path: &str) -> Vec { if path == "-" { let mut stdin = std::io::stdin().lock(); diff --git a/crates/directory/src/backend/internal/lookup.rs b/crates/directory/src/backend/internal/lookup.rs index d20b4576..e85fdb81 100644 --- a/crates/directory/src/backend/internal/lookup.rs +++ b/crates/directory/src/backend/internal/lookup.rs @@ -27,13 +27,17 @@ use store::{ IterateParams, Store, ValueKey, }; -use crate::{Principal, QueryBy}; +use crate::{Principal, QueryBy, Type}; -use super::manage::ManageDirectory; +use super::{manage::ManageDirectory, PrincipalIdType}; #[async_trait::async_trait] pub trait DirectoryStore: Sync + Send { - async fn query(&self, by: QueryBy<'_>) -> crate::Result>>; + async fn query( + &self, + by: QueryBy<'_>, + return_member_of: bool, + ) -> crate::Result>>; async fn email_to_ids(&self, email: &str) -> crate::Result>; async fn is_local_domain(&self, domain: &str) -> crate::Result; @@ -44,29 +48,28 @@ pub trait DirectoryStore: Sync + Send { #[async_trait::async_trait] impl DirectoryStore for Store { - async fn query(&self, by: QueryBy<'_>) -> crate::Result>> { - let (username, secret) = match by { - QueryBy::Name(name) => (name, None), - QueryBy::Id(account_id) => { - return self - .get_value::>(ValueKey::from(ValueClass::Directory( - DirectoryClass::Principal(account_id), - ))) - .await - .map_err(Into::into); - } + async fn query( + &self, + by: QueryBy<'_>, + return_member_of: bool, + ) -> crate::Result>> { + let (account_id, secret) = match by { + QueryBy::Name(name) => (self.get_account_id(name).await?, None), + QueryBy::Id(account_id) => (account_id.into(), None), QueryBy::Credentials(credentials) => match credentials { Credentials::Plain { username, secret } => { - (username.as_str(), secret.as_str().into()) + (self.get_account_id(username).await?, secret.as_str().into()) + } + Credentials::OAuthBearer { token } => { + (self.get_account_id(token).await?, token.as_str().into()) } - Credentials::OAuthBearer { token } => (token.as_str(), token.as_str().into()), Credentials::XOauth2 { username, secret } => { - (username.as_str(), secret.as_str().into()) + (self.get_account_id(username).await?, secret.as_str().into()) } }, }; - if let Some(account_id) = self.get_account_id(username).await? { + if let Some(account_id) = account_id { match ( self.get_value::>(ValueKey::from(ValueClass::Directory( DirectoryClass::Principal(account_id), @@ -74,10 +77,19 @@ impl DirectoryStore for Store { .await?, secret, ) { - (Some(principal), Some(secret)) if principal.verify_secret(secret).await => { + (Some(mut principal), Some(secret)) if principal.verify_secret(secret).await => { + if return_member_of { + principal.member_of = self.get_member_of(principal.id).await?; + } + Ok(Some(principal)) + } + (Some(mut principal), None) => { + if return_member_of { + principal.member_of = self.get_member_of(principal.id).await?; + } + Ok(Some(principal)) } - (Some(principal), None) => Ok(Some(principal)), _ => Ok(None), } } else { @@ -86,12 +98,20 @@ impl DirectoryStore for Store { } async fn email_to_ids(&self, email: &str) -> crate::Result> { - self.get_value::>(ValueKey::from(ValueClass::Directory( - DirectoryClass::EmailToId(email.as_bytes().to_vec()), - ))) - .await - .map(|ids| ids.unwrap_or_default()) - .map_err(Into::into) + if let Some(ptype) = self + .get_value::(ValueKey::from(ValueClass::Directory( + DirectoryClass::EmailToId(email.as_bytes().to_vec()), + ))) + .await? + { + if ptype.typ != Type::List { + Ok(vec![ptype.account_id]) + } else { + self.get_members(ptype.account_id).await.map_err(Into::into) + } + } else { + Ok(Vec::new()) + } } async fn is_local_domain(&self, domain: &str) -> crate::Result { diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs index 33cbd840..05510a3b 100644 --- a/crates/directory/src/backend/internal/manage.rs +++ b/crates/directory/src/backend/internal/manage.rs @@ -23,8 +23,10 @@ use jmap_proto::types::collection::Collection; use store::{ - write::{assert::HashedValue, BatchBuilder, DirectoryClass, ValueClass}, - Deserialize, IterateParams, Serialize, Store, ValueKey, + write::{ + assert::HashedValue, key::DeserializeBigEndian, BatchBuilder, DirectoryClass, ValueClass, + }, + Deserialize, IterateParams, Serialize, Store, ValueKey, U32_LEN, }; use crate::{DirectoryError, ManagementError, Principal, QueryBy, Type}; @@ -39,6 +41,8 @@ pub trait ManageDirectory: Sized { async fn get_account_id(&self, name: &str) -> crate::Result>; async fn get_or_create_account_id(&self, name: &str) -> crate::Result; async fn get_account_name(&self, account_id: u32) -> crate::Result>; + async fn get_member_of(&self, account_id: u32) -> crate::Result>; + async fn get_members(&self, account_id: u32) -> crate::Result>; async fn create_account(&self, principal: Principal) -> crate::Result; async fn update_account( &self, @@ -204,6 +208,7 @@ impl ManageDirectory for Store { // Write principal let mut batch = BatchBuilder::new(); + let ptype = PrincipalIdType::new(principal.id, principal.typ.into_base_type()).serialize(); batch .assert_value( ValueClass::Directory(DirectoryClass::NameToId( @@ -217,20 +222,32 @@ impl ManageDirectory for Store { ) .set( ValueClass::Directory(DirectoryClass::NameToId(principal.name.into_bytes())), - PrincipalIdType::new(principal.id, principal.typ.into_base_type()).serialize(), + ptype.clone(), ); // Write email to id mapping - let ids = if matches!(principal.typ, Type::List) { - principal.member_of - } else { - vec![principal.id] - }; - for email in principal.emails { batch.set( ValueClass::Directory(DirectoryClass::EmailToId(email.into_bytes())), - (&ids).serialize(), + ptype.clone(), + ); + } + + // Write membership + for member_of in principal.member_of { + batch.set( + ValueClass::Directory(DirectoryClass::MemberOf { + principal_id: principal.id, + member_of, + }), + vec![], + ); + batch.set( + ValueClass::Directory(DirectoryClass::Members { + principal_id: member_of, + has_member: principal.id, + }), + vec![], ); } @@ -278,6 +295,28 @@ impl ManageDirectory for Store { batch.clear(DirectoryClass::EmailToId(email.into_bytes())); } + for member_id in self.get_member_of(account_id).await? { + batch.clear(DirectoryClass::MemberOf { + principal_id: account_id, + member_of: member_id, + }); + batch.clear(DirectoryClass::Members { + principal_id: member_id, + has_member: account_id, + }); + } + + for member_id in self.get_members(account_id).await? { + batch.clear(DirectoryClass::MemberOf { + principal_id: member_id, + member_of: account_id, + }); + batch.clear(DirectoryClass::Members { + principal_id: account_id, + has_member: member_id, + }); + } + self.write(batch.build()).await?; Ok(()) @@ -306,14 +345,25 @@ impl ManageDirectory for Store { DirectoryError::Management(ManagementError::NotFound(account_id.to_string())) })?; + // Obtain members and memberOf + let mut member_of = self.get_member_of(account_id).await?; + let mut members = self.get_members(account_id).await?; + // Apply changes let mut batch = BatchBuilder::new(); - let is_list = matches!(principal.inner.typ, Type::List); - let mut has_list_changes = false; - batch.assert_value( - ValueClass::Directory(DirectoryClass::Principal(account_id)), - &principal, - ); + let ptype = + PrincipalIdType::new(account_id, principal.inner.typ.into_base_type()).serialize(); + let update_principal = !changes.is_empty() + && !changes + .iter() + .all(|c| matches!(c.field, PrincipalField::MemberOf | PrincipalField::Members)); + + if update_principal { + batch.assert_value( + ValueClass::Directory(DirectoryClass::Principal(account_id)), + &principal, + ); + } for change in changes { match (change.action, change.field, change.value) { (PrincipalAction::Set, PrincipalField::Name, PrincipalValue::String(new_name)) => { @@ -337,16 +387,20 @@ impl ManageDirectory for Store { batch.set( ValueClass::Directory(DirectoryClass::NameToId(new_name.into_bytes())), - PrincipalIdType::new(account_id, principal.inner.typ.into_base_type()) - .serialize(), + ptype.clone(), ); } } - (PrincipalAction::Set, PrincipalField::Type, PrincipalValue::Type(new_type)) - if matches!(principal.inner.typ, Type::Individual | Type::Superuser) - && matches!(new_type, Type::Individual | Type::Superuser) => - { - principal.inner.typ = new_type; + (PrincipalAction::Set, PrincipalField::Type, PrincipalValue::String(new_type)) => { + if let Some(new_type) = Type::parse(&new_type) { + if matches!(principal.inner.typ, Type::Individual | Type::Superuser) + && matches!(new_type, Type::Individual | Type::Superuser) + { + principal.inner.typ = new_type; + continue; + } + } + return Err(DirectoryError::Unsupported); } ( PrincipalAction::Set, @@ -369,6 +423,8 @@ impl ManageDirectory for Store { (PrincipalAction::Set, PrincipalField::Quota, PrincipalValue::Integer(quota)) => { principal.inner.quota = quota; } + + // Emails ( PrincipalAction::Set, PrincipalField::Emails, @@ -396,59 +452,25 @@ impl ManageDirectory for Store { )); } } - if !is_list { - batch.set( - ValueClass::Directory(DirectoryClass::EmailToId( - email.as_bytes().to_vec(), - )), - vec![account_id].serialize(), - ); - } + batch.set( + ValueClass::Directory(DirectoryClass::EmailToId( + email.as_bytes().to_vec(), + )), + ptype.clone(), + ); } } - if !is_list { - for email in &principal.inner.emails { - if !emails.contains(email) { - batch.clear(ValueClass::Directory(DirectoryClass::EmailToId( - email.as_bytes().to_vec(), - ))); - } + + for email in &principal.inner.emails { + if !emails.contains(email) { + batch.clear(ValueClass::Directory(DirectoryClass::EmailToId( + email.as_bytes().to_vec(), + ))); } } principal.inner.emails = emails; } - ( - PrincipalAction::Set, - PrincipalField::MemberOf, - PrincipalValue::StringList(members), - ) => { - if is_list { - has_list_changes = true; - } - principal.inner.member_of = Vec::with_capacity(members.len()); - for member in members { - let account_id = self.get_account_id(&member).await?.ok_or_else(|| { - DirectoryError::Management(ManagementError::NotFound(member)) - })?; - principal.inner.member_of.push(account_id); - } - } - ( - PrincipalAction::AddItem, - PrincipalField::MemberOf, - PrincipalValue::String(member), - ) => { - let account_id = self.get_account_id(&member).await?.ok_or_else(|| { - DirectoryError::Management(ManagementError::NotFound(member)) - })?; - if !principal.inner.member_of.contains(&account_id) { - principal.inner.member_of.push(account_id); - if is_list { - has_list_changes = true; - } - } - } ( PrincipalAction::AddItem, PrincipalField::Emails, @@ -471,36 +493,15 @@ impl ManageDirectory for Store { ))); } } - if !is_list { - batch.set( - ValueClass::Directory(DirectoryClass::EmailToId( - email.as_bytes().to_vec(), - )), - vec![account_id].serialize(), - ); - } + batch.set( + ValueClass::Directory(DirectoryClass::EmailToId( + email.as_bytes().to_vec(), + )), + ptype.clone(), + ); principal.inner.emails.push(email); } } - ( - PrincipalAction::RemoveItem, - PrincipalField::MemberOf, - PrincipalValue::String(member), - ) => { - if let Some(account_id) = self.get_account_id(&member).await? { - if let Some(pos) = principal - .inner - .member_of - .iter() - .position(|v| *v == account_id) - { - principal.inner.member_of.remove(pos); - if is_list { - has_list_changes = true; - } - } - } - } ( PrincipalAction::RemoveItem, PrincipalField::Emails, @@ -508,34 +509,209 @@ impl ManageDirectory for Store { ) => { let email = email.to_lowercase(); if let Some(pos) = principal.inner.emails.iter().position(|v| *v == email) { - if !is_list { - batch.clear(ValueClass::Directory(DirectoryClass::EmailToId( - email.as_bytes().to_vec(), - ))); - } + batch.clear(ValueClass::Directory(DirectoryClass::EmailToId( + email.as_bytes().to_vec(), + ))); principal.inner.emails.remove(pos); } } + + // MemberOf + ( + PrincipalAction::Set, + PrincipalField::MemberOf, + PrincipalValue::StringList(members), + ) => { + let mut new_member_of = Vec::new(); + for member in members { + let member_id = self.get_account_id(&member).await?.ok_or_else(|| { + DirectoryError::Management(ManagementError::NotFound(member)) + })?; + if !member_of.contains(&member_id) { + batch.set( + ValueClass::Directory(DirectoryClass::MemberOf { + principal_id: account_id, + member_of: member_id, + }), + vec![], + ); + batch.set( + ValueClass::Directory(DirectoryClass::Members { + principal_id: member_id, + has_member: account_id, + }), + vec![], + ); + } + + new_member_of.push(member_id); + } + + for member_id in &member_of { + if !new_member_of.contains(member_id) { + batch.clear(ValueClass::Directory(DirectoryClass::MemberOf { + principal_id: account_id, + member_of: *member_id, + })); + batch.clear(ValueClass::Directory(DirectoryClass::Members { + principal_id: *member_id, + has_member: account_id, + })); + } + } + + member_of = new_member_of; + } + ( + PrincipalAction::AddItem, + PrincipalField::MemberOf, + PrincipalValue::String(member), + ) => { + let member_id = self.get_account_id(&member).await?.ok_or_else(|| { + DirectoryError::Management(ManagementError::NotFound(member)) + })?; + if !member_of.contains(&member_id) { + batch.set( + ValueClass::Directory(DirectoryClass::MemberOf { + principal_id: account_id, + member_of: member_id, + }), + vec![], + ); + batch.set( + ValueClass::Directory(DirectoryClass::Members { + principal_id: member_id, + has_member: account_id, + }), + vec![], + ); + member_of.push(member_id); + } + } + ( + PrincipalAction::RemoveItem, + PrincipalField::MemberOf, + PrincipalValue::String(member), + ) => { + if let Some(member_id) = self.get_account_id(&member).await? { + if let Some(pos) = member_of.iter().position(|v| *v == member_id) { + batch.clear(ValueClass::Directory(DirectoryClass::MemberOf { + principal_id: account_id, + member_of: member_id, + })); + batch.clear(ValueClass::Directory(DirectoryClass::Members { + principal_id: member_id, + has_member: account_id, + })); + member_of.remove(pos); + } + } + } + + ( + PrincipalAction::Set, + PrincipalField::Members, + PrincipalValue::StringList(members_), + ) => { + let mut new_members = Vec::new(); + for member in members_ { + let member_id = self.get_account_id(&member).await?.ok_or_else(|| { + DirectoryError::Management(ManagementError::NotFound(member)) + })?; + if !members.contains(&member_id) { + batch.set( + ValueClass::Directory(DirectoryClass::MemberOf { + principal_id: member_id, + member_of: account_id, + }), + vec![], + ); + batch.set( + ValueClass::Directory(DirectoryClass::Members { + principal_id: account_id, + has_member: member_id, + }), + vec![], + ); + } + + new_members.push(member_id); + } + + for member_id in &members { + if !new_members.contains(member_id) { + batch.clear(ValueClass::Directory(DirectoryClass::MemberOf { + principal_id: *member_id, + member_of: account_id, + })); + batch.clear(ValueClass::Directory(DirectoryClass::Members { + principal_id: account_id, + has_member: *member_id, + })); + } + } + + members = new_members; + } + ( + PrincipalAction::AddItem, + PrincipalField::Members, + PrincipalValue::String(member), + ) => { + let member_id = self.get_account_id(&member).await?.ok_or_else(|| { + DirectoryError::Management(ManagementError::NotFound(member)) + })?; + if !members.contains(&member_id) { + batch.set( + ValueClass::Directory(DirectoryClass::MemberOf { + principal_id: member_id, + member_of: account_id, + }), + vec![], + ); + batch.set( + ValueClass::Directory(DirectoryClass::Members { + principal_id: account_id, + has_member: member_id, + }), + vec![], + ); + members.push(member_id); + } + } + ( + PrincipalAction::RemoveItem, + PrincipalField::Members, + PrincipalValue::String(member), + ) => { + if let Some(member_id) = self.get_account_id(&member).await? { + if let Some(pos) = members.iter().position(|v| *v == member_id) { + batch.clear(ValueClass::Directory(DirectoryClass::MemberOf { + principal_id: member_id, + member_of: account_id, + })); + batch.clear(ValueClass::Directory(DirectoryClass::Members { + principal_id: account_id, + has_member: member_id, + })); + members.remove(pos); + } + } + } + _ => { return Err(DirectoryError::Unsupported); } } } - if has_list_changes { - for email in &principal.inner.emails { - batch.set( - ValueClass::Directory(DirectoryClass::EmailToId(email.as_bytes().to_vec())), - (&principal.inner.member_of).serialize(), - ); - } + if update_principal { + batch.set( + ValueClass::Directory(DirectoryClass::Principal(account_id)), + principal.inner.serialize(), + ); } - batch.set( - ValueClass::Directory(DirectoryClass::Principal(account_id)), - principal.inner.serialize(), - ); - self.write(batch.build()).await?; Ok(()) @@ -683,6 +859,48 @@ impl ManageDirectory for Store { Ok(results) } + async fn get_member_of(&self, account_id: u32) -> crate::Result> { + let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::MemberOf { + principal_id: account_id, + member_of: 0, + })); + let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::MemberOf { + principal_id: account_id, + member_of: u32::MAX, + })); + let mut results = Vec::new(); + self.iterate( + IterateParams::new(from_key, to_key).no_values(), + |key, _| { + results.push(key.deserialize_be_u32(key.len() - U32_LEN)?); + Ok(true) + }, + ) + .await?; + Ok(results) + } + + async fn get_members(&self, account_id: u32) -> crate::Result> { + let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Members { + principal_id: account_id, + has_member: 0, + })); + let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Members { + principal_id: account_id, + has_member: u32::MAX, + })); + let mut results = Vec::new(); + self.iterate( + IterateParams::new(from_key, to_key).no_values(), + |key, _| { + results.push(key.deserialize_be_u32(key.len() - U32_LEN)?); + Ok(true) + }, + ) + .await?; + Ok(results) + } + async fn init(self) -> crate::Result { if let (Ok(admin_user), Ok(admin_pass)) = ( std::env::var("SET_ADMIN_USER"), @@ -698,7 +916,7 @@ impl ManageDirectory for Store { }], ) .await?; - eprintln!("Successfully updated administrator password for {admin_user:?}."); + eprintln!("Successfully updated password for {admin_user:?}."); } else { self.create_account(Principal { typ: Type::Superuser, diff --git a/crates/directory/src/backend/internal/mod.rs b/crates/directory/src/backend/internal/mod.rs index 8bc86cc2..ca95b18d 100644 --- a/crates/directory/src/backend/internal/mod.rs +++ b/crates/directory/src/backend/internal/mod.rs @@ -50,7 +50,6 @@ impl Serialize for &Principal { + self.name.len() + self.emails.iter().map(|s| s.len()).sum::() + self.secrets.iter().map(|s| s.len()).sum::() - + self.member_of.len() * U32_LEN + self.description.as_ref().map(|s| s.len()).unwrap_or(0), ) .write(1u8) @@ -69,11 +68,6 @@ impl Serialize for &Principal { } } - serializer = serializer.write_leb128(self.member_of.len()); - for id in &self.member_of { - serializer = serializer.write_leb128(*id); - } - serializer.finalize() } } @@ -134,7 +128,7 @@ fn deserialize(bytes: &[u8]) -> Option> { })?, secrets: deserialize_string_list(&mut bytes)?, emails: deserialize_string_list(&mut bytes)?, - member_of: deserialize_u32_list(&mut bytes)?, + member_of: Vec::new(), } .into() } @@ -155,6 +149,8 @@ pub enum PrincipalField { Emails, #[serde(rename = "memberOf")] MemberOf, + #[serde(rename = "members")] + Members, } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -180,7 +176,6 @@ pub enum PrincipalValue { String(String), StringList(Vec), Integer(u32), - Type(Type), } impl PrincipalUpdate { @@ -219,6 +214,7 @@ impl Display for PrincipalField { PrincipalField::Secrets => write!(f, "secrets"), PrincipalField::Emails => write!(f, "emails"), PrincipalField::MemberOf => write!(f, "memberOf"), + PrincipalField::Members => write!(f, "members"), } } } @@ -241,19 +237,11 @@ fn deserialize_string_list(bytes: &mut Iter<'_, u8>) -> Option> { Some(list) } -fn deserialize_u32_list(bytes: &mut Iter<'_, u8>) -> Option> { - let len = bytes.next_leb128()?; - let mut list = Vec::with_capacity(len); - for _ in 0..len { - list.push(bytes.next_leb128()?); - } - Some(list) -} - impl Type { pub fn parse(value: &str) -> Option { match value { - "individual" | "superuser" => Some(Type::Individual), + "individual" => Some(Type::Individual), + "superuser" => Some(Type::Superuser), "group" => Some(Type::Group), "resource" => Some(Type::Resource), "location" => Some(Type::Location), diff --git a/crates/directory/src/backend/ldap/lookup.rs b/crates/directory/src/backend/ldap/lookup.rs index 05bdcaf9..39578225 100644 --- a/crates/directory/src/backend/ldap/lookup.rs +++ b/crates/directory/src/backend/ldap/lookup.rs @@ -30,7 +30,11 @@ use crate::{backend::internal::manage::ManageDirectory, DirectoryError, Principa use super::{LdapDirectory, LdapMappings}; impl LdapDirectory { - pub async fn query(&self, by: QueryBy<'_>) -> crate::Result>> { + pub async fn query( + &self, + by: QueryBy<'_>, + return_member_of: bool, + ) -> crate::Result>> { let mut conn = self.pool.get().await?; let mut account_id = None; let account_name; @@ -132,7 +136,7 @@ impl LdapDirectory { principal.name = account_name; // Obtain groups - if !principal.member_of.is_empty() && self.has_id_store() { + if return_member_of && !principal.member_of.is_empty() && self.has_id_store() { for member_of in principal.member_of.iter_mut() { if member_of.contains('=') { let (rs, _res) = conn @@ -165,6 +169,7 @@ impl LdapDirectory { .await .map(Some) } else { + principal.member_of.clear(); Ok(Some(principal.into())) } } diff --git a/crates/directory/src/backend/sql/lookup.rs b/crates/directory/src/backend/sql/lookup.rs index c57651a7..14855099 100644 --- a/crates/directory/src/backend/sql/lookup.rs +++ b/crates/directory/src/backend/sql/lookup.rs @@ -29,7 +29,11 @@ use crate::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type use super::{SqlDirectory, SqlMappings}; impl SqlDirectory { - pub async fn query(&self, by: QueryBy<'_>) -> crate::Result>> { + pub async fn query( + &self, + by: QueryBy<'_>, + return_member_of: bool, + ) -> crate::Result>> { let mut account_id = None; let account_name; let mut secret = None; @@ -106,7 +110,7 @@ impl SqlDirectory { if self.has_id_store() { // Obtain members - if !self.mappings.query_members.is_empty() { + if return_member_of && !self.mappings.query_members.is_empty() { for row in self .store .query::( diff --git a/crates/directory/src/core/dispatch.rs b/crates/directory/src/core/dispatch.rs index 510aea7f..6771f74f 100644 --- a/crates/directory/src/core/dispatch.rs +++ b/crates/directory/src/core/dispatch.rs @@ -26,11 +26,15 @@ use crate::{ }; impl Directory { - pub async fn query(&self, by: QueryBy<'_>) -> crate::Result>> { + pub async fn query( + &self, + by: QueryBy<'_>, + return_member_of: bool, + ) -> crate::Result>> { match &self.store { - DirectoryInner::Internal(store) => store.query(by).await, - DirectoryInner::Ldap(store) => store.query(by).await, - DirectoryInner::Sql(store) => store.query(by).await, + DirectoryInner::Internal(store) => store.query(by, return_member_of).await, + DirectoryInner::Ldap(store) => store.query(by, return_member_of).await, + DirectoryInner::Sql(store) => store.query(by, return_member_of).await, DirectoryInner::Imap(store) => store.query(by).await, DirectoryInner::Smtp(store) => store.query(by).await, DirectoryInner::Memory(store) => store.query(by).await, diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs index bedbdea2..730d6359 100644 --- a/crates/imap/src/core/mailbox.rs +++ b/crates/imap/src/core/mailbox.rs @@ -52,7 +52,7 @@ impl SessionData { session .jmap .directory - .query(QueryBy::Id(account_id)) + .query(QueryBy::Id(account_id), false) .await .unwrap_or_default() .map(|p| p.name) @@ -320,7 +320,7 @@ impl SessionData { self.imap.name_shared, self.jmap .directory - .query(QueryBy::Id(account_id)) + .query(QueryBy::Id(account_id), false) .await .unwrap_or_default() .map(|p| p.name) @@ -407,7 +407,7 @@ impl SessionData { self.imap.name_shared, self.jmap .directory - .query(QueryBy::Id(account_id)) + .query(QueryBy::Id(account_id), false) .await .unwrap_or_default() .map(|p| p.name) diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs index e2795283..8e8b030a 100644 --- a/crates/imap/src/op/acl.rs +++ b/crates/imap/src/op/acl.rs @@ -76,7 +76,7 @@ impl Session { if let Some(account_name) = data .jmap .directory - .query(QueryBy::Id(id.document_id())) + .query(QueryBy::Id(id.document_id()), false) .await .unwrap_or_default() .map(|p| p.name) @@ -248,7 +248,7 @@ impl Session { let (acl_account_id, id) = match data .jmap .directory - .query(QueryBy::Name(arguments.identifier.as_ref().unwrap())) + .query(QueryBy::Name(arguments.identifier.as_ref().unwrap()), false) .await { Ok(Some(principal)) => (principal.id, Value::Id(Id::from(principal.id))), diff --git a/crates/jmap/src/api/admin.rs b/crates/jmap/src/api/admin.rs index d4fd59b6..e75dd45d 100644 --- a/crates/jmap/src/api/admin.rs +++ b/crates/jmap/src/api/admin.rs @@ -47,6 +47,7 @@ pub struct PrincipalResponse { pub secrets: Vec, #[serde(rename = "memberOf")] pub member_of: Vec, + pub members: Vec, pub description: Option, } @@ -136,7 +137,7 @@ impl JMAP { match *method { Method::GET => { - let result = match self.store.query(QueryBy::Id(account_id)).await { + let result = match self.store.query(QueryBy::Id(account_id), true).await { Ok(Some(principal)) => self.store.map_group_ids(principal).await, Ok(None) => { return RequestError::blank( @@ -157,6 +158,17 @@ impl JMAP { self.get_used_quota(account_id).await.unwrap_or_default() as u32; + // Obtain member names + for member_id in + self.store.get_members(account_id).await.unwrap_or_default() + { + if let Ok(Some(member_principal)) = + self.store.query(QueryBy::Id(member_id), false).await + { + principal.members.push(member_principal.name); + } + } + JsonResponse::new(json!({ "data": principal, })) @@ -355,6 +367,7 @@ impl From> for PrincipalResponse { description: principal.description, secrets: principal.secrets, used_quota: 0, + members: Vec::new(), } } } diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index a1c2e74f..001f414e 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -214,7 +214,7 @@ impl JMAP { session.add_account( (*id).into(), self.directory - .query(QueryBy::Id(*id)) + .query(QueryBy::Id(*id), false) .await .unwrap_or_default() .map(|p| p.name) diff --git a/crates/jmap/src/auth/acl.rs b/crates/jmap/src/auth/acl.rs index eee51652..7193faad 100644 --- a/crates/jmap/src/auth/acl.rs +++ b/crates/jmap/src/auth/acl.rs @@ -379,7 +379,7 @@ impl JMAP { { if let Some(principal) = self .directory - .query(QueryBy::Id(id.document_id())) + .query(QueryBy::Id(id.document_id()), false) .await .unwrap_or_default() { @@ -452,7 +452,11 @@ impl JMAP { async fn map_acl_accounts(&self, mut acl_set: Vec) -> Result, SetError> { for item in &mut acl_set { if let Value::Text(account_name) = item { - match self.directory.query(QueryBy::Name(account_name)).await { + match self + .directory + .query(QueryBy::Name(account_name), false) + .await + { Ok(Some(principal)) => { *item = Value::Id(principal.id.into()); } diff --git a/crates/jmap/src/auth/authenticate.rs b/crates/jmap/src/auth/authenticate.rs index f22b1d48..311bba69 100644 --- a/crates/jmap/src/auth/authenticate.rs +++ b/crates/jmap/src/auth/authenticate.rs @@ -174,10 +174,13 @@ impl JMAP { ) -> Option { match self .directory - .query(QueryBy::Credentials(&Credentials::Plain { - username: username.to_string(), - secret: secret.to_string(), - })) + .query( + QueryBy::Credentials(&Credentials::Plain { + username: username.to_string(), + secret: secret.to_string(), + }), + true, + ) .await { Ok(Some(principal)) => AccessToken::new(principal).into(), @@ -192,7 +195,10 @@ impl JMAP { pub async fn get_access_token(&self, account_id: u32) -> Option { // Create access token self.update_access_token(AccessToken::new( - self.directory.query(QueryBy::Id(account_id)).await.ok()??, + self.directory + .query(QueryBy::Id(account_id), true) + .await + .ok()??, )) .await } diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs index c963f04a..a328e83b 100644 --- a/crates/jmap/src/auth/oauth/token.rs +++ b/crates/jmap/src/auth/oauth/token.rs @@ -182,7 +182,7 @@ impl JMAP { ) -> Result { let password_hash = self .directory - .query(QueryBy::Id(account_id)) + .query(QueryBy::Id(account_id), false) .await .map_err(|_| "Temporary lookup error")? .ok_or("Account no longer exists")? @@ -301,7 +301,7 @@ impl JMAP { let password_hash = self .directory - .query(QueryBy::Id(account_id)) + .query(QueryBy::Id(account_id), false) .await .map_err(|_| "Temporary lookup error")? .ok_or("Account no longer exists")? diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index 4e0961c7..d12c553c 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -75,7 +75,7 @@ impl JMAP { if let Value::Text(email) = identity.get(&Property::Email) { if !self .directory - .query(QueryBy::Id(account_id)) + .query(QueryBy::Id(account_id), false) .await .unwrap_or_default() .unwrap_or_default() diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index e590f9e9..b3b478e2 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -600,7 +600,7 @@ impl JMAP { access_token.quota as i64 } else { self.directory - .query(QueryBy::Id(account_id)) + .query(QueryBy::Id(account_id), false) .await .map_err(|err| { tracing::error!( diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index 2e5c8f49..4e8dfbc0 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -70,7 +70,7 @@ impl JMAP { // Obtain the principal let principal = if let Some(principal) = self .directory - .query(QueryBy::Id(id.document_id())) + .query(QueryBy::Id(id.document_id()), false) .await .map_err(|_| MethodError::ServerPartialFail)? { diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index 15933929..1946bbcb 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -49,7 +49,7 @@ impl JMAP { Filter::Name(name) => { if let Some(principal) = self .directory - .query(QueryBy::Name(name.as_str())) + .query(QueryBy::Name(name.as_str()), false) .await .map_err(|_| MethodError::ServerPartialFail)? { diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs index 19e47d2b..6e2aa61f 100644 --- a/crates/jmap/src/services/ingest.rs +++ b/crates/jmap/src/services/ingest.rs @@ -69,7 +69,7 @@ impl JMAP { .await } Ok(None) => { - let account_quota = match self.directory.query(QueryBy::Id(*uid)).await { + let account_quota = match self.directory.query(QueryBy::Id(*uid), false).await { Ok(Some(p)) => p.quota as i64, Ok(None) => 0, Err(_) => { diff --git a/crates/jmap/src/sieve/ingest.rs b/crates/jmap/src/sieve/ingest.rs index 4cd5de48..c81f0340 100644 --- a/crates/jmap/src/sieve/ingest.rs +++ b/crates/jmap/src/sieve/ingest.rs @@ -78,16 +78,17 @@ impl JMAP { let mut instance = self.sieve_runtime.filter_parsed(message); // Set account name and obtain quota - let (account_quota, mail_from) = match self.directory.query(QueryBy::Id(account_id)).await { - Ok(Some(p)) => { - instance.set_user_full_name(p.description().unwrap_or_else(|| p.name())); - (p.quota as i64, p.emails.into_iter().next()) - } - Ok(None) => (0, None), - Err(_) => { - return Err(IngestError::Temporary); - } - }; + let (account_quota, mail_from) = + match self.directory.query(QueryBy::Id(account_id), false).await { + Ok(Some(p)) => { + instance.set_user_full_name(p.description().unwrap_or_else(|| p.name())); + (p.quota as i64, p.emails.into_iter().next()) + } + Ok(None) => (0, None), + Err(_) => { + return Err(IngestError::Temporary); + } + }; // Set account address let mail_from = mail_from.unwrap_or_else(|| envelope_to.to_string()); diff --git a/crates/smtp/src/core/management.rs b/crates/smtp/src/core/management.rs index 4bb8a782..3580e280 100644 --- a/crates/smtp/src/core/management.rs +++ b/crates/smtp/src/core/management.rs @@ -255,10 +255,10 @@ impl SMTP { .queue .config .management_lookup - .query(QueryBy::Credentials(&Credentials::Plain { - username, - secret, - })) + .query( + QueryBy::Credentials(&Credentials::Plain { username, secret }), + false, + ) .await { Ok(Some(principal)) if principal.typ == Type::Superuser => { diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs index c3ceca0e..fbd9eb02 100644 --- a/crates/smtp/src/inbound/auth.rs +++ b/crates/smtp/src/inbound/auth.rs @@ -182,7 +182,7 @@ impl Session { | Credentials::OAuthBearer { token: username } => username.to_string(), }; if let Ok(is_authenticated) = lookup - .query(QueryBy::Credentials(&credentials)) + .query(QueryBy::Credentials(&credentials), false) .await .map(|r| r.is_some()) { diff --git a/crates/store/src/backend/mysql/write.rs b/crates/store/src/backend/mysql/write.rs index 461e3178..6e0dd52c 100644 --- a/crates/store/src/backend/mysql/write.rs +++ b/crates/store/src/backend/mysql/write.rs @@ -131,7 +131,8 @@ impl MysqlStore { let key = key.serialize(false); if let ValueOp::Set(value) = op { - let s = if let Some(exists) = asserted_values.get(&key) { + let exists = asserted_values.get(&key); + let s = if let Some(exists) = exists { if *exists { trx.prep(&format!("UPDATE {} SET v = :v WHERE k = :k", table)) .await? @@ -149,7 +150,7 @@ impl MysqlStore { match trx.exec_drop(&s, params! {"k" => key, "v" => value}).await { Ok(_) => { - if trx.affected_rows() == 0 { + if exists.is_some() && trx.affected_rows() == 0 { trx.rollback().await?; return Ok(false); } diff --git a/crates/store/src/write/key.rs b/crates/store/src/write/key.rs index 0b241def..74200496 100644 --- a/crates/store/src/write/key.rs +++ b/crates/store/src/write/key.rs @@ -280,6 +280,20 @@ impl + Sync + Send> Key for ValueKey { DirectoryClass::Principal(uid) => serializer.write(22u8).write_leb128(*uid), DirectoryClass::Domain(name) => serializer.write(23u8).write(name.as_slice()), DirectoryClass::UsedQuota(uid) => serializer.write(24u8).write_leb128(*uid), + DirectoryClass::MemberOf { + principal_id, + member_of, + } => serializer + .write(25u8) + .write(*principal_id) + .write(*member_of), + DirectoryClass::Members { + principal_id, + has_member, + } => serializer + .write(26u8) + .write(*principal_id) + .write(*has_member), }, } .finalize() @@ -411,6 +425,7 @@ impl ValueClass { | DirectoryClass::EmailToId(v) | DirectoryClass::Domain(v) => v.len(), DirectoryClass::Principal(_) | DirectoryClass::UsedQuota(_) => U32_LEN, + DirectoryClass::Members { .. } | DirectoryClass::MemberOf { .. } => U32_LEN * 2, }, ValueClass::Blob(op) => match op { BlobOp::Reserve { .. } => BLOB_HASH_LEN + U64_LEN + U32_LEN + 1, diff --git a/crates/store/src/write/mod.rs b/crates/store/src/write/mod.rs index 76c1d4cd..64517bf1 100644 --- a/crates/store/src/write/mod.rs +++ b/crates/store/src/write/mod.rs @@ -143,6 +143,8 @@ pub enum ValueClass { pub enum DirectoryClass { NameToId(Vec), EmailToId(Vec), + MemberOf { principal_id: u32, member_of: u32 }, + Members { principal_id: u32, has_member: u32 }, Domain(Vec), Principal(u32), UsedQuota(u32), diff --git a/tests/src/directory/imap.rs b/tests/src/directory/imap.rs index 36a8b4d8..c79980e0 100644 --- a/tests/src/directory/imap.rs +++ b/tests/src/directory/imap.rs @@ -79,7 +79,7 @@ async fn imap_directory() { assert_eq!( &LookupResult::from( handle - .query(QueryBy::Credentials(item.as_credentials())) + .query(QueryBy::Credentials(item.as_credentials()), true) .await .unwrap() .is_some() @@ -99,7 +99,7 @@ async fn imap_directory() { tokio::spawn(async move { LookupResult::from( handle - .query(QueryBy::Credentials(item.as_credentials())) + .query(QueryBy::Credentials(item.as_credentials()), true) .await .unwrap() .is_some(), diff --git a/tests/src/directory/internal.rs b/tests/src/directory/internal.rs index 33c87d03..73869ce1 100644 --- a/tests/src/directory/internal.rs +++ b/tests/src/directory/internal.rs @@ -158,10 +158,13 @@ async fn internal_directory() { assert_eq!(store.vrfy("jane").await.unwrap(), vec!["jane@example.org"]); assert_eq!( store - .query(QueryBy::Credentials(&Credentials::new( - "jane".to_string(), - "my_secret".to_string() - ))) + .query( + QueryBy::Credentials(&Credentials::new( + "jane".to_string(), + "my_secret".to_string() + )), + true + ) .await .unwrap(), Some(Principal { @@ -176,10 +179,13 @@ async fn internal_directory() { ); assert_eq!( store - .query(QueryBy::Credentials(&Credentials::new( - "jane".to_string(), - "wrong_password".to_string() - ))) + .query( + QueryBy::Credentials(&Credentials::new( + "jane".to_string(), + "wrong_password".to_string() + )), + true + ) .await .unwrap(), None @@ -208,12 +214,23 @@ async fn internal_directory() { name: "list".to_string(), typ: Type::List, emails: vec!["list@example.org".to_string()], - member_of: vec!["john".to_string(), "jane".to_string()], ..Default::default() }) .await, Ok(2) ); + assert_eq!( + store + .update_account( + QueryBy::Name("list"), + vec![PrincipalUpdate::set( + PrincipalField::Members, + PrincipalValue::StringList(vec!["john".to_string(), "jane".to_string()]), + ),], + ) + .await, + Ok(()) + ); assert!(store.rcpt("list@example.org").await.unwrap()); assert_eq!( store.email_to_ids("list@example.org").await.unwrap(), @@ -221,15 +238,15 @@ async fn internal_directory() { ); assert_eq!( store - .map_group_ids(store.query(QueryBy::Name("list")).await.unwrap().unwrap()) + .query(QueryBy::Name("list"), true) .await + .unwrap() .unwrap(), Principal { name: "list".to_string(), id: 2, typ: Type::List, emails: vec!["list@example.org".to_string()], - member_of: vec!["john".to_string(), "jane".to_string()], ..Default::default() } ); @@ -283,7 +300,13 @@ async fn internal_directory() { ); assert_eq!( store - .map_group_ids(store.query(QueryBy::Name("john")).await.unwrap().unwrap()) + .map_group_ids( + store + .query(QueryBy::Name("john"), true) + .await + .unwrap() + .unwrap() + ) .await .unwrap(), Principal { @@ -291,7 +314,11 @@ async fn internal_directory() { description: Some("John Doe".to_string()), secrets: vec!["secret".to_string(), "secret2".to_string()], emails: vec!["john@example.org".to_string()], - member_of: vec!["sales".to_string(), "support".to_string()], + member_of: vec![ + "list".to_string(), + "sales".to_string(), + "support".to_string() + ], ..Default::default() } ); @@ -327,7 +354,13 @@ async fn internal_directory() { ); assert_eq!( store - .map_group_ids(store.query(QueryBy::Name("john")).await.unwrap().unwrap()) + .map_group_ids( + store + .query(QueryBy::Name("john"), true) + .await + .unwrap() + .unwrap() + ) .await .unwrap(), Principal { @@ -335,7 +368,7 @@ async fn internal_directory() { description: Some("John Doe".to_string()), secrets: vec!["secret".to_string(), "secret2".to_string()], emails: vec!["john@example.org".to_string()], - member_of: vec!["sales".to_string()], + member_of: vec!["list".to_string(), "sales".to_string()], ..Default::default() } ); @@ -380,7 +413,7 @@ async fn internal_directory() { store .map_group_ids( store - .query(QueryBy::Name("john.doe")) + .query(QueryBy::Name("john.doe"), true) .await .unwrap() .unwrap() @@ -394,7 +427,7 @@ async fn internal_directory() { emails: vec!["john.doe@example.org".to_string()], quota: 1024, typ: Type::Superuser, - member_of: vec!["sales".to_string()], + member_of: vec!["list".to_string(), "sales".to_string()], ..Default::default() } ); @@ -408,7 +441,7 @@ async fn internal_directory() { .update_account( QueryBy::Name("list"), vec![PrincipalUpdate::remove_item( - PrincipalField::MemberOf, + PrincipalField::Members, PrincipalValue::String("john.doe".to_string()), )], ) @@ -416,25 +449,15 @@ async fn internal_directory() { Ok(()) ); assert_eq!( - store - .map_group_ids(store.query(QueryBy::Name("list")).await.unwrap().unwrap()) - .await - .unwrap(), - Principal { - name: "list".to_string(), - id: 2, - typ: Type::List, - emails: vec!["list@example.org".to_string()], - member_of: vec!["jane".to_string()], - ..Default::default() - } + store.email_to_ids("list@example.org").await.unwrap(), + vec![1] ); assert_eq!( store .update_account( QueryBy::Name("list"), vec![PrincipalUpdate::add_item( - PrincipalField::MemberOf, + PrincipalField::Members, PrincipalValue::String("john.doe".to_string()), )], ) @@ -442,18 +465,8 @@ async fn internal_directory() { Ok(()) ); assert_eq!( - store - .map_group_ids(store.query(QueryBy::Name("list")).await.unwrap().unwrap()) - .await - .unwrap(), - Principal { - name: "list".to_string(), - id: 2, - typ: Type::List, - emails: vec!["list@example.org".to_string()], - member_of: vec!["jane".to_string(), "john.doe".to_string()], - ..Default::default() - } + store.email_to_ids("list@example.org").await.unwrap(), + vec![0, 1] ); // Field validation diff --git a/tests/src/directory/ldap.rs b/tests/src/directory/ldap.rs index b2fa7c60..c606f1b4 100644 --- a/tests/src/directory/ldap.rs +++ b/tests/src/directory/ldap.rs @@ -46,10 +46,13 @@ async fn ldap_directory() { // Test authentication assert_eq!( handle - .query(QueryBy::Credentials(&Credentials::Plain { - username: "john".to_string(), - secret: "12345".to_string() - })) + .query( + QueryBy::Credentials(&Credentials::Plain { + username: "john".to_string(), + secret: "12345".to_string() + }), + true + ) .await .unwrap() .unwrap() @@ -71,10 +74,13 @@ async fn ldap_directory() { ); assert_eq!( handle - .query(QueryBy::Credentials(&Credentials::Plain { - username: "bill".to_string(), - secret: "password".to_string() - })) + .query( + QueryBy::Credentials(&Credentials::Plain { + username: "bill".to_string(), + secret: "password".to_string() + }), + true + ) .await .unwrap() .unwrap() @@ -94,10 +100,13 @@ async fn ldap_directory() { .into_sorted() ); assert!(handle - .query(QueryBy::Credentials(&Credentials::Plain { - username: "bill".to_string(), - secret: "invalid".to_string() - })) + .query( + QueryBy::Credentials(&Credentials::Plain { + username: "bill".to_string(), + secret: "invalid".to_string() + }), + true + ) .await .unwrap() .is_none()); @@ -105,7 +114,7 @@ async fn ldap_directory() { // Get user by name assert_eq!( handle - .query(QueryBy::Name("jane")) + .query(QueryBy::Name("jane"), true) .await .unwrap() .unwrap() @@ -125,7 +134,11 @@ async fn ldap_directory() { // Get group by name assert_eq!( - handle.query(QueryBy::Name("sales")).await.unwrap().unwrap(), + handle + .query(QueryBy::Name("sales"), true) + .await + .unwrap() + .unwrap(), Principal { id: base_store.get_account_id("sales").await.unwrap().unwrap(), name: "sales".to_string(), diff --git a/tests/src/directory/smtp.rs b/tests/src/directory/smtp.rs index 94ff054a..9aa38e5f 100644 --- a/tests/src/directory/smtp.rs +++ b/tests/src/directory/smtp.rs @@ -97,7 +97,7 @@ async fn smtp_directory() { let result: LookupResult = match item { Item::IsAccount(v) => handle.rcpt(v).await.unwrap().into(), Item::Authenticate(v) => handle - .query(QueryBy::Credentials(v)) + .query(QueryBy::Credentials(v), true) .await .unwrap() .is_some() @@ -129,7 +129,7 @@ async fn smtp_directory() { let result: LookupResult = match &item { Item::IsAccount(v) => handle.rcpt(v).await.unwrap().into(), Item::Authenticate(v) => handle - .query(QueryBy::Credentials(v)) + .query(QueryBy::Credentials(v), true) .await .unwrap() .is_some() diff --git a/tests/src/directory/sql.rs b/tests/src/directory/sql.rs index 4b81352c..340deb58 100644 --- a/tests/src/directory/sql.rs +++ b/tests/src/directory/sql.rs @@ -133,10 +133,13 @@ async fn sql_directory() { // Test authentication assert_eq!( handle - .query(QueryBy::Credentials(&Credentials::Plain { - username: "john".to_string(), - secret: "12345".to_string() - })) + .query( + QueryBy::Credentials(&Credentials::Plain { + username: "john".to_string(), + secret: "12345".to_string() + }), + true + ) .await .unwrap() .unwrap(), @@ -157,10 +160,13 @@ async fn sql_directory() { ); assert_eq!( handle - .query(QueryBy::Credentials(&Credentials::Plain { - username: "bill".to_string(), - secret: "password".to_string() - })) + .query( + QueryBy::Credentials(&Credentials::Plain { + username: "bill".to_string(), + secret: "password".to_string() + }), + true + ) .await .unwrap() .unwrap(), @@ -178,17 +184,24 @@ async fn sql_directory() { } ); assert!(handle - .query(QueryBy::Credentials(&Credentials::Plain { - username: "bill".to_string(), - secret: "invalid".to_string() - })) + .query( + QueryBy::Credentials(&Credentials::Plain { + username: "bill".to_string(), + secret: "invalid".to_string() + }), + true + ) .await .unwrap() .is_none()); // Get user by name assert_eq!( - handle.query(QueryBy::Name("jane")).await.unwrap().unwrap(), + handle + .query(QueryBy::Name("jane"), true) + .await + .unwrap() + .unwrap(), Principal { id: base_store.get_account_id("jane").await.unwrap().unwrap(), name: "jane".to_string(), @@ -203,7 +216,11 @@ async fn sql_directory() { // Get group by name assert_eq!( - handle.query(QueryBy::Name("sales")).await.unwrap().unwrap(), + handle + .query(QueryBy::Name("sales"), true) + .await + .unwrap() + .unwrap(), Principal { id: base_store.get_account_id("sales").await.unwrap().unwrap(), name: "sales".to_string(),