diff --git a/README.md b/README.md index 6780a207..6cdcca34 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,29 @@ defined in the "Login Attributes" tab. In other words, make sure that your OpenID Connect provider's "User ID mapping" setting is set to an attribute which provides the same values as the LDAP attribute set in "Internal Username" in your LDAP settings. +#### Soft auto provisioning + +If you have existing users managed by another backend (local or LDAP users for example) and you want them to be managed +by user_oidc but you still want user_oidc to auto-provision users +(create new users when they are in the Oidc IdP but not found in any other user backend), +this is possible with **soft** auto provisioning. + +There is a `soft_auto_provision` system config flag that is enabled by default and is effective only if `auto_provision` +is enabled. +``` php +'user_oidc' => [ + 'auto_provision' => true, // default: true + 'soft_auto_provision' => true, // default: true +], +``` + +* When `soft_auto_provision` is enabled + * If the user already exists in another backend, we don't create a new one in the user_oidc backend. + We update the information (mapped attributes) of the existing user. + If the user does not exist in another backend, we create it in the user_oidc backend +* When `soft_auto_provision` is disabled + * We refuse Oidc login of users that already exist in other backends + ### UserInfo request for Bearer token validation The OIDC tokens used to make API call to Nextcloud might have been generated by an external entity. diff --git a/lib/Controller/LoginController.php b/lib/Controller/LoginController.php index aca8cec2..f099bfa1 100644 --- a/lib/Controller/LoginController.php +++ b/lib/Controller/LoginController.php @@ -493,19 +493,28 @@ public function code(string $state = '', string $code = '', string $scope = '', $autoProvisionAllowed = (!isset($oidcSystemConfig['auto_provision']) || $oidcSystemConfig['auto_provision']); - // Provisioning + // in case user is provisioned by user_ldap, userManager->search() triggers an ldap search which syncs the results + // so new users will be directly available even if they were not synced before this login attempt + $this->userManager->search($userId); + $this->ldapService->syncUser($userId); + $userFromOtherBackend = $this->userManager->get($userId); + if ($userFromOtherBackend !== null && $this->ldapService->isLdapDeletedUser($userFromOtherBackend)) { + $userFromOtherBackend = null; + } + if ($autoProvisionAllowed) { - $user = $this->provisioningService->provisionUser($userId, $providerId, $idTokenPayload); + $softAutoProvisionAllowed = (!isset($oidcSystemConfig['soft_auto_provision']) || $oidcSystemConfig['soft_auto_provision']); + if (!$softAutoProvisionAllowed && $userFromOtherBackend !== null) { + // if soft auto-provisioning is disabled, + // we refuse login for a user that already exists in another backend + $message = $this->l10n->t('User conflict'); + return $this->build403TemplateResponse($message, Http::STATUS_BAD_REQUEST, ['reason' => 'non-soft auto provision, user conflict'], false); + } + // use potential user from other backend, create it in our backend if it does not exist + $user = $this->provisioningService->provisionUser($userId, $providerId, $idTokenPayload, $userFromOtherBackend); } else { - // in case user is provisioned by user_ldap, userManager->search() triggers an ldap search which syncs the results - // so new users will be directly available even if they were not synced before this login attempt - $this->userManager->search($userId); - $this->ldapService->syncUser($userId); // when auto provision is disabled, we assume the user has been created by another user backend (or manually) - $user = $this->userManager->get($userId); - if ($user !== null && $this->ldapService->isLdapDeletedUser($user)) { - $user = null; - } + $user = $userFromOtherBackend; } if ($user === null) { diff --git a/lib/Service/ProvisioningService.php b/lib/Service/ProvisioningService.php index 90e6ef7f..80a52339 100644 --- a/lib/Service/ProvisioningService.php +++ b/lib/Service/ProvisioningService.php @@ -11,8 +11,6 @@ use OCP\IUser; use OCP\IUserManager; use OCP\User\Events\UserChangedEvent; -use Psr\Container\ContainerExceptionInterface; -use Psr\Container\NotFoundExceptionInterface; use Psr\Log\LoggerInterface; class ProvisioningService { @@ -65,12 +63,11 @@ public function __construct( * @param string $tokenUserId * @param int $providerId * @param object $idTokenPayload + * @param IUser|null $existingLocalUser * @return IUser|null * @throws Exception - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface */ - public function provisionUser(string $tokenUserId, int $providerId, object $idTokenPayload): ?IUser { + public function provisionUser(string $tokenUserId, int $providerId, object $idTokenPayload, ?IUser $existingLocalUser = null): ?IUser { // get name/email/quota information from the token itself $emailAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_EMAIL, 'email'); $email = $idTokenPayload->{$emailAttribute} ?? null; @@ -132,9 +129,18 @@ public function provisionUser(string $tokenUserId, int $providerId, object $idTo $event = new AttributeMappedEvent(ProviderService::SETTING_MAPPING_UID, $idTokenPayload, $tokenUserId); $this->eventDispatcher->dispatchTyped($event); - // Casting to get empty string if value is null - $backendUser = $this->userMapper->getOrCreate($providerId, $event->getValue() ?? ''); - $this->logger->debug('User obtained from the OIDC user backend: ' . $backendUser->getUserId()); + // use an existing user (from another backend) when soft auto provisioning is enabled + if ($existingLocalUser !== null) { + $user = $existingLocalUser; + } else { + $backendUser = $this->userMapper->getOrCreate($providerId, $event->getValue() ?? ''); + $this->logger->debug('User obtained from the OIDC user backend: ' . $backendUser->getUserId()); + + $user = $this->userManager->get($backendUser->getUserId()); + if ($user === null) { + return null; + } + } $user = $this->userManager->get($backendUser->getUserId()); if ($user === null) { @@ -154,17 +160,24 @@ public function provisionUser(string $tokenUserId, int $providerId, object $idTo $this->eventDispatcher->dispatchTyped($event); $this->logger->debug('Displayname mapping event dispatched'); if ($event->hasValue()) { - $oldDisplayName = $backendUser->getDisplayName(); $newDisplayName = $event->getValue(); - if ($newDisplayName !== $oldDisplayName) { - $backendUser->setDisplayName($newDisplayName); - $this->userMapper->update($backendUser); - } - // 2 reasons why we should update the display name: It does not match the one - // - of our backend - // - returned by the user manager (outdated one before the fix in https://github.com/nextcloud/user_oidc/pull/530) - if ($newDisplayName !== $oldDisplayName || $newDisplayName !== $user->getDisplayName()) { - $this->eventDispatcher->dispatchTyped(new UserChangedEvent($user, 'displayName', $newDisplayName, $oldDisplayName)); + if ($existingLocalUser === null) { + $oldDisplayName = $backendUser->getDisplayName(); + if ($newDisplayName !== $oldDisplayName) { + $backendUser->setDisplayName($newDisplayName); + $this->userMapper->update($backendUser); + } + // 2 reasons why we should update the display name: It does not match the one + // - of our backend + // - returned by the user manager (outdated one before the fix in https://github.com/nextcloud/user_oidc/pull/530) + if ($newDisplayName !== $oldDisplayName || $newDisplayName !== $user->getDisplayName()) { + $this->eventDispatcher->dispatchTyped(new UserChangedEvent($user, 'displayName', $newDisplayName, $oldDisplayName)); + } + } else { + $oldDisplayName = $user->getDisplayName(); + if ($newDisplayName !== $oldDisplayName) { + $user->setDisplayName($newDisplayName); + } } } diff --git a/lib/User/Backend.php b/lib/User/Backend.php index a49e2765..a0b55b55 100644 --- a/lib/User/Backend.php +++ b/lib/User/Backend.php @@ -269,15 +269,36 @@ public function getCurrentUserId(): string { $this->eventDispatcher->dispatchTyped(new TokenValidatedEvent(['token' => $headerToken], $provider, $discovery)); if ($autoProvisionAllowed) { - $backendUser = $this->userMapper->getOrCreate($provider->getId(), $tokenUserId); - $userId = $backendUser->getUserId(); + // look for user in other backends + if (!$this->userManager->userExists($tokenUserId)) { + $this->userManager->search($tokenUserId); + $this->ldapService->syncUser($tokenUserId); + } + $userFromOtherBackend = $this->userManager->get($tokenUserId); + if ($userFromOtherBackend !== null && $this->ldapService->isLdapDeletedUser($userFromOtherBackend)) { + $userFromOtherBackend = null; + } + + $softAutoProvisionAllowed = (!isset($oidcSystemConfig['soft_auto_provision']) || $oidcSystemConfig['soft_auto_provision']); + if (!$softAutoProvisionAllowed && $userFromOtherBackend !== null) { + // if soft auto-provisioning is disabled, + // we refuse login for a user that already exists in another backend + return ''; + } + if ($userFromOtherBackend === null) { + // only create the user in our backend if the user does not exist in another backend + $backendUser = $this->userMapper->getOrCreate($provider->getId(), $tokenUserId); + $userId = $backendUser->getUserId(); + } else { + $userId = $userFromOtherBackend->getUID(); + } $this->checkFirstLogin($userId); if ($this->providerService->getSetting($provider->getId(), ProviderService::SETTING_BEARER_PROVISIONING, '0') === '1') { $provisioningStrategy = $validator->getProvisioningStrategy(); if ($provisioningStrategy) { - $this->provisionUser($validator->getProvisioningStrategy(), $provider, $tokenUserId, $headerToken); + $this->provisionUser($validator->getProvisioningStrategy(), $provider, $tokenUserId, $headerToken, $userFromOtherBackend); } } @@ -291,6 +312,7 @@ public function getCurrentUserId(): string { // to get the user if it has not been synced yet if (!$this->userManager->userExists($tokenUserId)) { $this->userManager->search($tokenUserId); + $this->ldapService->syncUser($tokenUserId); // return nothing, if the user was not found after the user_ldap search if (!$this->userManager->userExists($tokenUserId)) { @@ -299,7 +321,7 @@ public function getCurrentUserId(): string { } $user = $this->userManager->get($tokenUserId); - if ($this->ldapService->isLdapDeletedUser($user)) { + if ($user === null || $this->ldapService->isLdapDeletedUser($user)) { return ''; } $this->checkFirstLogin($tokenUserId); @@ -351,13 +373,17 @@ private function checkFirstLogin(string $userId): bool { * Triggers user provisioning based on the provided strategy * * @param string $provisioningStrategyClass - * @param string $tokenUserId * @param Provider $provider + * @param string $tokenUserId * @param string $headerToken + * @param IUser|null $userFromOtherBackend * @return IUser|null */ - private function provisionUser(string $provisioningStrategyClass, Provider $provider, string $tokenUserId, string $headerToken): ?IUser { + private function provisionUser( + string $provisioningStrategyClass, Provider $provider, string $tokenUserId, string $headerToken, + ?IUser $userFromOtherBackend + ): ?IUser { $provisioningStrategy = \OC::$server->get($provisioningStrategyClass); - return $provisioningStrategy->provisionUser($provider, $tokenUserId, $headerToken); + return $provisioningStrategy->provisionUser($provider, $tokenUserId, $headerToken, $userFromOtherBackend); } } diff --git a/lib/User/Provisioning/IProvisioningStrategy.php b/lib/User/Provisioning/IProvisioningStrategy.php index 6e1a1421..628c5dcc 100644 --- a/lib/User/Provisioning/IProvisioningStrategy.php +++ b/lib/User/Provisioning/IProvisioningStrategy.php @@ -13,7 +13,8 @@ interface IProvisioningStrategy { * @param Provider $provider * @param string $tokenUserId * @param string $bearerToken + * @param IUser|null $userFromOtherBackend * @return IUser|null */ - public function provisionUser(Provider $provider, string $tokenUserId, string $bearerToken): ?IUser; + public function provisionUser(Provider $provider, string $tokenUserId, string $bearerToken, ?IUser $userFromOtherBackend): ?IUser; } diff --git a/lib/User/Provisioning/SelfEncodedTokenProvisioning.php b/lib/User/Provisioning/SelfEncodedTokenProvisioning.php index d45c1fab..ba958048 100644 --- a/lib/User/Provisioning/SelfEncodedTokenProvisioning.php +++ b/lib/User/Provisioning/SelfEncodedTokenProvisioning.php @@ -27,7 +27,7 @@ public function __construct(ProvisioningService $provisioningService, DiscoveryS $this->logger = $logger; } - public function provisionUser(Provider $provider, string $tokenUserId, string $bearerToken): ?IUser { + public function provisionUser(Provider $provider, string $tokenUserId, string $bearerToken, ?IUser $userFromOtherBackend): ?IUser { JWT::$leeway = 60; try { $jwks = $this->discoveryService->obtainJWK($provider, $bearerToken); @@ -37,6 +37,6 @@ public function provisionUser(Provider $provider, string $tokenUserId, string $b return null; } - return $this->provisioningService->provisionUser($tokenUserId, $provider->getId(), $payload); + return $this->provisioningService->provisionUser($tokenUserId, $provider->getId(), $payload, $userFromOtherBackend); } }