-
Notifications
You must be signed in to change notification settings - Fork 299
feat(provisioning): Add support for Dovecot master user authentication #12442
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5db4d04
d85b9c6
db13be6
507857f
332dd28
7b9191f
ba82449
df3627d
f7a0068
2a222c6
ba3e097
393be4d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,7 +67,7 @@ public function validate(array $data): Provisioning { | |
| $exception->setField('imapHost', false); | ||
| } | ||
| if (!isset($data['imapPort']) || (int)$data['imapPort'] === 0) { | ||
| $exception->setField('imapHost', false); | ||
| $exception->setField('imapPort', false); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unrelated, yet I fixed it while on it. It's extra commit; we can pull that out if necessary. |
||
| } | ||
| if (!isset($data['imapSslMode']) || $data['imapSslMode'] === '') { | ||
| $exception->setField('imapSslMode', false); | ||
|
|
@@ -92,6 +92,20 @@ public function validate(array $data): Provisioning { | |
| $exception->setField('ldapAliasesAttribute', false); | ||
| } | ||
|
|
||
| $masterPasswordEnabled = (bool)($data['masterPasswordEnabled'] ?? false); | ||
| $masterPassword = $data['masterPassword'] ?? ''; | ||
| $masterUser = $data['masterUser'] ?? ''; | ||
| $masterUserSeparator = $data['masterUserSeparator'] ?? ''; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 ($masterPasswordEnabled) { | ||
| if ($masterPassword === '') { | ||
| $exception->setField('masterPassword', false); | ||
| } | ||
| if ($masterUser !== '' && $masterUserSeparator === '') { | ||
| $exception->setField('masterUserSeparator', false); | ||
| } | ||
| } | ||
|
|
||
| if (!empty($exception->getFields())) { | ||
| throw $exception; | ||
| } | ||
|
|
@@ -108,12 +122,6 @@ public function validate(array $data): Provisioning { | |
| $provisioning->setSmtpHost($data['smtpHost']); | ||
| $provisioning->setSmtpPort((int)$data['smtpPort']); | ||
| $provisioning->setSmtpSslMode($data['smtpSslMode']); | ||
|
|
||
| $provisioning->setMasterPasswordEnabled((bool)($data['masterPasswordEnabled'] ?? false)); | ||
| if (isset($data['masterPassword']) && $data['masterPassword'] !== Provisioning::MASTER_PASSWORD_PLACEHOLDER) { | ||
| $provisioning->setMasterPassword($data['masterPassword']); | ||
| } | ||
|
|
||
| $provisioning->setSieveEnabled((bool)$data['sieveEnabled']); | ||
| $provisioning->setSieveHost($data['sieveHost'] ?? ''); | ||
| $provisioning->setSieveUser($data['sieveUser'] ?? ''); | ||
|
|
@@ -123,6 +131,20 @@ public function validate(array $data): Provisioning { | |
| $provisioning->setLdapAliasesProvisioning($ldapAliasesProvisioning); | ||
| $provisioning->setLdapAliasesAttribute($ldapAliasesAttribute); | ||
|
|
||
| if ($masterPasswordEnabled) { | ||
| $provisioning->setMasterPasswordEnabled(true); | ||
| if ($masterPassword !== Provisioning::MASTER_PASSWORD_PLACEHOLDER) { | ||
| $provisioning->setMasterPassword($masterPassword); | ||
| } | ||
| $provisioning->setMasterUser($masterUser); | ||
| $provisioning->setMasterUserSeparator($masterUserSeparator); | ||
| } else { | ||
| $provisioning->setMasterPasswordEnabled(false); | ||
| $provisioning->setMasterPassword(null); | ||
| $provisioning->setMasterUser(null); | ||
| $provisioning->setMasterUserSeparator(null); | ||
| } | ||
|
|
||
| return $provisioning; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |
| use Horde_Imap_Client_Socket; | ||
| use OCA\Mail\Account; | ||
| use OCA\Mail\Cache\HordeCacheFactory; | ||
| use OCA\Mail\Db\ProvisioningMapper; | ||
| use OCA\Mail\Events\BeforeImapClientCreated; | ||
| use OCA\Mail\Exception\ServiceException; | ||
| use OCP\AppFramework\Utility\ITimeFactory; | ||
|
|
@@ -42,12 +43,15 @@ class IMAPClientFactory { | |
| private ITimeFactory $timeFactory; | ||
| private HordeCacheFactory $hordeCacheFactory; | ||
|
|
||
| public function __construct(ICrypto $crypto, | ||
| public function __construct( | ||
| ICrypto $crypto, | ||
| IConfig $config, | ||
| ICacheFactory $cacheFactory, | ||
| IEventDispatcher $eventDispatcher, | ||
| ITimeFactory $timeFactory, | ||
| HordeCacheFactory $hordeCacheFactory) { | ||
| HordeCacheFactory $hordeCacheFactory, | ||
| private ProvisioningMapper $provisioningMapper | ||
| ) { | ||
| $this->crypto = $crypto; | ||
| $this->config = $config; | ||
| $this->cacheFactory = $cacheFactory; | ||
|
|
@@ -85,6 +89,16 @@ public function getClient(Account $account, bool $useCache = true): Horde_Imap_C | |
| $sslMode = false; | ||
| } | ||
|
|
||
| // 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() ?? '*'; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ChristophWurst if $provisioning = null, throw (like for oauth)? |
||
| $user = $user . $separator . $provisioning->getMasterUser(); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ChristophWurst wdyt about moving that logic to a trait? |
||
| } | ||
| } | ||
|
|
||
| $params = [ | ||
| 'username' => $user, | ||
| 'password' => $decryptedPassword, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /** | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| namespace OCA\Mail\Migration; | ||
|
|
||
| use Closure; | ||
| use OCP\DB\ISchemaWrapper; | ||
| use OCP\DB\Types; | ||
| use OCP\Migration\IOutput; | ||
| use OCP\Migration\SimpleMigrationStep; | ||
|
|
||
| /** | ||
| * Add master_user and master_user_separator columns to mail_provisionings table | ||
| * for Dovecot Master User authentication support. | ||
| * | ||
| * @codeCoverageIgnore | ||
| */ | ||
| class Version5007Date20260124120000 extends SimpleMigrationStep { | ||
| /** | ||
| * @param IOutput $output | ||
| * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` | ||
| * @param array $options | ||
| * @return null|ISchemaWrapper | ||
| */ | ||
| #[\Override] | ||
| public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { | ||
| $schema = $schemaClosure(); | ||
|
|
||
| $provisioningTable = $schema->getTable('mail_provisionings'); | ||
| if (!$provisioningTable->hasColumn('master_user')) { | ||
| $provisioningTable->addColumn('master_user', Types::STRING, [ | ||
| 'notnull' => false, | ||
| 'length' => 256, | ||
| ]); | ||
| } | ||
| if (!$provisioningTable->hasColumn('master_user_separator')) { | ||
| $provisioningTable->addColumn('master_user_separator', Types::STRING, [ | ||
| 'notnull' => false, | ||
| 'length' => 8, | ||
| ]); | ||
| } | ||
|
|
||
| return $schema; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.