diff --git a/lib/Db/Provisioning.php b/lib/Db/Provisioning.php index 31e0a22081..32b1da9083 100644 --- a/lib/Db/Provisioning.php +++ b/lib/Db/Provisioning.php @@ -60,6 +60,24 @@ class Provisioning extends Entity implements JsonSerializable { public const WILDCARD = '*'; public const MASTER_PASSWORD_PLACEHOLDER = '********'; + /** + * Captured names reach the directory as the requested attribute of an LDAP read, + * so the class stays too narrow for any LDAP special character to pass. + */ + private const LDAP_PLACEHOLDER_PATTERN = '%LDAP:([A-Za-z][A-Za-z0-9-]*)%'; + private const LDAP_PLACEHOLDER_REGEX = '/' . self::LDAP_PLACEHOLDER_PATTERN . '/'; + private const LDAP_PLACEHOLDER_ANCHORED_REGEX = '/^' . self::LDAP_PLACEHOLDER_PATTERN . '$/D'; + + /** Anything shaped like an LDAP placeholder, including unsupported syntax */ + private const LDAP_PLACEHOLDER_LOOSE_REGEX = '/%ldap:[^%]*%/i'; + + /** + * '%' opens and closes a placeholder, so replacing the token types one after + * another lets adjacent placeholders consume each other's delimiter. All of + * them therefore have to be substituted in a single pass. + */ + private const PLACEHOLDER_REGEX = '/%USERID%|%EMAIL%|' . self::LDAP_PLACEHOLDER_PATTERN . '/'; + protected $provisioningDomain; protected $emailTemplate; protected $imapUser; @@ -120,57 +138,121 @@ public function jsonSerialize() { } /** + * @param IUser $user + * @param array $ldapValues resolved %LDAP:attr% tokens, keyed by full token * @return string */ - public function buildImapUser(IUser $user) { + public function buildImapUser(IUser $user, array $ldapValues = []) { if (!is_null($this->getImapUser())) { - return $this->buildUserEmail($this->getImapUser(), $user); + return $this->buildUserEmail($this->getImapUser(), $user, $ldapValues); } - return $this->buildEmail($user); + return $this->buildEmail($user, $ldapValues); } /** * @param IUser $user + * @param array $ldapValues resolved %LDAP:attr% tokens, keyed by full token * @return string */ - public function buildEmail(IUser $user) { - return $this->buildUserEmail($this->getEmailTemplate(), $user); + public function buildEmail(IUser $user, array $ldapValues = []) { + return $this->buildUserEmail($this->getEmailTemplate(), $user, $ldapValues); + } + + /** + * Unique attribute names referenced via %LDAP:attr%, spelled as in the templates. + * The sieve template only counts while sieve is enabled, as its account settings + * are not built otherwise. + * + * @return string[] + */ + public function ldapAttributesInTemplates(): array { + $attributes = []; + $templates = [ + $this->getEmailTemplate(), + $this->getImapUser(), + $this->getSmtpUser(), + ]; + if ($this->getSieveEnabled()) { + $templates[] = $this->getSieveUser(); + } + foreach ($templates as $template) { + if ($template === null) { + continue; + } + if (preg_match_all(self::LDAP_PLACEHOLDER_REGEX, $template, $matches) > 0) { + $attributes = array_merge($attributes, $matches[1]); + } + } + return array_values(array_unique($attributes)); + } + + /** + * Placeholders using unsupported syntax, e.g. a lowercase prefix or an attribute + * option. They would never be substituted and end up literally in an account. + * + * @return string[] + */ + public static function findMalformedLdapPlaceholders(?string $template): array { + if ($template === null || preg_match_all(self::LDAP_PLACEHOLDER_LOOSE_REGEX, $template, $matches) === 0) { + return []; + } + $malformed = []; + foreach ($matches[0] as $candidate) { + if (preg_match(self::LDAP_PLACEHOLDER_ANCHORED_REGEX, $candidate) !== 1) { + $malformed[] = $candidate; + } + } + return array_values(array_unique($malformed)); } /** - * Replace %USERID% and %EMAIL% to allow special configurations + * Replace %USERID%, %EMAIL% and %LDAP:attr% to allow special configurations. + * Tokens without a value stay literal. * * @param string $original * @param IUser $user + * @param array $ldapValues resolved %LDAP:attr% tokens, keyed by full token * @return string */ - private function buildUserEmail(string $original, IUser $user) { - $original = str_replace('%USERID%', $user->getUID(), $original); - if ($user->getEMailAddress() !== null) { - $original = str_replace('%EMAIL%', $user->getEMailAddress(), $original); - } - return $original; + private function buildUserEmail(string $original, IUser $user, array $ldapValues = []) { + $replaced = preg_replace_callback( + self::PLACEHOLDER_REGEX, + static function (array $match) use ($user, $ldapValues): string { + switch ($match[0]) { + case '%USERID%': + return $user->getUID(); + case '%EMAIL%': + return $user->getEMailAddress() ?? $match[0]; + default: + return $ldapValues[$match[0]] ?? $match[0]; + } + }, + $original + ); + return $replaced ?? $original; } /** * @param IUser $user + * @param array $ldapValues resolved %LDAP:attr% tokens, keyed by full token * @return string */ - public function buildSmtpUser(IUser $user) { + public function buildSmtpUser(IUser $user, array $ldapValues = []) { if (!is_null($this->getSmtpUser())) { - return $this->buildUserEmail($this->getSmtpUser(), $user); + return $this->buildUserEmail($this->getSmtpUser(), $user, $ldapValues); } - return $this->buildEmail($user); + return $this->buildEmail($user, $ldapValues); } /** * @param IUser $user + * @param array $ldapValues resolved %LDAP:attr% tokens, keyed by full token * @return string */ - public function buildSieveUser(IUser $user) { + public function buildSieveUser(IUser $user, array $ldapValues = []) { if (!is_null($this->getSieveUser())) { - return $this->buildUserEmail($this->getSieveUser(), $user); + return $this->buildUserEmail($this->getSieveUser(), $user, $ldapValues); } - return $this->buildEmail($user); + return $this->buildEmail($user, $ldapValues); } } diff --git a/lib/Db/ProvisioningMapper.php b/lib/Db/ProvisioningMapper.php index deb46d7263..d108057267 100644 --- a/lib/Db/ProvisioningMapper.php +++ b/lib/Db/ProvisioningMapper.php @@ -84,6 +84,12 @@ public function validate(array $data): Provisioning { $exception->setField('smtpSslMode', false); } + foreach (['emailTemplate', 'imapUser', 'smtpUser', 'sieveUser'] as $templateField) { + if (Provisioning::findMalformedLdapPlaceholders($data[$templateField] ?? null) !== []) { + $exception->setField($templateField, false); + } + } + $ldapAliasesProvisioning = (bool)($data['ldapAliasesProvisioning'] ?? false); $ldapAliasesAttribute = $data['ldapAliasesAttribute'] ?? ''; diff --git a/lib/Service/Provisioning/Manager.php b/lib/Service/Provisioning/Manager.php index aae667d08c..5d7aa674b8 100644 --- a/lib/Service/Provisioning/Manager.php +++ b/lib/Service/Provisioning/Manager.php @@ -207,12 +207,18 @@ public function provisionSingleUser(array $provisionings, IUser $user): bool { return false; } + $ldapValues = $this->resolveLdapPlaceholders($provisioning, $user); + if ($ldapValues === null) { + // Persisting the raw templates would break an already working account + return false; + } + try { // TODO: match by UID only, catch multiple objects returned below and delete all those accounts $mailAccount = $this->mailAccountMapper->findProvisionedAccount($user); $mailAccount = $this->mailAccountMapper->update( - $this->updateAccount($user, $mailAccount, $provisioning) + $this->updateAccount($user, $mailAccount, $provisioning, $ldapValues) ); } catch (DoesNotExistException|MultipleObjectsReturnedException $e) { if ($e instanceof MultipleObjectsReturnedException) { @@ -226,7 +232,7 @@ public function provisionSingleUser(array $provisionings, IUser $user): bool { $mailAccount->setUserId($user->getUID()); $mailAccount->setClassificationEnabled($this->classificationSettingsService->isClassificationEnabledByDefault()); $mailAccount = $this->mailAccountMapper->insert( - $this->updateAccount($user, $mailAccount, $provisioning) + $this->updateAccount($user, $mailAccount, $provisioning, $ldapValues) ); $this->accountService->scheduleBackgroundJobs($mailAccount->getId()); @@ -283,24 +289,91 @@ public function updateProvisioning(array $data): void { } } - private function updateAccount(IUser $user, MailAccount $account, Provisioning $config): MailAccount { + /** + * Resolved values become login names and the account email address. Commas and + * angle brackets would let the directory value break out of the templated domain, + * spaces and control characters yield a login that can never authenticate, and 64 + * is the narrowest account column. + */ + private const LDAP_VALUE_REGEX = '/^[A-Za-z0-9._+@-]{1,64}$/D'; + + /** + * Resolve the %LDAP:attr% placeholders of a config for a user. + * + * @return array|null the token => value map, or null if a + * placeholder could not be resolved + */ + private function resolveLdapPlaceholders(Provisioning $config, IUser $user): ?array { + $attributes = $config->ldapAttributesInTemplates(); + if ($attributes === []) { + return []; + } + + if ($user->getBackendClassName() !== 'LDAP') { + // Expected in mixed setups, where a wildcard config also matches local users + $this->logger->debug('Provisioning config uses LDAP placeholders but user ' . $user->getUID() . ' is not an LDAP user'); + return null; + } + + if ($this->ldapProviderFactory->isAvailable() === false) { + $this->logger->warning('Provisioning config uses LDAP placeholders but LDAP is not available'); + return null; + } + + try { + $provider = $this->ldapProviderFactory->getLDAPProvider(); + } catch (\Throwable $e) { + $this->logger->warning('LDAP provider unavailable for mail provisioning placeholders', ['exception' => $e]); + return null; + } + + $fetched = []; + $values = []; + foreach ($attributes as $attribute) { + $key = strtolower($attribute); + if (!array_key_exists($key, $fetched)) { + try { + $fetched[$key] = $provider->getUserAttribute($user->getUID(), $attribute); + } catch (\Throwable $e) { + $this->logger->warning('Could not read LDAP attribute ' . $attribute . ' of user ' . $user->getUID() . ', skipping provisioning', ['exception' => $e]); + return null; + } + } + $value = $fetched[$key]; + if ($value === null || $value === '') { + $this->logger->warning('LDAP attribute ' . $attribute . ' of user ' . $user->getUID() . ' is empty, skipping provisioning'); + return null; + } + if (preg_match(self::LDAP_VALUE_REGEX, $value) !== 1) { + $this->logger->warning('LDAP attribute ' . $attribute . ' of user ' . $user->getUID() . ' contains unsupported characters or is too long, skipping provisioning'); + return null; + } + $values['%LDAP:' . $attribute . '%'] = $value; + } + return $values; + } + + /** + * @param array $ldapValues + */ + private function updateAccount(IUser $user, MailAccount $account, Provisioning $config, array $ldapValues): MailAccount { // Set the ID to make sure it reflects when the account switches from one config to another $account->setProvisioningId($config->getId()); - $account->setEmail($config->buildEmail($user)); + $account->setEmail($config->buildEmail($user, $ldapValues)); $account->setName($this->userManager->getDisplayName($user->getUID())); - $account->setInboundUser($config->buildImapUser($user)); + $account->setInboundUser($config->buildImapUser($user, $ldapValues)); $account->setInboundHost($config->getImapHost()); $account->setInboundPort($config->getImapPort()); $account->setInboundSslMode($config->getImapSslMode()); - $account->setOutboundUser($config->buildSmtpUser($user)); + $account->setOutboundUser($config->buildSmtpUser($user, $ldapValues)); $account->setOutboundHost($config->getSmtpHost()); $account->setOutboundPort($config->getSmtpPort()); $account->setOutboundSslMode($config->getSmtpSslMode()); $account->setSieveEnabled($config->getSieveEnabled()); if ($config->getSieveEnabled()) { - $account->setSieveUser($config->buildSieveUser($user)); + $account->setSieveUser($config->buildSieveUser($user, $ldapValues)); $account->setSieveHost($config->getSieveHost()); $account->setSievePort($config->getSievePort()); $account->setSieveSslMode($config->getSieveSslMode()); diff --git a/src/components/settings/ProvisioningSettings.vue b/src/components/settings/ProvisioningSettings.vue index ac7be23921..bde46466ab 100644 --- a/src/components/settings/ProvisioningSettings.vue +++ b/src/components/settings/ProvisioningSettings.vue @@ -14,7 +14,7 @@

- +
diff --git a/tests/Integration/Db/ProvisioningMapperTest.php b/tests/Integration/Db/ProvisioningMapperTest.php index f95b91c027..8aee3e3f2a 100644 --- a/tests/Integration/Db/ProvisioningMapperTest.php +++ b/tests/Integration/Db/ProvisioningMapperTest.php @@ -98,6 +98,24 @@ public function testValidateException() { $provisioning = $this->mapper->validate($data); } + public function testValidateRejectsMalformedLdapPlaceholder() { + $data = $this->data; + $data['emailTemplate'] = '%ldap:uid%@heart-of-gold.com'; + + $this->expectException(ValidationException::class); + + $this->mapper->validate($data); + } + + public function testValidateAcceptsLdapPlaceholder() { + $data = $this->data; + $data['emailTemplate'] = '%LDAP:sAMAccountName%@heart-of-gold.com'; + + $provisioning = $this->mapper->validate($data); + + $this->assertSame('%LDAP:sAMAccountName%@heart-of-gold.com', $provisioning->getEmailTemplate()); + } + public function testGetNoResult() { $db = $this->mapper->get(99999); $this->assertNull($db); diff --git a/tests/Unit/Db/ProvisioningTest.php b/tests/Unit/Db/ProvisioningTest.php index d6817381a8..61ece1f195 100644 --- a/tests/Unit/Db/ProvisioningTest.php +++ b/tests/Unit/Db/ProvisioningTest.php @@ -11,6 +11,7 @@ use ChristophWurst\Nextcloud\Testing\TestCase; use OCA\Mail\Db\Provisioning; +use OCP\IUser; /** * @covers \OCA\Mail\Db\Provisioning @@ -25,4 +26,168 @@ public function testJsonSerialize(): void { self::assertArrayHasKey('masterPasswordEnabled', $data); } + private function createUser(?string $email = 'jane@corp.example'): IUser { + return $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'jane', + 'getEMailAddress' => $email, + ]); + } + + public function testBuildEmailWithLdapValue(): void { + $provisioning = new Provisioning(); + $provisioning->setEmailTemplate('%LDAP:sAMAccountName%@corp.example'); + + $email = $provisioning->buildEmail($this->createUser(), [ + '%LDAP:sAMAccountName%' => 'jdoe', + ]); + + self::assertSame('jdoe@corp.example', $email); + } + + public function testBuildEmailWithMultipleDistinctAttributes(): void { + $provisioning = new Provisioning(); + $provisioning->setEmailTemplate('%LDAP:uid%.%LDAP:ou%@corp.example'); + + $email = $provisioning->buildEmail($this->createUser(), [ + '%LDAP:uid%' => 'jdoe', + '%LDAP:ou%' => 'sales', + ]); + + self::assertSame('jdoe.sales@corp.example', $email); + } + + public function testBuildEmailWithRepeatedToken(): void { + $provisioning = new Provisioning(); + $provisioning->setEmailTemplate('%LDAP:uid%+%LDAP:uid%@corp.example'); + + $email = $provisioning->buildEmail($this->createUser(), [ + '%LDAP:uid%' => 'jdoe', + ]); + + self::assertSame('jdoe+jdoe@corp.example', $email); + } + + public function testBuildEmailUnresolvedLdapStaysLiteral(): void { + $provisioning = new Provisioning(); + $provisioning->setEmailTemplate('%LDAP:sAMAccountName%@corp.example'); + + $email = $provisioning->buildEmail($this->createUser(), []); + + self::assertSame('%LDAP:sAMAccountName%@corp.example', $email); + } + + public function testBuildEmailMixedPlaceholders(): void { + $provisioning = new Provisioning(); + $provisioning->setEmailTemplate('%USERID%.%LDAP:uid%.%EMAIL%'); + + $email = $provisioning->buildEmail($this->createUser(), [ + '%LDAP:uid%' => 'jdoe', + ]); + + self::assertSame('jane.jdoe.jane@corp.example', $email); + } + + public function testBuildImapUserFallsBackToEmailTemplateWithLdapValues(): void { + $provisioning = new Provisioning(); + $provisioning->setEmailTemplate('%LDAP:uid%@corp.example'); + + $imapUser = $provisioning->buildImapUser($this->createUser(), [ + '%LDAP:uid%' => 'jdoe', + ]); + + self::assertSame('jdoe@corp.example', $imapUser); + } + + public function testLdapAttributesInTemplates(): void { + $provisioning = new Provisioning(); + $provisioning->setEmailTemplate('%LDAP:uid%@corp.example'); + $provisioning->setImapUser('%LDAP:sAMAccountName%'); + $provisioning->setSmtpUser('%LDAP:sAMAccountName%'); + $provisioning->setSieveEnabled(true); + $provisioning->setSieveUser('%LDAP:mail-alias%'); + + $attributes = $provisioning->ldapAttributesInTemplates(); + + self::assertSame(['uid', 'sAMAccountName', 'mail-alias'], $attributes); + } + + public function testLdapAttributesInTemplatesIgnoresDisabledSieve(): void { + $provisioning = new Provisioning(); + $provisioning->setEmailTemplate('%LDAP:uid%@corp.example'); + $provisioning->setSieveEnabled(false); + $provisioning->setSieveUser('%LDAP:mail-alias%'); + + $attributes = $provisioning->ldapAttributesInTemplates(); + + self::assertSame(['uid'], $attributes); + } + + public function testLdapAttributesInTemplatesIgnoresInvalidTokens(): void { + $provisioning = new Provisioning(); + $provisioning->setEmailTemplate('%LDAP:%@corp.example'); + $provisioning->setImapUser('%LDAP:foo bar%'); + $provisioning->setSmtpUser('%ldap:uid%'); + + $attributes = $provisioning->ldapAttributesInTemplates(); + + self::assertSame([], $attributes); + } + + public function testBuildEmailDoesNotMergeAdjacentPlaceholders(): void { + $provisioning = new Provisioning(); + $provisioning->setEmailTemplate('%LDAP:dept%USERID%@corp.example'); + + $attributes = $provisioning->ldapAttributesInTemplates(); + $email = $provisioning->buildEmail($this->createUser(), [ + '%LDAP:dept%' => 'sales', + ]); + + self::assertSame(['dept'], $attributes); + self::assertSame('salesUSERID%@corp.example', $email); + } + + public function testBuildEmailDoesNotSubstituteResolvedValues(): void { + $provisioning = new Provisioning(); + $provisioning->setEmailTemplate('%LDAP:uid%@corp.example'); + + $email = $provisioning->buildEmail($this->createUser(), [ + '%LDAP:uid%' => '%USERID%', + ]); + + self::assertSame('%USERID%@corp.example', $email); + } + + public function testBuildEmailKeepsEmailTokenWithoutUserEmail(): void { + $provisioning = new Provisioning(); + $provisioning->setEmailTemplate('%USERID%.%EMAIL%'); + + $email = $provisioning->buildEmail($this->createUser(null)); + + self::assertSame('jane.%EMAIL%', $email); + } + + public function testFindMalformedLdapPlaceholders(): void { + $malformed = Provisioning::findMalformedLdapPlaceholders('%ldap:uid%.%LDAP:foo_bar%.%LDAP:mail;binary%.%LDAP:%'); + + self::assertSame(['%ldap:uid%', '%LDAP:foo_bar%', '%LDAP:mail;binary%', '%LDAP:%'], $malformed); + } + + public function testFindMalformedLdapPlaceholdersAcceptsValidSyntax(): void { + $malformed = Provisioning::findMalformedLdapPlaceholders('%LDAP:sAMAccountName%.%LDAP:mail-alias%@corp.example'); + + self::assertSame([], $malformed); + } + + public function testFindMalformedLdapPlaceholdersWithoutTemplate(): void { + self::assertSame([], Provisioning::findMalformedLdapPlaceholders(null)); + } + + public function testLdapAttributesInTemplatesWithoutTemplates(): void { + $provisioning = new Provisioning(); + + $attributes = $provisioning->ldapAttributesInTemplates(); + + self::assertSame([], $attributes); + } + } diff --git a/tests/Unit/Service/Provisioning/ManagerTest.php b/tests/Unit/Service/Provisioning/ManagerTest.php index a1af411851..d33ee4bbb8 100644 --- a/tests/Unit/Service/Provisioning/ManagerTest.php +++ b/tests/Unit/Service/Provisioning/ManagerTest.php @@ -14,6 +14,7 @@ use OCA\Mail\Service\Provisioning\Manager; use OCP\AppFramework\Db\DoesNotExistException; use OCP\IUser; +use OCP\LDAP\ILDAPProvider; use PHPUnit\Framework\MockObject\MockObject; class ManagerTest extends TestCase { @@ -373,4 +374,237 @@ public function testNewProvisioning(): void { self::assertInstanceOf(Provisioning::class, $result); } + + private function ldapPlaceholderConfig(): Provisioning { + $config = new Provisioning(); + $config->setId(1); + $config->setProvisioningDomain('*'); + $config->setEmailTemplate('%LDAP:sAMAccountName%@batman.com'); + $config->setImapUser('%LDAP:sAMAccountName%'); + $config->setSmtpUser('%LDAP:sAMAccountName%'); + return $config; + } + + public function testProvisionSingleUserResolvesLdapPlaceholders(): void { + /** @var IUser|MockObject $user */ + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com', + 'getUID' => 'bruce', + 'getBackendClassName' => 'LDAP', + ]); + $configs = [$this->ldapPlaceholderConfig()]; + $mailAccount = new MailAccount(); + $mailAccount->setId(1000); + $ldapProvider = $this->createMock(ILDAPProvider::class); + $ldapProvider->expects($this->once()) + ->method('getUserAttribute') + ->with('bruce', 'sAMAccountName') + ->willReturn('BWAYNE'); + $this->mock->getParameter('ldapProviderFactory') + ->method('isAvailable') + ->willReturn(true); + $this->mock->getParameter('ldapProviderFactory') + ->method('getLDAPProvider') + ->willReturn($ldapProvider); + $this->mock->getParameter('appManager') + ->expects($this->once()) + ->method('isEnabledForUser') + ->willReturn(true); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->once()) + ->method('findProvisionedAccount') + ->willReturn($mailAccount); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->once()) + ->method('update') + ->with($this->callback(static function (MailAccount $account) { + return $account->getEmail() === 'BWAYNE@batman.com' + && $account->getInboundUser() === 'BWAYNE' + && $account->getOutboundUser() === 'BWAYNE'; + })) + ->willReturn($mailAccount); + + $result = $this->manager->provisionSingleUser($configs, $user); + + $this->assertTrue($result); + } + + private function expectNoAccountWrite(): void { + $this->mock->getParameter('mailAccountMapper') + ->expects($this->never()) + ->method('findProvisionedAccount'); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->never()) + ->method('update'); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->never()) + ->method('insert'); + } + + public function provideUnresolvableLdapValues(): array { + return [ + 'empty' => [''], + 'control characters' => ["BWAYNE\r\nX"], + 'invalid UTF-8' => ["BWAYNE\xFF"], + 'space' => ['Bruce Wayne'], + 'unicode line separator' => ["BWAYNE\u{2028}X"], + 'trailing newline' => ["BWAYNE\n"], + 'trailing carriage return' => ["BWAYNE\r"], + 'comma escaping the domain' => ['ceo@evil.example,x'], + 'angle brackets escaping the domain' => ['a@b.com'], + 'longer than the account column' => [str_repeat('a', 65)], + ]; + } + + /** + * @dataProvider provideUnresolvableLdapValues + */ + public function testProvisionSingleUserSkipsUnusableLdapValue(string $ldapValue): void { + /** @var IUser|MockObject $user */ + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com', + 'getUID' => 'bruce', + 'getBackendClassName' => 'LDAP', + ]); + $configs = [$this->ldapPlaceholderConfig()]; + $ldapProvider = $this->createMock(ILDAPProvider::class); + $ldapProvider->expects($this->once()) + ->method('getUserAttribute') + ->willReturn($ldapValue); + $this->mock->getParameter('ldapProviderFactory') + ->method('isAvailable') + ->willReturn(true); + $this->mock->getParameter('ldapProviderFactory') + ->method('getLDAPProvider') + ->willReturn($ldapProvider); + $this->mock->getParameter('logger') + ->expects($this->atLeastOnce()) + ->method('warning'); + $this->mock->getParameter('appManager') + ->expects($this->once()) + ->method('isEnabledForUser') + ->willReturn(true); + $this->expectNoAccountWrite(); + + $result = $this->manager->provisionSingleUser($configs, $user); + + $this->assertFalse($result); + } + + public function testProvisionSingleUserSkipsWhenLdapThrows(): void { + /** @var IUser|MockObject $user */ + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com', + 'getUID' => 'bruce', + 'getBackendClassName' => 'LDAP', + ]); + $configs = [$this->ldapPlaceholderConfig()]; + $ldapProvider = $this->createMock(ILDAPProvider::class); + $ldapProvider->expects($this->once()) + ->method('getUserAttribute') + ->willThrowException(new \Exception('User id not found in LDAP')); + $this->mock->getParameter('ldapProviderFactory') + ->method('isAvailable') + ->willReturn(true); + $this->mock->getParameter('ldapProviderFactory') + ->method('getLDAPProvider') + ->willReturn($ldapProvider); + $this->mock->getParameter('logger') + ->expects($this->atLeastOnce()) + ->method('warning'); + $this->mock->getParameter('appManager') + ->expects($this->once()) + ->method('isEnabledForUser') + ->willReturn(true); + $this->expectNoAccountWrite(); + + $result = $this->manager->provisionSingleUser($configs, $user); + + $this->assertFalse($result); + } + + public function testProvisionSingleUserSkipsWhenLdapUnavailable(): void { + /** @var IUser|MockObject $user */ + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com', + 'getUID' => 'bruce', + 'getBackendClassName' => 'LDAP', + ]); + $configs = [$this->ldapPlaceholderConfig()]; + $this->mock->getParameter('ldapProviderFactory') + ->method('isAvailable') + ->willReturn(false); + $this->mock->getParameter('ldapProviderFactory') + ->expects($this->never()) + ->method('getLDAPProvider'); + $this->mock->getParameter('appManager') + ->expects($this->once()) + ->method('isEnabledForUser') + ->willReturn(true); + $this->expectNoAccountWrite(); + + $result = $this->manager->provisionSingleUser($configs, $user); + + $this->assertFalse($result); + } + + public function testProvisionSingleUserNonLdapBackendSkipsLookup(): void { + /** @var IUser|MockObject $user */ + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com', + 'getUID' => 'bruce', + 'getBackendClassName' => 'Database', + ]); + $configs = [$this->ldapPlaceholderConfig()]; + $this->mock->getParameter('ldapProviderFactory') + ->method('isAvailable') + ->willReturn(true); + $this->mock->getParameter('ldapProviderFactory') + ->expects($this->never()) + ->method('getLDAPProvider'); + $this->mock->getParameter('appManager') + ->expects($this->once()) + ->method('isEnabledForUser') + ->willReturn(true); + $this->expectNoAccountWrite(); + + $result = $this->manager->provisionSingleUser($configs, $user); + + $this->assertFalse($result); + } + + public function testProvisionSingleUserWithoutLdapTokensSkipsFactory(): void { + /** @var IUser|MockObject $user */ + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com', + 'getUID' => 'bruce', + 'getBackendClassName' => 'LDAP', + ]); + $config = new Provisioning(); + $config->setId(1); + $config->setProvisioningDomain('*'); + $config->setEmailTemplate('%USERID%@batman.com'); + $configs = [$config]; + $mailAccount = new MailAccount(); + $mailAccount->setId(1000); + $this->mock->getParameter('ldapProviderFactory') + ->expects($this->never()) + ->method('isAvailable'); + $this->mock->getParameter('appManager') + ->expects($this->once()) + ->method('isEnabledForUser') + ->willReturn(true); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->once()) + ->method('findProvisionedAccount') + ->willReturn($mailAccount); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->once()) + ->method('update') + ->willReturn($mailAccount); + + $result = $this->manager->provisionSingleUser($configs, $user); + + $this->assertTrue($result); + } }