feat(provisioning): Add support for Dovecot master user authentication#12442
feat(provisioning): Add support for Dovecot master user authentication#12442
Conversation
Signed-off-by: Timo Nieminen <timo.nieminen@tnnet.fi>
Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
| 'smtpSslMode' => $this->getSmtpSslMode(), | ||
| 'masterPasswordEnabled' => $this->getMasterPasswordEnabled(), | ||
| 'masterPassword' => !empty($this->getMasterPassword()) ? self::MASTER_PASSWORD_PLACEHOLDER : null, | ||
| 'masterUser' => $this->getMasterUser(), |
There was a problem hiding this comment.
The initial PR flagged the master user as confidential (like the masterPasword). The username doesn't sound too critical to me, so I've dropped it.
| } | ||
| if (!isset($data['imapPort']) || (int)$data['imapPort'] === 0) { | ||
| $exception->setField('imapHost', false); | ||
| $exception->setField('imapPort', false); |
There was a problem hiding this comment.
Unrelated, yet I fixed it while on it. It's extra commit; we can pull that out if necessary.
| $masterPasswordEnabled = (bool)($data['masterPasswordEnabled'] ?? false); | ||
| $masterPassword = $data['masterPassword'] ?? ''; | ||
| $masterUser = $data['masterUser'] ?? ''; | ||
| $masterUserSeparator = $data['masterUserSeparator'] ?? ''; |
There was a problem hiding this comment.
The initial PR had a check "if masterUser not empty, and masterUser is not the placeholder, and masterPasswordEnabled is false", then make masterPasswordEnabled required.
I've reworked it to only show the inputs for password, username, and separator when the checkbox is toggled.
Backend-wise, the validation should follow the checkbox. If master password enabled, then we need a password. If non-empty username is given, also the separator is needed.
In addition, the current values are now cleared if the master password is disabled.
| if ($provisioningId !== null) { | ||
| $provisioning = $this->provisioningMapper->get($provisioningId); | ||
| if ($provisioning !== null && !empty($provisioning->getMasterUser())) { | ||
| $separator = $provisioning->getMasterUserSeparator() ?? '*'; |
There was a problem hiding this comment.
@ChristophWurst if $provisioning = null, throw (like for oauth)?
| $provisioning = $this->provisioningMapper->get($provisioningId); | ||
| if ($provisioning !== null && !empty($provisioning->getMasterUser())) { | ||
| $separator = $provisioning->getMasterUserSeparator() ?? '*'; | ||
| $user = $user . $separator . $provisioning->getMasterUser(); |
There was a problem hiding this comment.
@ChristophWurst wdyt about moving that logic to a trait?
There was a problem hiding this comment.
Pull request overview
Adds provisioning support for Dovecot “master user” authentication by letting provisioned accounts authenticate as user{separator}masteruser (using the existing master password), and exposes this configuration in the provisioning settings UI.
Changes:
- Add
masterUserandmasterUserSeparatorfields to provisioning settings (UI + DB migration). - Append master-user login formatting in IMAP/SMTP/Sieve client factories when a provisioning config specifies a master user.
- Extend validation and tests to cover the new provisioning fields and constructor wiring.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Unit/SMTP/SmtpClientFactoryTest.php | Updates factory construction to include ProvisioningMapper. |
| tests/Unit/Db/ProvisioningMapperTest.php | Adds unit tests for provisioning validation (new file). |
| tests/Integration/Sieve/SieveClientFactoryTest.php | Updates factory construction to include ProvisioningMapper. |
| tests/Integration/IMAP/IMAPClientFactoryTest.php | Updates factory construction to include ProvisioningMapper. |
| tests/Integration/Framework/Caching.php | Updates IMAP factory wiring to include ProvisioningMapper. |
| src/components/settings/ProvisioningSettings.vue | Adds UI inputs/help text for master user + separator. |
| src/components/settings/ProvisionPreview.vue | Updates preview to show effective login usernames and mode (static vs master user). |
| lib/Sieve/SieveClientFactory.php | Applies master-user username formatting for Sieve connections. |
| lib/SMTP/SmtpClientFactory.php | Applies master-user username formatting for SMTP connections. |
| lib/IMAP/IMAPClientFactory.php | Applies master-user username formatting for IMAP connections. |
| lib/Migration/Version5007Date20260124120000.php | Adds master_user and master_user_separator columns to mail_provisionings. |
| lib/Db/ProvisioningMapper.php | Extends validation + entity population for master user/separator fields. |
| lib/Db/Provisioning.php | Adds new provisioning entity properties + JSON serialization fields. |
| appinfo/info.xml | Bumps app version to 5.7.0-rc.2. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <input | ||
| id="mail-master-password" | ||
| v-model="masterPassword" | ||
| :disabled="loading" | ||
| type="password" | ||
| :required="masterPasswordEnabled"> | ||
| <label for="mail-master-password"> {{ t('mail', 'Master password') }} </label> | ||
| </div> |
There was a problem hiding this comment.
The master password input/label uses a static id/for ("mail-master-password"). This component uses setting.id to namespace other inputs; keeping this one static will create duplicate IDs when multiple provisioning settings are rendered and breaks label association / accessibility. Use an ID that includes setting.id (and match the label’s for).
| $masterPasswordEnabled = (bool)($data['masterPasswordEnabled'] ?? false); | ||
| $masterPassword = $data['masterPassword'] ?? ''; | ||
| $masterUser = $data['masterUser'] ?? ''; | ||
| $masterUserSeparator = $data['masterUserSeparator'] ?? ''; | ||
|
|
||
| if ($masterPasswordEnabled) { | ||
| if ($masterPassword === '') { | ||
| $exception->setField('masterPassword', false); | ||
| } | ||
| if ($masterUser !== '' && $masterUserSeparator === '') { | ||
| $exception->setField('masterUserSeparator', false); | ||
| } | ||
| } |
There was a problem hiding this comment.
masterUserSeparator is treated as required when masterUser is set, but the PR description calls the separator optional and other code paths default to * when unset. If the separator should be optional, adjust validation to treat empty as default * (and ideally normalize empty to null so factories can reliably fall back).
| // Check for Dovecot master user authentication | ||
| $provisioningId = $account->getMailAccount()->getProvisioningId(); | ||
| if ($provisioningId !== null) { | ||
| $provisioning = $this->provisioningMapper->get($provisioningId); | ||
| if ($provisioning !== null && !empty($provisioning->getMasterUser())) { | ||
| $separator = $provisioning->getMasterUserSeparator() ?? '*'; | ||
| $user = $user . $separator . $provisioning->getMasterUser(); | ||
| } | ||
| } |
There was a problem hiding this comment.
The newly introduced Dovecot master-user behavior isn’t covered by tests in this file. Add a test case where the account has a provisioning with masterUser (and separator) set, and assert that the IMAP client is created with the expected combined username (including default * behavior when separator is unset/null).
| // Check for Dovecot master user authentication | ||
| $provisioningId = $mailAccount->getProvisioningId(); | ||
| if ($provisioningId !== null) { | ||
| $provisioning = $this->provisioningMapper->get($provisioningId); | ||
| if ($provisioning !== null && !empty($provisioning->getMasterUser())) { | ||
| $separator = $provisioning->getMasterUserSeparator() ?? '*'; | ||
| $username = $username . $separator . $provisioning->getMasterUser(); | ||
| } | ||
| } |
There was a problem hiding this comment.
This adds an additional DB lookup ($this->provisioningMapper->get(...)) every time an SMTP transport is created for a provisioned account. If transports are created multiple times per request or during background jobs, consider caching provisionings by ID in the factory (or attaching provisioning data to the MailAccount) to avoid repeated queries.
| // Check for Dovecot master user authentication | ||
| $provisioningId = $account->getMailAccount()->getProvisioningId(); | ||
| if ($provisioningId !== null) { | ||
| $provisioning = $this->provisioningMapper->get($provisioningId); | ||
| if ($provisioning !== null && !empty($provisioning->getMasterUser())) { | ||
| $separator = $provisioning->getMasterUserSeparator() ?? '*'; | ||
| $user = $user . $separator . $provisioning->getMasterUser(); | ||
| } | ||
| } |
There was a problem hiding this comment.
The newly introduced Dovecot master-user behavior isn’t covered by tests. Consider adding an integration/unit test that sets a provisioning ID on the MailAccount, mocks ProvisioningMapper->get(...) to return a Provisioning with masterUser/separator, and asserts the created Sieve client uses the expected combined username.
| public function setup(): void { | ||
| parent::setUp(); | ||
|
|
||
| $this->mapper = new ProvisioningMapper( | ||
| $this->createMock(IDBConnection::class), | ||
| new NullLogger(), | ||
| ); | ||
| } |
There was a problem hiding this comment.
PHPUnit will not call setup(); the correct lifecycle hook is setUp(). As written, $this->mapper is never initialized and the first test will error when calling $this->mapper->validate(...). Rename to protected function setUp(): void (and keep the parent::setUp() call).
| <input | ||
| :id="'mail-master-user-separator' + setting.id" | ||
| v-model="masterUserSeparator" | ||
| :disabled="loading" | ||
| type="text" | ||
| maxlength="8" | ||
| :required="needsMasterUserSeparator"> | ||
| </label> |
There was a problem hiding this comment.
The PR description says the master-user separator is optional, and the preview defaults it to * when unset. However the UI marks it as required when masterUser is set, and the backend validation rejects an empty separator. Consider allowing an empty separator to mean “use default *” (and persist null), or update the PR description/UI to make the separator mandatory.
| // Check for Dovecot master user authentication | ||
| $provisioningId = $account->getMailAccount()->getProvisioningId(); | ||
| if ($provisioningId !== null) { | ||
| $provisioning = $this->provisioningMapper->get($provisioningId); | ||
| if ($provisioning !== null && !empty($provisioning->getMasterUser())) { | ||
| $separator = $provisioning->getMasterUserSeparator() ?? '*'; | ||
| $user = $user . $separator . $provisioning->getMasterUser(); | ||
| } | ||
| } |
There was a problem hiding this comment.
This adds an extra DB lookup ($this->provisioningMapper->get(...)) every time an IMAP client is created for a provisioned account. If getClient is called frequently, this can become a noticeable overhead. Consider caching provisionings by ID in the factory (or passing the provisioning data along with the MailAccount) so the lookup happens at most once per provisioning per request.
| // Check for Dovecot master user authentication | ||
| $provisioningId = $account->getMailAccount()->getProvisioningId(); | ||
| if ($provisioningId !== null) { | ||
| $provisioning = $this->provisioningMapper->get($provisioningId); | ||
| if ($provisioning !== null && !empty($provisioning->getMasterUser())) { | ||
| $separator = $provisioning->getMasterUserSeparator() ?? '*'; | ||
| $user = $user . $separator . $provisioning->getMasterUser(); | ||
| } | ||
| } |
There was a problem hiding this comment.
The new master-user username formatting logic is duplicated across IMAP/SMTP/Sieve factories. To reduce drift (e.g., default separator handling, enablement checks), consider extracting this into a shared helper (e.g., on Provisioning or a small utility) and reuse it here.
| // Check for Dovecot master user authentication | ||
| $provisioningId = $mailAccount->getProvisioningId(); | ||
| if ($provisioningId !== null) { | ||
| $provisioning = $this->provisioningMapper->get($provisioningId); | ||
| if ($provisioning !== null && !empty($provisioning->getMasterUser())) { | ||
| $separator = $provisioning->getMasterUserSeparator() ?? '*'; | ||
| $username = $username . $separator . $provisioning->getMasterUser(); | ||
| } | ||
| } |
There was a problem hiding this comment.
The newly introduced Dovecot master-user behavior isn’t covered by unit tests. Consider adding a test that sets a provisioning ID on the MailAccount, mocks ProvisioningMapper->get(...) to return a Provisioning with masterUser/separator, and asserts the resulting transport params use the combined username.
Local copy of #12306 with conflicts resolved