Internal directory + CLI improvements

This commit is contained in:
mdecimus
2023-12-20 20:09:39 +01:00
parent d4aca0a8e0
commit 91e8597998
34 changed files with 699 additions and 360 deletions

View File

@@ -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<Option<Principal<u32>>>;
async fn query(
&self,
by: QueryBy<'_>,
return_member_of: bool,
) -> crate::Result<Option<Principal<u32>>>;
async fn email_to_ids(&self, email: &str) -> crate::Result<Vec<u32>>;
async fn is_local_domain(&self, domain: &str) -> crate::Result<bool>;
@@ -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<Option<Principal<u32>>> {
let (username, secret) = match by {
QueryBy::Name(name) => (name, None),
QueryBy::Id(account_id) => {
return self
.get_value::<Principal<u32>>(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<Option<Principal<u32>>> {
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::<Principal<u32>>(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<Vec<u32>> {
self.get_value::<Vec<u32>>(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::<PrincipalIdType>(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<bool> {

View File

@@ -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<Option<u32>>;
async fn get_or_create_account_id(&self, name: &str) -> crate::Result<u32>;
async fn get_account_name(&self, account_id: u32) -> crate::Result<Option<String>>;
async fn get_member_of(&self, account_id: u32) -> crate::Result<Vec<u32>>;
async fn get_members(&self, account_id: u32) -> crate::Result<Vec<u32>>;
async fn create_account(&self, principal: Principal<String>) -> crate::Result<u32>;
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<Vec<u32>> {
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<Vec<u32>> {
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<Self> {
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,

View File

@@ -50,7 +50,6 @@ impl Serialize for &Principal<u32> {
+ self.name.len()
+ self.emails.iter().map(|s| s.len()).sum::<usize>()
+ self.secrets.iter().map(|s| s.len()).sum::<usize>()
+ 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<u32> {
}
}
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<Principal<u32>> {
})?,
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<String>),
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<Vec<String>> {
Some(list)
}
fn deserialize_u32_list(bytes: &mut Iter<'_, u8>) -> Option<Vec<u32>> {
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<Self> {
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),

View File

@@ -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<Option<Principal<u32>>> {
pub async fn query(
&self,
by: QueryBy<'_>,
return_member_of: bool,
) -> crate::Result<Option<Principal<u32>>> {
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()))
}
}

View File

@@ -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<Option<Principal<u32>>> {
pub async fn query(
&self,
by: QueryBy<'_>,
return_member_of: bool,
) -> crate::Result<Option<Principal<u32>>> {
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::<Rows>(

View File

@@ -26,11 +26,15 @@ use crate::{
};
impl Directory {
pub async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
pub async fn query(
&self,
by: QueryBy<'_>,
return_member_of: bool,
) -> crate::Result<Option<Principal<u32>>> {
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,