Skip to content

Commit

Permalink
Merge pull request #730 from nextcloud/enh/660/soft-auto-provisioning
Browse files Browse the repository at this point in the history
Implement soft auto provisioning
  • Loading branch information
julien-nc authored Feb 23, 2024
2 parents ba4b2b4 + 22a4cb6 commit 84f46e1
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 38 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
29 changes: 19 additions & 10 deletions lib/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
49 changes: 31 additions & 18 deletions lib/Service/ProvisioningService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
}
}

Expand Down
40 changes: 33 additions & 7 deletions lib/User/Backend.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -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)) {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
}
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 84f46e1

Please sign in to comment.