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:
2026-09-19 10:32:57 -07:00
parent b080fa0736
commit f72e3bb85c
4 changed files with 173 additions and 13 deletions
+11
View File
@@ -106,6 +106,17 @@ impl Server {
Cow::Borrowed(rcpt)
};
match directory.recipient(address.as_ref()).await? {
// inbuxa: DIR-6: an answer for another directory's domain is no answer
Recipient::Account(account)
if self
.assert_directory_serves(directory, &account.email)
.await
.is_err() => {}
Recipient::Group(group)
if self
.assert_directory_serves(directory, &group.email)
.await
.is_err() => {}
Recipient::Account(account) => {
Box::pin(self.synchronize_account(account)).await?;
return Ok(if is_subaddressed {