Skip to content

Commit

Permalink
implement soft auto provisioning: allow users from other backends to …
Browse files Browse the repository at this point in the history
…login, update their info

Signed-off-by: Julien Veyssier <[email protected]>
  • Loading branch information
julien-nc committed Dec 8, 2023
1 parent efb066e commit 5111947
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 39 deletions.
31 changes: 20 additions & 11 deletions lib/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -483,19 +483,28 @@ public function code(string $state = '', string $code = '', string $scope = '',
$oidcSystemConfig = $this->config->getSystemValue('user_oidc', []);
$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);
// when auto provision is disabled, we assume the user has been created by another user backend (or manually)
$userFromOtherBackend = $this->userManager->get($userId);
if ($this->ldapService->isLdapDeletedUser($userFromOtherBackend)) {
$userFromOtherBackend = null;
}

if ($autoProvisionAllowed) {
$user = $this->provisioningService->provisionUser($userId, $providerId, $idTokenPayload);
} 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 ($this->ldapService->isLdapDeletedUser($user)) {
$user = 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
$message = $this->l10n->t('User conflict');
return $this->build403TemplateResponse($message, Http::STATUS_BAD_REQUEST, ['reason' => 'non-soft auto provision, user conflict']);
}
// use potential user from other backend, create it in out backend if it does not exist
$user = $this->provisioningService->provisionUser($userId, $providerId, $idTokenPayload, $userFromOtherBackend);
} else {
$user = $userFromOtherBackend;
}

if ($user === null) {
Expand Down
46 changes: 27 additions & 19 deletions lib/Service/ProvisioningService.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,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;
Expand All @@ -75,12 +74,17 @@ public function provisionUser(string $tokenUserId, int $providerId, object $idTo
$event = new AttributeMappedEvent(ProviderService::SETTING_MAPPING_UID, $idTokenPayload, $tokenUserId);
$this->eventDispatcher->dispatchTyped($event);

$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) {
return null;
}
}

// Update displayname
Expand All @@ -93,17 +97,21 @@ 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();
$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));
}
} else {
$user->setDisplayName($newDisplayName);
}
}

Expand Down
36 changes: 30 additions & 6 deletions lib/User/Backend.php
Original file line number Diff line number Diff line change
Expand Up @@ -262,15 +262,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 ($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);
}
}

Expand All @@ -284,6 +305,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)) {
Expand Down Expand Up @@ -344,13 +366,15 @@ 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);
}
}
3 changes: 2 additions & 1 deletion lib/User/Provisioning/IProvisioningStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 2 additions & 2 deletions lib/User/Provisioning/SelfEncodedTokenProvisioning.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
}

0 comments on commit 5111947

Please sign in to comment.