/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use crate::directory::{DirectoryTest, IntoTestPrincipal, TestPrincipal}; use ahash::AHashSet; use directory::{ Permission, QueryBy, QueryParams, Type, backend::{ RcptType, internal::{ PrincipalField, PrincipalSet, PrincipalUpdate, PrincipalValue, lookup::DirectoryStore, manage::{self, ChangedPrincipals, ManageDirectory, UpdatePrincipal}, }, }, }; use mail_send::Credentials; use store::{ BitmapKey, Store, ValueKey, roaring::RoaringBitmap, write::{BatchBuilder, BitmapClass, ValueClass}, }; use types::collection::Collection; #[tokio::test] async fn internal_directory() { let config = DirectoryTest::new(None).await; for (store_id, store) in config.stores.stores { println!("Testing internal directory with store {:?}", store_id); store.destroy().await; // A principal without name should fail assert_eq!( store .create_principal(PrincipalSet::default(), None, None) .await, Err(manage::err_missing(PrincipalField::Name)) ); // Basic account creation let john_id = store .create_principal( TestPrincipal { name: "john".into(), description: Some("John Doe".into()), secrets: vec!["secret".into(), "secret2".into()], ..Default::default() } .into(), None, None, ) .await .unwrap() .id; // Two accounts with the same name should fail assert_eq!( store .create_principal( TestPrincipal { name: "john".into(), ..Default::default() } .into(), None, None ) .await, Err(manage::err_exists(PrincipalField::Name, "john")) ); // An account using a non-existent domain should fail assert_eq!( store .create_principal( TestPrincipal { name: "jane".into(), emails: vec!["jane@example.org".into()], ..Default::default() } .into(), None, None ) .await, Err(manage::not_found("example.org")) ); // Create a domain name store .create_principal( TestPrincipal { name: "example.org".into(), typ: Type::Domain, ..Default::default() } .into(), None, None, ) .await .unwrap(); assert!(store.is_local_domain("example.org").await.unwrap()); assert!(!store.is_local_domain("otherdomain.org").await.unwrap()); // Add an email address assert!( store .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![ PrincipalUpdate::add_item( PrincipalField::Emails, PrincipalValue::String("john@example.org".into()), ) ])) .await .is_ok() ); assert_eq!( store.rcpt("john@example.org").await.unwrap(), RcptType::Mailbox ); assert_eq!( store.email_to_id("john@example.org").await.unwrap(), Some(john_id) ); // Using non-existent domain should fail assert_eq!( store .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![ PrincipalUpdate::add_item( PrincipalField::Emails, PrincipalValue::String("john@otherdomain.org".into()), ) ])) .await, Err(manage::not_found("otherdomain.org")) ); // Create an account with an email address let jane_id = store .create_principal( TestPrincipal { name: "jane".into(), description: Some("Jane Doe".into()), secrets: vec!["my_secret".into(), "my_secret2".into()], emails: vec!["jane@example.org".into()], quota: 123, ..Default::default() } .into(), None, None, ) .await .unwrap() .id; assert_eq!( store.rcpt("jane@example.org").await.unwrap(), RcptType::Mailbox ); assert_eq!( store.rcpt("jane@otherdomain.org").await.unwrap(), RcptType::Invalid ); assert_eq!( store.email_to_id("jane@example.org").await.unwrap(), Some(jane_id) ); assert_eq!(store.vrfy("jane").await.unwrap(), vec!["jane@example.org"]); assert_eq!( store .query( QueryParams::credentials(&Credentials::new("jane".into(), "my_secret".into())) .with_return_member_of(true) ) .await .unwrap() .map(|p| p.into_test()), Some(TestPrincipal { id: jane_id, name: "jane".into(), description: Some("Jane Doe".into()), emails: vec!["jane@example.org".into()], secrets: vec!["my_secret".into(), "my_secret2".into()], quota: 123, ..Default::default() }) ); assert_eq!( store .query( QueryParams::credentials(&Credentials::new( "jane".into(), "wrong_password".into() )) .with_return_member_of(true) ) .await .unwrap(), None ); // Duplicate email address should fail assert_eq!( store .create_principal( TestPrincipal { name: "janeth".into(), description: Some("Janeth Doe".into()), emails: vec!["jane@example.org".into()], ..Default::default() } .into(), None, None ) .await, Err(manage::err_exists( PrincipalField::Emails, "jane@example.org" )) ); // Create a mailing list let list_id = store .create_principal( TestPrincipal { name: "list".into(), typ: Type::List, emails: vec!["list@example.org".into()], ..Default::default() } .into(), None, None, ) .await .unwrap() .id; assert!( store .update_principal(UpdatePrincipal::by_name("list").with_updates(vec![ PrincipalUpdate::set( PrincipalField::Members, PrincipalValue::StringList(vec!["john".into(), "jane".into()]), ), PrincipalUpdate::set( PrincipalField::ExternalMembers, PrincipalValue::StringList(vec![ "mike@other.org".into(), "lucy@foobar.net".into() ]), ) ])) .await .is_ok() ); assert_list_members( &store, "list@example.org", [ "john@example.org", "mike@other.org", "lucy@foobar.net", "jane@example.org", ], ) .await; assert_eq!( store .query(QueryParams::name("list").with_return_member_of(true)) .await .unwrap() .unwrap() .into_test(), TestPrincipal { name: "list".into(), id: list_id, typ: Type::List, emails: vec!["list@example.org".into()], ..Default::default() } ); assert_eq!( store .expn("list@example.org") .await .unwrap() .into_iter() .collect::>(), [ "john@example.org", "mike@other.org", "lucy@foobar.net", "jane@example.org" ] .into_iter() .map(|s| s.into()) .collect::>() ); // Create groups store .create_principal( TestPrincipal { name: "sales".into(), description: Some("Sales Team".into()), typ: Type::Group, ..Default::default() } .into(), None, None, ) .await .unwrap(); store .create_principal( TestPrincipal { name: "support".into(), description: Some("Support Team".into()), typ: Type::Group, ..Default::default() } .into(), None, None, ) .await .unwrap(); // Add John to the Sales and Support groups assert!( store .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![ PrincipalUpdate::add_item( PrincipalField::MemberOf, PrincipalValue::String("sales".into()), ), PrincipalUpdate::add_item( PrincipalField::MemberOf, PrincipalValue::String("support".into()), ) ])) .await .is_ok() ); let principal = store .query(QueryParams::name("john").with_return_member_of(true)) .await .unwrap() .unwrap(); let principal = store.map_principal(principal, &[]).await.unwrap(); assert_eq!( principal.into_test().into_sorted(), TestPrincipal { id: john_id, name: "john".into(), description: Some("John Doe".into()), secrets: vec!["secret".into(), "secret2".into()], emails: vec!["john@example.org".into()], member_of: vec!["sales".into(), "support".into()], lists: vec!["list".into()], ..Default::default() } ); // Adding a non-existent user should fail assert_eq!( store .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![ PrincipalUpdate::add_item( PrincipalField::MemberOf, PrincipalValue::String("accounting".into()), ) ])) .await, Err(manage::not_found("accounting")) ); // Remove a member from a group assert!( store .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![ PrincipalUpdate::remove_item( PrincipalField::MemberOf, PrincipalValue::String("support".into()), ) ])) .await .is_ok() ); let principal = store .query(QueryParams::name("john").with_return_member_of(true)) .await .unwrap() .unwrap(); let principal = store.map_principal(principal, &[]).await.unwrap(); assert_eq!( principal.into_test().into_sorted(), TestPrincipal { id: john_id, name: "john".into(), description: Some("John Doe".into()), secrets: vec!["secret".into(), "secret2".into()], emails: vec!["john@example.org".into()], member_of: vec!["sales".into()], lists: vec!["list".into()], ..Default::default() } ); // Update multiple fields assert!( store .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![ PrincipalUpdate::set( PrincipalField::Name, PrincipalValue::String("john.doe".into()) ), PrincipalUpdate::set( PrincipalField::Description, PrincipalValue::String("Johnny Doe".into()) ), PrincipalUpdate::set( PrincipalField::Secrets, PrincipalValue::StringList(vec!["12345".into()]) ), PrincipalUpdate::set(PrincipalField::Quota, PrincipalValue::Integer(1024)), PrincipalUpdate::remove_item( PrincipalField::Emails, PrincipalValue::String("john@example.org".into()), ), PrincipalUpdate::add_item( PrincipalField::Emails, PrincipalValue::String("john.doe@example.org".into()), ) ])) .await .is_ok() ); let principal = store .query(QueryParams::name("john.doe").with_return_member_of(true)) .await .unwrap() .unwrap(); let principal = store.map_principal(principal, &[]).await.unwrap(); assert_eq!( principal.into_test().into_sorted(), TestPrincipal { id: john_id, name: "john.doe".into(), description: Some("Johnny Doe".into()), secrets: vec!["12345".into()], emails: vec!["john.doe@example.org".into()], quota: 1024, typ: Type::Individual, member_of: vec!["sales".into()], lists: vec!["list".into()], ..Default::default() } ); assert_eq!(store.get_principal_id("john").await.unwrap(), None); assert_eq!( store.rcpt("john@example.org").await.unwrap(), RcptType::Invalid ); assert_eq!( store.rcpt("john.doe@example.org").await.unwrap(), RcptType::Mailbox ); // Remove a member from a mailing list and then add it back assert!( store .update_principal(UpdatePrincipal::by_name("list").with_updates(vec![ PrincipalUpdate::remove_item( PrincipalField::Members, PrincipalValue::String("john.doe".into()), ) ])) .await .is_ok() ); assert_list_members( &store, "list@example.org", ["jane@example.org", "mike@other.org", "lucy@foobar.net"], ) .await; assert!( store .update_principal(UpdatePrincipal::by_name("list").with_updates(vec![ PrincipalUpdate::add_item( PrincipalField::Members, PrincipalValue::String("john.doe".into()), ) ])) .await .is_ok() ); assert_list_members( &store, "list@example.org", [ "john.doe@example.org", "jane@example.org", "mike@other.org", "lucy@foobar.net", ], ) .await; // Field validation assert_eq!( store .update_principal(UpdatePrincipal::by_name("john.doe").with_updates(vec![ PrincipalUpdate::set( PrincipalField::Name, PrincipalValue::String("jane".into()) ), ])) .await, Err(manage::err_exists(PrincipalField::Name, "jane")) ); assert_eq!( store .update_principal(UpdatePrincipal::by_name("john.doe").with_updates(vec![ PrincipalUpdate::add_item( PrincipalField::Emails, PrincipalValue::String("jane@example.org".into()) ), ])) .await, Err(manage::err_exists( PrincipalField::Emails, "jane@example.org" )) ); // List accounts assert_eq!( store .list_principals( None, None, &[Type::Individual, Type::Group, Type::List], true, 0, 0 ) .await .unwrap() .items .into_iter() .map(|p| p.name) .collect::>(), ["jane", "john.doe", "list", "sales", "support"] .into_iter() .map(|s| s.into()) .collect::>() ); assert_eq!( store .list_principals("john".into(), None, &[], true, 0, 0) .await .unwrap() .items .into_iter() .map(|p| p.name) .collect::>(), vec!["john.doe"] ); assert_eq!( store .list_principals(None, None, &[Type::Individual], true, 0, 0) .await .unwrap() .items .into_iter() .map(|p| p.name) .collect::>(), ["jane", "john.doe"] .into_iter() .map(|s| s.into()) .collect::>() ); assert_eq!( store .list_principals(None, None, &[Type::Group], true, 0, 0) .await .unwrap() .items .into_iter() .map(|p| p.name) .collect::>(), ["sales", "support"] .into_iter() .map(|s| s.into()) .collect::>() ); assert_eq!( store .list_principals(None, None, &[Type::List], true, 0, 0) .await .unwrap() .items .into_iter() .map(|p| p.name) .collect::>(), vec!["list"] ); assert_eq!( store .list_principals("example.org".into(), None, &[], true, 0, 0) .await .unwrap() .items .into_iter() .map(|p| p.name) .collect::>(), vec!["example.org", "jane", "john.doe", "list"] ); assert_eq!( store .list_principals("johnny doe".into(), None, &[], true, 0, 0) .await .unwrap() .items .into_iter() .map(|p| p.name) .collect::>(), vec!["john.doe"] ); // Write records on John's and Jane's accounts let mut document_id = u32::MAX; for account_id in [john_id, jane_id] { document_id = store .assign_document_ids(u32::MAX, Collection::Principal, 1) .await .unwrap(); store .write( BatchBuilder::new() .with_account_id(account_id) .with_collection(Collection::Email) .create_document(document_id) .set(ValueClass::Property(0), "hello".as_bytes()) .build_all(), ) .await .unwrap(); assert_eq!( store .get_value::(ValueKey { account_id, collection: Collection::Email.into(), document_id, class: ValueClass::Property(0) }) .await .unwrap(), Some("hello".into()) ); } // Delete John's account and make sure his records are gone store.delete_principal(QueryBy::Id(john_id)).await.unwrap(); assert_eq!(store.get_principal_id("john.doe").await.unwrap(), None); assert_eq!( store.email_to_id("john.doe@example.org").await.unwrap(), None ); assert_eq!( store.rcpt("john.doe@example.org").await.unwrap(), RcptType::Invalid ); assert_eq!( store .list_principals( None, None, &[Type::Individual, Type::Group, Type::List], true, 0, 0 ) .await .unwrap() .items .into_iter() .map(|p| p.name) .collect::>(), ["jane", "list", "sales", "support"] .into_iter() .map(|s| s.into()) .collect::>() ); assert_eq!( store .get_bitmap(BitmapKey { account_id: john_id, collection: Collection::Email.into(), class: BitmapClass::DocumentIds, document_id: 0 }) .await .unwrap(), None ); assert_eq!( store .get_value::(ValueKey { account_id: john_id, collection: Collection::Email.into(), document_id: 0, class: ValueClass::Property(0) }) .await .unwrap(), None ); // Make sure Jane's records are still there assert_eq!(store.get_principal_id("jane").await.unwrap(), Some(jane_id)); assert_eq!( store.email_to_id("jane@example.org").await.unwrap(), Some(jane_id) ); assert_eq!( store.rcpt("jane@example.org").await.unwrap(), RcptType::Mailbox ); assert_eq!( store .get_bitmap(BitmapKey { account_id: jane_id, collection: Collection::Email.into(), class: BitmapClass::DocumentIds, document_id: 0 }) .await .unwrap(), Some(RoaringBitmap::from_sorted_iter([document_id]).unwrap()) ); assert_eq!( store .get_value::(ValueKey { account_id: jane_id, collection: Collection::Email.into(), document_id, class: ValueClass::Property(0) }) .await .unwrap(), Some("hello".into()) ); } } #[allow(async_fn_in_trait)] pub trait TestInternalDirectory { async fn create_test_user(&self, login: &str, secret: &str, name: &str, emails: &[&str]) -> u32; async fn create_test_group(&self, login: &str, name: &str, emails: &[&str]) -> u32; async fn create_test_list(&self, login: &str, name: &str, emails: &[&str]) -> u32; async fn set_test_quota(&self, login: &str, quota: u32); async fn add_permissions(&self, login: &str, permissions: impl IntoIterator); async fn remove_permissions( &self, login: &str, permissions: impl IntoIterator, ); async fn add_to_group(&self, login: &str, group: &str) -> ChangedPrincipals; async fn remove_from_group(&self, login: &str, group: &str) -> ChangedPrincipals; async fn remove_test_alias(&self, login: &str, alias: &str); async fn create_test_domains(&self, domains: &[&str]); } impl TestInternalDirectory for Store { async fn create_test_user( &self, login: &str, secret: &str, name: &str, emails: &[&str], ) -> u32 { let role = if login == "admin" { "admin" } else { "user" }; self.create_test_domains(emails).await; if let Some(principal) = self .query(QueryParams::name(login).with_return_member_of(false)) .await .unwrap() { self.update_principal(UpdatePrincipal::by_id(principal.id()).with_updates(vec![ PrincipalUpdate::set( PrincipalField::Secrets, PrincipalValue::StringList(vec![secret.into()]), ), PrincipalUpdate::set( PrincipalField::Description, PrincipalValue::String(name.into()), ), PrincipalUpdate::set( PrincipalField::Emails, PrincipalValue::StringList(emails.iter().map(|s| (*s).into()).collect()), ), PrincipalUpdate::add_item( PrincipalField::Roles, PrincipalValue::String(role.into()), ), PrincipalUpdate::add_item( PrincipalField::EnabledPermissions, PrincipalValue::String(Permission::UnlimitedRequests.name().into()), ), ])) .await .unwrap(); principal.id() } else { self.create_principal( PrincipalSet::new(0, Type::Individual) .with_field(PrincipalField::Name, login) .with_field(PrincipalField::Description, name) .with_field( PrincipalField::Secrets, PrincipalValue::StringList(vec![secret.into()]), ) .with_field( PrincipalField::Emails, PrincipalValue::StringList(emails.iter().map(|s| (*s).into()).collect()), ) .with_field( PrincipalField::Roles, PrincipalValue::StringList(vec![role.into()]), ) .with_field( PrincipalField::EnabledPermissions, PrincipalValue::StringList(vec![ Permission::UnlimitedRequests.name().into(), ]), ), None, None, ) .await .unwrap() .id } } async fn create_test_group(&self, login: &str, name: &str, emails: &[&str]) -> u32 { self.create_test_domains(emails).await; if let Some(principal) = self .query(QueryParams::name(login).with_return_member_of(false)) .await .unwrap() { principal.id() } else { self.create_principal( PrincipalSet::new(0, Type::Group) .with_field(PrincipalField::Name, login) .with_field(PrincipalField::Description, name) .with_field( PrincipalField::Emails, PrincipalValue::StringList(emails.iter().map(|s| (*s).into()).collect()), ) .with_field( PrincipalField::Roles, PrincipalValue::StringList(vec!["user".into()]), ), None, None, ) .await .unwrap() .id } } async fn create_test_list(&self, login: &str, name: &str, members: &[&str]) -> u32 { if let Some(principal) = self .query(QueryParams::name(login).with_return_member_of(false)) .await .unwrap() { principal.id() } else { self.create_test_domains(&[login]).await; self.create_principal( PrincipalSet::new(0, Type::List) .with_field(PrincipalField::Name, login) .with_field(PrincipalField::Description, name) .with_field( PrincipalField::Members, PrincipalValue::StringList(members.iter().map(|s| (*s).into()).collect()), ) .with_field( PrincipalField::Emails, PrincipalValue::StringList(vec![login.into()]), ), None, None, ) .await .unwrap() .id } } async fn set_test_quota(&self, login: &str, quota: u32) { self.update_principal(UpdatePrincipal::by_name(login).with_updates(vec![ PrincipalUpdate::set(PrincipalField::Quota, PrincipalValue::Integer(quota as u64)), ])) .await .unwrap(); } async fn add_permissions( &self, login: &str, permissions: impl IntoIterator, ) { self.update_principal( UpdatePrincipal::by_name(login).with_updates( permissions .into_iter() .map(|p| { PrincipalUpdate::add_item( PrincipalField::EnabledPermissions, PrincipalValue::String(p.name().to_string()), ) }) .collect(), ), ) .await .unwrap(); } async fn remove_permissions( &self, login: &str, permissions: impl IntoIterator, ) { self.update_principal( UpdatePrincipal::by_name(login).with_updates( permissions .into_iter() .map(|p| { PrincipalUpdate::remove_item( PrincipalField::EnabledPermissions, PrincipalValue::String(p.name().to_string()), ) }) .collect(), ), ) .await .unwrap(); } async fn add_to_group(&self, login: &str, group: &str) -> ChangedPrincipals { self.update_principal(UpdatePrincipal::by_name(login).with_updates(vec![ PrincipalUpdate::add_item( PrincipalField::MemberOf, PrincipalValue::String(group.into()), ), ])) .await .unwrap() } async fn remove_from_group(&self, login: &str, group: &str) -> ChangedPrincipals { self.update_principal(UpdatePrincipal::by_name(login).with_updates(vec![ PrincipalUpdate::remove_item( PrincipalField::MemberOf, PrincipalValue::String(group.into()), ), ])) .await .unwrap() } async fn remove_test_alias(&self, login: &str, alias: &str) { self.update_principal(UpdatePrincipal::by_name(login).with_updates(vec![ PrincipalUpdate::remove_item( PrincipalField::Emails, PrincipalValue::String(alias.into()), ), ])) .await .unwrap(); } async fn create_test_domains(&self, domains: &[&str]) { for domain in domains { let domain = domain.rsplit_once('@').map_or(*domain, |(_, d)| d); if self .query(QueryParams::name(domain).with_return_member_of(false)) .await .unwrap() .is_none() { self.create_principal( PrincipalSet::new(0, Type::Domain).with_field(PrincipalField::Name, domain), None, None, ) .await .unwrap(); } } } } async fn assert_list_members( store: &Store, list_addr: &str, members: impl IntoIterator, ) { match store.rcpt(list_addr).await.unwrap() { RcptType::List(items) => { assert_eq!( items.into_iter().collect::>(), members .into_iter() .map(|s| s.into()) .collect::>() ); } other => panic!("invalid {other:?}"), } }