config: make alias sources Store authoritative
Debian Trixie package bundle / packages (push) Successful in 22m14s
Debian Trixie package bundle / packages (push) Successful in 22m14s
This commit is contained in:
+11
-2
@@ -59,8 +59,17 @@ way.
|
||||
|
||||
Aliases and virtual domains are split into suffixless JSON fragments below
|
||||
`/etc/bongo/aliases.d`. Every fragment may contain `aliases`, `domains`, or
|
||||
both. The required `default_config` supplies global aliases; a full source
|
||||
address overrides one domain only:
|
||||
both. Their complete source set is stored operatively in the internal
|
||||
`alias-fragments` Store document; the directory is its editable mirror.
|
||||
Alias changes made by an administrative command update the Store and mirror
|
||||
as one transaction. At manager startup, only a strictly newer file or
|
||||
directory timestamp is treated as an emergency edit. An equally dated or
|
||||
newer Store source refreshes the directory, including additions and
|
||||
deletions. `bongo-admin config sync` explicitly selects a validated direct
|
||||
file edit even within the same timestamp second.
|
||||
|
||||
The required `default_config` supplies global aliases; a full source address
|
||||
overrides one domain only:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -92,9 +92,11 @@ configured `bongo` service account before accepting either HTTP listener.
|
||||
Agent settings are operative suffixless JSON Store documents such as `smtp`,
|
||||
`imap`, `queue`, `web`, `authentication`, and `manager`. Their declarative
|
||||
counterparts live below `/etc/bongo/config.d`; alias fragments live below
|
||||
`/etc/bongo/aliases.d`. The manager validates and imports the files only after
|
||||
the Store starts, so agents always consume one coherent Store-facing model
|
||||
and never fall back to a second runtime configuration source.
|
||||
`/etc/bongo/aliases.d` and are represented operatively by one internal
|
||||
`alias-fragments` Store source document. The manager reconciles the mirrors
|
||||
only after the Store starts, compiles the authoritative alias source into its
|
||||
per-domain delivery documents, and never lets an equally dated file override
|
||||
the Store.
|
||||
|
||||
`bongo-admin config` reads the Store and writes Store plus files as one
|
||||
rollback-capable operation. Use `bongo-admin config check` for a read-only
|
||||
|
||||
@@ -10,7 +10,7 @@ configuration under `/etc`, not `/usr/etc`.
|
||||
| --- | --- |
|
||||
| `/etc/bongo` | Administrator-managed configuration and protected key directories. |
|
||||
| `/etc/bongo/config.d` | Suffixless JSON mirrors for every operative Store configuration document. |
|
||||
| `/etc/bongo/aliases.d` | Mergeable suffixless alias and virtual-domain JSON fragments. |
|
||||
| `/etc/bongo/aliases.d` | Editable mirror of the Store-authoritative, mergeable suffixless alias and virtual-domain fragments. |
|
||||
| `/etc/bongo/ssl.d` | TLS certificates and private keys managed by setup or an ACME deploy hook. |
|
||||
| `/etc/bongo/dkim.d` | Per-domain DKIM keys and related material. |
|
||||
| `/etc/bongo/dmarc.d` | DMARC setup state and report-processing policy. |
|
||||
|
||||
@@ -101,7 +101,9 @@ admin changes, and rolls back prior writes if a later write fails.
|
||||
`bongo-admin config sync` transactionally writes validated file changes and
|
||||
new template defaults to the Store, then asks the running manager to reload.
|
||||
Alias and virtual-domain fragments are kept separately in
|
||||
`/etc/bongo/aliases.d` and are synchronized by the same manager operation.
|
||||
`/etc/bongo/aliases.d`. Their operative `alias-fragments` Store source remains
|
||||
authoritative; the same manager operation imports an explicitly newer or
|
||||
forced emergency file edit and otherwise refreshes the fragment mirror.
|
||||
|
||||
Configuration names such as `smtp`, `imap`, `antispam`, `antivirus`, `web`,
|
||||
and `authentication` have no `.json` suffix, although their contents are
|
||||
|
||||
@@ -85,6 +85,8 @@ ValidConfigurationName(const char *name)
|
||||
if (!strcmp(name, *cursor))
|
||||
return 1;
|
||||
}
|
||||
if (!strcmp(name, "alias-fragments"))
|
||||
return 1;
|
||||
if (!strncmp(name, "aliases/", 8)) {
|
||||
const unsigned char *alias = (const unsigned char *)name + 8;
|
||||
size_t length = strlen((const char *)alias);
|
||||
@@ -372,6 +374,7 @@ AccessConfiguration(const char *name, int replace)
|
||||
char *original = NULL;
|
||||
size_t length = 0;
|
||||
uint64_t modified = 0;
|
||||
int mirror;
|
||||
int conn_started = 0;
|
||||
int msg_started = 0;
|
||||
int result = 1;
|
||||
@@ -381,6 +384,7 @@ AccessConfiguration(const char *name, int replace)
|
||||
name ? name : "(null)");
|
||||
return 2;
|
||||
}
|
||||
mirror = !strchr(name, '/') && strcmp(name, "alias-fragments");
|
||||
if (replace && ReadConfigurationInput(&content, &length) != 0)
|
||||
return 1;
|
||||
if (replace && (BongoJsonParseString(content, &root) != BONGO_JSON_OK ||
|
||||
@@ -402,7 +406,7 @@ AccessConfiguration(const char *name, int replace)
|
||||
goto done;
|
||||
}
|
||||
if (replace) {
|
||||
if (!strchr(name, '/') &&
|
||||
if (mirror &&
|
||||
(!NMAPReadConfigFile(name, &original) || !original)) {
|
||||
fprintf(stderr,
|
||||
"bongo-admin: cannot snapshot configuration %s\n", name);
|
||||
@@ -413,7 +417,7 @@ AccessConfiguration(const char *name, int replace)
|
||||
name);
|
||||
goto done;
|
||||
}
|
||||
if (!strchr(name, '/') &&
|
||||
if (mirror &&
|
||||
(!NMAPReadConfigFileModified(name, &modified) ||
|
||||
!WriteConfigurationMirror(name, content, length, modified))) {
|
||||
if (!original ||
|
||||
|
||||
@@ -75,7 +75,7 @@ class ConfigurationModelTest(unittest.TestCase):
|
||||
source = (SOURCE / "src" / "apps" / "manager" / "manager.c").read_text(
|
||||
encoding="utf-8")
|
||||
match = re.search(
|
||||
r"SyncLocalConfiguration\(\).*?"
|
||||
r"SyncLocalConfiguration\(BOOL forceLocal\).*?"
|
||||
r"static const char \*const names\[\] = \{(.*?)NULL",
|
||||
source, re.DOTALL)
|
||||
self.assertIsNotNone(match)
|
||||
|
||||
@@ -59,6 +59,8 @@ class FakeStore(ConfigurationStore):
|
||||
}
|
||||
self.domain_aliases = {}
|
||||
self.username_mappings = {}
|
||||
self.alias_fragments = {}
|
||||
self.fail_alias_fragments = False
|
||||
self.fail_name = None
|
||||
|
||||
def read_store_document(self, name):
|
||||
@@ -84,6 +86,19 @@ class FakeStore(ConfigurationStore):
|
||||
return value, json_bytes(value)
|
||||
return self.read_store_document(name)
|
||||
|
||||
def _helper(self, operation, name, content=None):
|
||||
if name != "alias-fragments":
|
||||
return super()._helper(operation, name, content)
|
||||
if operation == "__config-replace":
|
||||
if self.fail_alias_fragments:
|
||||
self.fail_alias_fragments = False
|
||||
raise ConfigurationError("injected alias Store failure")
|
||||
self.alias_fragments = json.loads(content)
|
||||
return b""
|
||||
if operation == "__config-read":
|
||||
return json_bytes(self.alias_fragments)
|
||||
raise ConfigurationError("unsupported fake alias helper operation")
|
||||
|
||||
|
||||
class FakeSocket:
|
||||
def __init__(self, response):
|
||||
@@ -450,6 +465,9 @@ class ConfigurationStorageTest(unittest.TestCase):
|
||||
aliases = json.loads(
|
||||
(self.paths.aliases_dir / "account-renames").read_text())
|
||||
self.assertEqual(aliases["aliases"]["old-user"], "new-user")
|
||||
self.assertEqual(
|
||||
self.store.alias_fragments["account-renames"]["aliases"]
|
||||
["old-user"], "new-user")
|
||||
self.store.finish_retained_alias("101", True)
|
||||
self.assertFalse(
|
||||
(self.paths.lock.parent / "bongo-alias-rename-101").exists())
|
||||
@@ -465,6 +483,21 @@ class ConfigurationStorageTest(unittest.TestCase):
|
||||
self.store.prepare_retained_alias("old-user", "new-user", "102")
|
||||
self.store.finish_retained_alias("102", False)
|
||||
self.assertFalse((self.paths.aliases_dir / "account-renames").exists())
|
||||
self.assertNotIn("account-renames", self.store.alias_fragments)
|
||||
|
||||
def test_retained_alias_store_failure_restores_file_and_transaction(self):
|
||||
self.paths.aliases_dir.mkdir(parents=True)
|
||||
(self.paths.aliases_dir / "default_config").write_bytes(json_bytes({
|
||||
"aliases": {"postmaster": "admin"},
|
||||
}))
|
||||
self.store.fail_alias_fragments = True
|
||||
with self.assertRaisesRegex(ConfigurationError,
|
||||
"injected alias Store failure"):
|
||||
self.store.prepare_retained_alias(
|
||||
"old-user", "new-user", "105")
|
||||
self.assertFalse((self.paths.aliases_dir / "account-renames").exists())
|
||||
self.assertFalse(
|
||||
(self.paths.lock.parent / "bongo-alias-rename-105").exists())
|
||||
|
||||
def test_retained_alias_prepare_rejects_conflicting_mapping(self):
|
||||
self.paths.aliases_dir.mkdir(parents=True)
|
||||
|
||||
+338
-7
@@ -801,7 +801,8 @@ done:
|
||||
}
|
||||
|
||||
static BOOL
|
||||
SyncConfigurationDocument(const char *storeName, const char *path)
|
||||
SyncConfigurationDocument(const char *storeName, const char *path,
|
||||
BOOL forceLocal)
|
||||
{
|
||||
char *local = NULL;
|
||||
char *stored = NULL;
|
||||
@@ -834,7 +835,7 @@ SyncConfigurationDocument(const char *storeName, const char *path)
|
||||
result = SetConfigurationFileTime(path, &details, storeModified);
|
||||
goto done;
|
||||
}
|
||||
if ((uint64_t)details.st_mtime > storeModified) {
|
||||
if (forceLocal || (uint64_t)details.st_mtime > storeModified) {
|
||||
if (!SyncConfigurationContent(storeName, path, local) ||
|
||||
!NMAPReadConfigFileModified(storeName, &storeModified))
|
||||
goto done;
|
||||
@@ -878,6 +879,267 @@ ValidAliasConfigurationName(const char *name)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
ReadAliasSourceDirectory(BongoJsonNode **rootOut, char **contentOut,
|
||||
uint64_t *modifiedOut)
|
||||
{
|
||||
DIR *directory = NULL;
|
||||
struct dirent *entry;
|
||||
struct stat directoryDetails;
|
||||
BongoJsonObject *fragments = NULL;
|
||||
BongoJsonNode *aggregate = NULL;
|
||||
char *content = NULL;
|
||||
uint64_t modified;
|
||||
size_t total = 0;
|
||||
BOOL foundDefault = FALSE;
|
||||
BOOL result = FALSE;
|
||||
|
||||
if (lstat(XPL_DEFAULT_ALIASES_DIR, &directoryDetails) != 0 ||
|
||||
!S_ISDIR(directoryDetails.st_mode)) {
|
||||
fprintf(stderr, _("bongo-manager: invalid alias directory %s\n"),
|
||||
XPL_DEFAULT_ALIASES_DIR);
|
||||
goto done;
|
||||
}
|
||||
modified = (uint64_t)directoryDetails.st_mtime;
|
||||
fragments = BongoJsonObjectNew();
|
||||
if (!fragments)
|
||||
goto done;
|
||||
directory = opendir(XPL_DEFAULT_ALIASES_DIR);
|
||||
if (!directory)
|
||||
goto done;
|
||||
while ((entry = readdir(directory)) != NULL) {
|
||||
BongoJsonNode *fragment = NULL;
|
||||
struct stat details;
|
||||
char path[XPL_MAX_PATH + 1];
|
||||
char *fragmentContent = NULL;
|
||||
size_t fragmentLength;
|
||||
|
||||
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
|
||||
continue;
|
||||
if (!ValidAliasConfigurationName(entry->d_name) ||
|
||||
snprintf(path, sizeof(path), "%s/%s", XPL_DEFAULT_ALIASES_DIR,
|
||||
entry->d_name) >= (int)sizeof(path) ||
|
||||
!ReadConfigurationFile(path, &fragmentContent, &details) ||
|
||||
BongoJsonParseString(fragmentContent, &fragment) != BONGO_JSON_OK ||
|
||||
!fragment || fragment->type != BONGO_JSON_OBJECT) {
|
||||
fprintf(stderr,
|
||||
_("bongo-manager: invalid alias source fragment %s/%s\n"),
|
||||
XPL_DEFAULT_ALIASES_DIR, entry->d_name);
|
||||
if (fragment)
|
||||
BongoJsonNodeFree(fragment);
|
||||
if (fragmentContent)
|
||||
MemFree(fragmentContent);
|
||||
goto done;
|
||||
}
|
||||
fragmentLength = strlen(fragmentContent);
|
||||
if (fragmentLength > MAX_LOCAL_CONFIG_SIZE - total) {
|
||||
fprintf(stderr,
|
||||
_("bongo-manager: alias source exceeds %u bytes\n"),
|
||||
MAX_LOCAL_CONFIG_SIZE);
|
||||
BongoJsonNodeFree(fragment);
|
||||
MemFree(fragmentContent);
|
||||
goto done;
|
||||
}
|
||||
total += fragmentLength;
|
||||
MemFree(fragmentContent);
|
||||
if ((uint64_t)details.st_mtime > modified)
|
||||
modified = (uint64_t)details.st_mtime;
|
||||
if (!strcmp(entry->d_name, "default_config"))
|
||||
foundDefault = TRUE;
|
||||
if (BongoJsonObjectPut(fragments, entry->d_name, fragment) !=
|
||||
BONGO_JSON_OK) {
|
||||
BongoJsonNodeFree(fragment);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
if (!foundDefault) {
|
||||
fprintf(stderr, _("bongo-manager: missing %s/default_config\n"),
|
||||
XPL_DEFAULT_ALIASES_DIR);
|
||||
goto done;
|
||||
}
|
||||
aggregate = BongoJsonNodeNewObject(fragments);
|
||||
if (!aggregate)
|
||||
goto done;
|
||||
fragments = NULL;
|
||||
content = BongoJsonNodeToString(aggregate);
|
||||
if (!content)
|
||||
goto done;
|
||||
*rootOut = aggregate;
|
||||
*contentOut = content;
|
||||
*modifiedOut = modified;
|
||||
aggregate = NULL;
|
||||
content = NULL;
|
||||
result = TRUE;
|
||||
|
||||
done:
|
||||
if (directory)
|
||||
closedir(directory);
|
||||
if (aggregate)
|
||||
BongoJsonNodeFree(aggregate);
|
||||
else if (fragments)
|
||||
BongoJsonObjectFree(fragments);
|
||||
if (content)
|
||||
MemFree(content);
|
||||
return result;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
SetAliasSourceTimes(BongoJsonNode *root, uint64_t modified)
|
||||
{
|
||||
BongoJsonObjectIter iterator;
|
||||
BongoJsonObject *fragments;
|
||||
struct stat details;
|
||||
char path[XPL_MAX_PATH + 1];
|
||||
|
||||
if (!root || root->type != BONGO_JSON_OBJECT)
|
||||
return FALSE;
|
||||
fragments = BongoJsonNodeAsObject(root);
|
||||
for (BongoJsonObjectIterFirst(fragments, &iterator); iterator.key;
|
||||
BongoJsonObjectIterNext(fragments, &iterator)) {
|
||||
if (snprintf(path, sizeof(path), "%s/%s", XPL_DEFAULT_ALIASES_DIR,
|
||||
iterator.key) >= (int)sizeof(path) ||
|
||||
lstat(path, &details) != 0 || !S_ISREG(details.st_mode) ||
|
||||
!SetConfigurationFileTime(path, &details, modified))
|
||||
return FALSE;
|
||||
}
|
||||
if (lstat(XPL_DEFAULT_ALIASES_DIR, &details) != 0 ||
|
||||
!S_ISDIR(details.st_mode) ||
|
||||
!SetConfigurationFileTime(XPL_DEFAULT_ALIASES_DIR, &details, modified))
|
||||
return FALSE;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
ValidateAliasSourceRoot(BongoJsonNode *root)
|
||||
{
|
||||
BongoJsonObjectIter iterator;
|
||||
BongoJsonObject *fragments;
|
||||
BOOL foundDefault = FALSE;
|
||||
|
||||
if (!root || root->type != BONGO_JSON_OBJECT)
|
||||
return FALSE;
|
||||
fragments = BongoJsonNodeAsObject(root);
|
||||
for (BongoJsonObjectIterFirst(fragments, &iterator); iterator.key;
|
||||
BongoJsonObjectIterNext(fragments, &iterator)) {
|
||||
if (!ValidAliasConfigurationName(iterator.key) || !iterator.value ||
|
||||
iterator.value->type != BONGO_JSON_OBJECT)
|
||||
return FALSE;
|
||||
if (!strcmp(iterator.key, "default_config"))
|
||||
foundDefault = TRUE;
|
||||
}
|
||||
return foundDefault;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
WriteAliasSourceDirectory(BongoJsonNode *root, uint64_t modified)
|
||||
{
|
||||
DIR *directory = NULL;
|
||||
struct dirent *entry;
|
||||
BongoJsonObjectIter iterator;
|
||||
BongoJsonObject *fragments;
|
||||
struct stat directoryDetails;
|
||||
uid_t effectiveUser = geteuid();
|
||||
int savedError = 0;
|
||||
BOOL result = FALSE;
|
||||
|
||||
if (!ValidateAliasSourceRoot(root) ||
|
||||
lstat(XPL_DEFAULT_ALIASES_DIR, &directoryDetails) != 0 ||
|
||||
!S_ISDIR(directoryDetails.st_mode))
|
||||
return FALSE;
|
||||
fragments = BongoJsonNodeAsObject(root);
|
||||
directory = opendir(XPL_DEFAULT_ALIASES_DIR);
|
||||
if (!directory)
|
||||
return FALSE;
|
||||
while ((entry = readdir(directory)) != NULL) {
|
||||
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
|
||||
continue;
|
||||
if (!ValidAliasConfigurationName(entry->d_name)) {
|
||||
fprintf(stderr, _("bongo-manager: unsupported alias file %s/%s\n"),
|
||||
XPL_DEFAULT_ALIASES_DIR, entry->d_name);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
closedir(directory);
|
||||
directory = NULL;
|
||||
if (effectiveUser != 0 && XplSetEffectiveUserId(0) != 0)
|
||||
goto done;
|
||||
for (BongoJsonObjectIterFirst(fragments, &iterator); iterator.key;
|
||||
BongoJsonObjectIterNext(fragments, &iterator)) {
|
||||
struct stat details = directoryDetails;
|
||||
char path[XPL_MAX_PATH + 1];
|
||||
char *content;
|
||||
|
||||
if (snprintf(path, sizeof(path), "%s/%s", XPL_DEFAULT_ALIASES_DIR,
|
||||
iterator.key) >= (int)sizeof(path))
|
||||
goto done;
|
||||
if (lstat(path, &details) != 0) {
|
||||
if (errno != ENOENT)
|
||||
goto done;
|
||||
details = directoryDetails;
|
||||
details.st_mode = S_IFREG | 0644;
|
||||
} else if (!S_ISREG(details.st_mode)) {
|
||||
errno = EINVAL;
|
||||
goto done;
|
||||
}
|
||||
content = BongoJsonNodeToString(iterator.value);
|
||||
if (!content)
|
||||
goto done;
|
||||
if (!WriteConfigurationFile(path, content, &details, modified)) {
|
||||
MemFree(content);
|
||||
goto done;
|
||||
}
|
||||
MemFree(content);
|
||||
}
|
||||
directory = opendir(XPL_DEFAULT_ALIASES_DIR);
|
||||
if (!directory)
|
||||
goto done;
|
||||
while ((entry = readdir(directory)) != NULL) {
|
||||
BongoJsonNode *unused = NULL;
|
||||
char path[XPL_MAX_PATH + 1];
|
||||
|
||||
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
|
||||
continue;
|
||||
if (BongoJsonObjectGet(fragments, entry->d_name, &unused) ==
|
||||
BONGO_JSON_OK)
|
||||
continue;
|
||||
if (snprintf(path, sizeof(path), "%s/%s", XPL_DEFAULT_ALIASES_DIR,
|
||||
entry->d_name) >= (int)sizeof(path) ||
|
||||
unlink(path) != 0)
|
||||
goto done;
|
||||
}
|
||||
closedir(directory);
|
||||
directory = NULL;
|
||||
if (!SetAliasSourceTimes(root, modified))
|
||||
goto done;
|
||||
{
|
||||
int descriptor = open(XPL_DEFAULT_ALIASES_DIR,
|
||||
O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
|
||||
if (descriptor < 0 || fsync(descriptor) != 0) {
|
||||
if (descriptor >= 0)
|
||||
close(descriptor);
|
||||
goto done;
|
||||
}
|
||||
close(descriptor);
|
||||
}
|
||||
result = TRUE;
|
||||
|
||||
done:
|
||||
savedError = errno;
|
||||
if (directory)
|
||||
closedir(directory);
|
||||
if (effectiveUser != 0 && XplSetEffectiveUserId(effectiveUser) != 0) {
|
||||
savedError = errno;
|
||||
result = FALSE;
|
||||
}
|
||||
errno = savedError;
|
||||
if (!result)
|
||||
fprintf(stderr,
|
||||
_("bongo-manager: cannot refresh alias source from Store: %s\n"),
|
||||
strerror(errno));
|
||||
return result;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
BongoJsonObject *aliases;
|
||||
@@ -1691,7 +1953,76 @@ done:
|
||||
}
|
||||
|
||||
static BOOL
|
||||
SyncLocalConfiguration()
|
||||
SyncAliasSourceConfiguration(BOOL forceLocal)
|
||||
{
|
||||
BongoJsonNode *localRoot = NULL;
|
||||
BongoJsonNode *storedRoot = NULL;
|
||||
char *local = NULL;
|
||||
char *stored = NULL;
|
||||
char *canonicalStored = NULL;
|
||||
uint64_t localModified = 0;
|
||||
uint64_t storeModified = 0;
|
||||
BOOL result = FALSE;
|
||||
|
||||
if (!ReadAliasSourceDirectory(&localRoot, &local, &localModified))
|
||||
goto done;
|
||||
if (!NMAPReadConfigFile("alias-fragments", &stored) || !stored) {
|
||||
if (!SyncAliasConfiguration() ||
|
||||
!NMAPReplaceConfigFile("alias-fragments", local, strlen(local)) ||
|
||||
!NMAPReadConfigFileModified("alias-fragments", &storeModified) ||
|
||||
!SetAliasSourceTimes(localRoot, storeModified))
|
||||
goto done;
|
||||
printf(_("bongo-manager: initialized authoritative alias fragments\n"));
|
||||
result = TRUE;
|
||||
goto done;
|
||||
}
|
||||
if (BongoJsonParseString(stored, &storedRoot) != BONGO_JSON_OK ||
|
||||
!ValidateAliasSourceRoot(storedRoot) ||
|
||||
!(canonicalStored = BongoJsonNodeToString(storedRoot)) ||
|
||||
!NMAPReadConfigFileModified("alias-fragments", &storeModified)) {
|
||||
fprintf(stderr,
|
||||
_("bongo-manager: cannot read authoritative alias fragments\n"));
|
||||
goto done;
|
||||
}
|
||||
if (BongoJsonNodeEqual(localRoot, storedRoot)) {
|
||||
if (!SetAliasSourceTimes(localRoot, storeModified) ||
|
||||
!SyncAliasConfiguration())
|
||||
goto done;
|
||||
result = TRUE;
|
||||
goto done;
|
||||
}
|
||||
if (forceLocal || localModified > storeModified) {
|
||||
if (!SyncAliasConfiguration() ||
|
||||
!NMAPReplaceConfigFile("alias-fragments", local, strlen(local)) ||
|
||||
!NMAPReadConfigFileModified("alias-fragments", &storeModified) ||
|
||||
!SetAliasSourceTimes(localRoot, storeModified))
|
||||
goto done;
|
||||
printf(_("bongo-manager: applied emergency alias file changes to Store\n"));
|
||||
result = TRUE;
|
||||
} else {
|
||||
if (!WriteAliasSourceDirectory(storedRoot, storeModified) ||
|
||||
!SyncAliasConfiguration())
|
||||
goto done;
|
||||
printf(_("bongo-manager: refreshed alias files from the Store\n"));
|
||||
result = TRUE;
|
||||
}
|
||||
|
||||
done:
|
||||
if (localRoot)
|
||||
BongoJsonNodeFree(localRoot);
|
||||
if (storedRoot)
|
||||
BongoJsonNodeFree(storedRoot);
|
||||
if (local)
|
||||
MemFree(local);
|
||||
if (stored)
|
||||
MemFree(stored);
|
||||
if (canonicalStored)
|
||||
MemFree(canonicalStored);
|
||||
return result;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
SyncLocalConfiguration(BOOL forceLocal)
|
||||
{
|
||||
static const char *const names[] = {
|
||||
"global", "queue", "smtp", "imap", "pop3", "sieve", "collector",
|
||||
@@ -1705,10 +2036,10 @@ SyncLocalConfiguration()
|
||||
|
||||
if (snprintf(path, sizeof(path), "%s/%s", XPL_DEFAULT_CONFIG_DIR,
|
||||
*name) >= (int)sizeof(path) ||
|
||||
!SyncConfigurationDocument(*name, path))
|
||||
!SyncConfigurationDocument(*name, path, forceLocal))
|
||||
return FALSE;
|
||||
}
|
||||
return SyncAliasConfiguration();
|
||||
return SyncAliasSourceConfiguration(forceLocal);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -2216,7 +2547,7 @@ main(int argc, char **argv)
|
||||
goto err_handler;
|
||||
}
|
||||
StartStore();
|
||||
if (!SyncLocalConfiguration()) {
|
||||
if (!SyncLocalConfiguration(FALSE)) {
|
||||
printf(_("bongo-manager: Couldn't apply local configuration\n"));
|
||||
goto err_handler;
|
||||
}
|
||||
@@ -2244,7 +2575,7 @@ main(int argc, char **argv)
|
||||
}
|
||||
|
||||
if (ReloadNow) {
|
||||
if (!SyncLocalConfiguration()) {
|
||||
if (!SyncLocalConfiguration(TRUE)) {
|
||||
fprintf(stderr,
|
||||
_("bongo-manager: configuration synchronization failed\n"));
|
||||
ReloadNow = FALSE;
|
||||
|
||||
@@ -376,6 +376,13 @@ class ConfigurationStore:
|
||||
merged_domains[source_key] = (destination, fragment)
|
||||
return aliases
|
||||
|
||||
def _write_alias_fragments_to_store(
|
||||
self, fragments: Mapping[str, Mapping[str, Any]]) -> None:
|
||||
self._helper(
|
||||
"__config-replace", "alias-fragments",
|
||||
json_bytes({name: dict(value)
|
||||
for name, value in fragments.items()}))
|
||||
|
||||
def dmarc_report_users(self, hosted_domains: Iterable[str]) -> dict[str, str]:
|
||||
"""Resolve each hosted domain's DMARC reporting alias for UI tasks."""
|
||||
domains = [domain_to_ascii(str(domain)) for domain in hosted_domains]
|
||||
@@ -618,7 +625,12 @@ class ConfigurationStore:
|
||||
transaction, json_bytes(record),
|
||||
_FileSnapshot(False, b"", 0o600, os.geteuid(),
|
||||
os.getegid()))
|
||||
self._write_alias_fragments_to_store(self.load_aliases())
|
||||
except BaseException:
|
||||
try:
|
||||
transaction.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if changed:
|
||||
_restore(target, before)
|
||||
raise
|
||||
@@ -654,10 +666,17 @@ class ConfigurationStore:
|
||||
raise ConfigurationError(
|
||||
"invalid alias rename rollback data") from error
|
||||
current = _snapshot(target)
|
||||
if not current.exists or current.content != expected:
|
||||
before_matches = (
|
||||
current.exists == before.exists and
|
||||
(not current.exists or current.content == before.content)
|
||||
)
|
||||
if not before_matches and (
|
||||
not current.exists or current.content != expected):
|
||||
raise ConfigurationError(
|
||||
"account rename aliases changed during the transaction")
|
||||
_restore(target, before)
|
||||
if not before_matches:
|
||||
_restore(target, before)
|
||||
self._write_alias_fragments_to_store(self.load_aliases())
|
||||
transaction.unlink()
|
||||
_fsync_directory(transaction.parent)
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user