Per-domain directories: each domain signs in against its own directory (DIR-1 to DIR-15)
The two lookups every caller uses now honor Domain.directoryId, then the server default, then the internal directory, so sign-in, bearer routing, recipient lookup, discovery, the PACC record and the refusal of password changes on external accounts all follow the domain. A directoryId, or a server default, naming a directory that doesn't exist is unavailable, never the internal directory. A directory speaks only for the domains it serves: an account it returns on another directory's domain is refused, for sign-in and recipients alike, and aliases and group claims on such domains are dropped with a warning. A bearer token must belong to the user the client names, or the name must be one of its aliases with alias sign-in allowed. Accounts and groups that sync creates pass the tenant checks, limits included.
This commit is contained in:
Vendored
+54
-2
@@ -87,6 +87,7 @@ impl Server {
|
||||
for alias in account.email_aliases {
|
||||
if let Some((local, alias_domain)) = self.validate_alias(&alias).await?
|
||||
&& alias_domain.id_tenant == domain.id_tenant
|
||||
&& self.same_directory(&domain, &alias).await?
|
||||
&& self
|
||||
.rcpt_id_from_parts(local, alias_domain.id)
|
||||
.await?
|
||||
@@ -105,7 +106,9 @@ impl Server {
|
||||
let mut member_group_ids = Vec::with_capacity(groups.len());
|
||||
for email in groups {
|
||||
// inbuxa: SCIM-58: no group comes from a claim on a SCIM domain
|
||||
if self.is_scim_address(&email).await? {
|
||||
if self.is_scim_address(&email).await?
|
||||
|| !self.same_directory(&domain, &email).await?
|
||||
{
|
||||
continue;
|
||||
}
|
||||
member_group_ids.push(
|
||||
@@ -179,6 +182,7 @@ impl Server {
|
||||
for alias in account.email_aliases {
|
||||
if let Some((local, alias_domain)) = self.validate_alias(&alias).await?
|
||||
&& alias_domain.id_tenant == domain.id_tenant
|
||||
&& self.same_directory(&domain, &alias).await?
|
||||
&& self
|
||||
.rcpt_id_from_parts(local, alias_domain.id)
|
||||
.await?
|
||||
@@ -195,7 +199,9 @@ impl Server {
|
||||
let mut member_group_ids = Vec::new();
|
||||
for email in account.groups.unwrap_or_default() {
|
||||
// inbuxa: SCIM-58: no group comes from a claim on a SCIM domain
|
||||
if self.is_scim_address(&email).await? {
|
||||
if self.is_scim_address(&email).await?
|
||||
|| !self.same_directory(&domain, &email).await?
|
||||
{
|
||||
continue;
|
||||
}
|
||||
member_group_ids.push(
|
||||
@@ -228,6 +234,8 @@ impl Server {
|
||||
}));
|
||||
|
||||
|
||||
// inbuxa: DIR-15
|
||||
self.check_tenant_limits(&account).await?;
|
||||
match self
|
||||
.registry()
|
||||
.write(RegistryWrite::insert(&account))
|
||||
@@ -303,6 +311,7 @@ impl Server {
|
||||
for alias in group.email_aliases {
|
||||
if let Some((local, alias_domain)) = self.validate_alias(&alias).await?
|
||||
&& alias_domain.id_tenant == domain.id_tenant
|
||||
&& self.same_directory(&domain, &alias).await?
|
||||
&& self
|
||||
.rcpt_id_from_parts(local, alias_domain.id)
|
||||
.await?
|
||||
@@ -362,6 +371,7 @@ impl Server {
|
||||
for alias in group.email_aliases {
|
||||
if let Some((local, alias_domain)) = self.validate_alias(&alias).await?
|
||||
&& alias_domain.id_tenant == domain.id_tenant
|
||||
&& self.same_directory(&domain, &alias).await?
|
||||
&& self
|
||||
.rcpt_id_from_parts(local, alias_domain.id)
|
||||
.await?
|
||||
@@ -388,6 +398,8 @@ impl Server {
|
||||
}));
|
||||
|
||||
|
||||
// inbuxa: DIR-15
|
||||
self.check_tenant_limits(&account).await?;
|
||||
match self
|
||||
.registry()
|
||||
.write(RegistryWrite::insert(&account))
|
||||
@@ -413,6 +425,46 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
/// inbuxa: DIR-6: whether an address is on a domain served by the same
|
||||
/// directory as `domain`; a warning when it isn't.
|
||||
async fn same_directory(&self, domain: &DomainCache, address: &str) -> trc::Result<bool> {
|
||||
let Some((_, other)) = address.rsplit_once('@') else {
|
||||
return Ok(true);
|
||||
};
|
||||
let Some(other) = self.domain(other).await? else {
|
||||
return Ok(true);
|
||||
};
|
||||
let same = match (
|
||||
self.get_directory_for_cached_domain(domain),
|
||||
self.get_directory_for_cached_domain(&other),
|
||||
) {
|
||||
(None, None) => true,
|
||||
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
|
||||
_ => false,
|
||||
};
|
||||
if !same {
|
||||
trc::event!(
|
||||
Auth(trc::AuthEvent::Warning),
|
||||
AccountName = address.to_string(),
|
||||
Domain = other.name().to_string(),
|
||||
Reason = "Dropped: the address is on a domain served by another directory",
|
||||
);
|
||||
}
|
||||
Ok(same)
|
||||
}
|
||||
|
||||
/// inbuxa: DIR-15, MT-3, MT-17: an object created from a directory
|
||||
/// passes the same tenant checks as one created over JMAP.
|
||||
async fn check_tenant_limits(&self, object: &Object) -> trc::Result<()> {
|
||||
match inbuxa_features::tenancy::writes::check(self.registry(), None, None, object).await? {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(trc::AuthEvent::Failed
|
||||
.into_err()
|
||||
.details(err.description().unwrap_or("A tenant limit is reached").to_string())
|
||||
.reason("The directory's account can't be created")),
|
||||
}
|
||||
}
|
||||
|
||||
/// inbuxa: SCIM-58: whether an address is on a domain SCIM manages.
|
||||
async fn is_scim_address(&self, address: &str) -> trc::Result<bool> {
|
||||
Ok(match address.rsplit_once('@') {
|
||||
|
||||
Reference in New Issue
Block a user