Skip to content
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

Extends provisioning of users #502

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/build/
node_modules/
/.php_cs.cache
/.php-cs-fixer.cache
/lib/Vendor
tests/.phpunit.result.cache
/vendor-bin/mozart/vendor
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"scripts": {
"cs:fix": "php-cs-fixer fix",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"lint": "find . -name \\*.php -not -path './vendor/*' -exec php -l \"{}\" \\;",
"lint": "find . -name \\*.php ! -path './vendor/*' ! -path './vendor-bin/*' ! -path './lib/Vendor/*' -exec php -l \"{}\" \\;",
"test:unit": "phpunit -c tests/phpunit.xml",
"post-install-cmd": [
"@composer bin all install --ansi",
Expand Down
366 changes: 184 additions & 182 deletions composer.lock

Large diffs are not rendered by default.

147 changes: 35 additions & 112 deletions lib/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,14 @@
use OC\Authentication\Exceptions\InvalidTokenException;
use OC\Authentication\Token\IProvider;
use OCA\UserOIDC\Db\SessionMapper;
use OCA\UserOIDC\Event\AttributeMappedEvent;
use OCA\UserOIDC\Event\TokenObtainedEvent;
use OCA\UserOIDC\Service\DiscoveryService;
use OCA\UserOIDC\Service\LdapService;
use OCA\UserOIDC\Service\ProviderService;
use OCA\UserOIDC\Service\ProvisioningService;
use OCA\UserOIDC\Vendor\Firebase\JWT\JWT;
use OCA\UserOIDC\AppInfo\Application;
use OCA\UserOIDC\Db\ProviderMapper;
use OCA\UserOIDC\Db\UserMapper;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
Expand All @@ -56,14 +55,10 @@
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Security\ISecureRandom;
use OCP\Session\Exceptions\SessionNotAvailableException;
use OCP\User\Events\UserChangedEvent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;

class LoginController extends Controller {
private const STATE = 'oidc.state';
Expand All @@ -84,9 +79,6 @@ class LoginController extends Controller {
/** @var IURLGenerator */
private $urlGenerator;

/** @var UserMapper */
private $userMapper;

/** @var IUserSession */
private $userSession;

Expand All @@ -110,23 +102,22 @@ class LoginController extends Controller {

/** @var DiscoveryService */
private $discoveryService;
/**
* @var IConfig
*/

/** @var IConfig */
private $config;
/**
* @var LdapService
*/

/** @var LdapService */
private $ldapService;
/**
* @var IProvider
*/

/** @var IProvider */
private $authTokenProvider;
/**
* @var SessionMapper
*/

/** @var SessionMapper */
private $sessionMapper;

/** @var ProvisioningService */
private $provisioningService;

public function __construct(
IRequest $request,
ProviderMapper $providerMapper,
Expand All @@ -137,14 +128,14 @@ public function __construct(
ISession $session,
IClientService $clientService,
IURLGenerator $urlGenerator,
UserMapper $userMapper,
IUserSession $userSession,
IUserManager $userManager,
ITimeFactory $timeFactory,
IEventDispatcher $eventDispatcher,
IConfig $config,
IProvider $authTokenProvider,
SessionMapper $sessionMapper,
ProvisioningService $provisioningService,
ILogger $logger
) {
parent::__construct(Application::APP_ID, $request);
Expand All @@ -154,7 +145,6 @@ public function __construct(
$this->clientService = $clientService;
$this->discoveryService = $discoveryService;
$this->urlGenerator = $urlGenerator;
$this->userMapper = $userMapper;
$this->userSession = $userSession;
$this->userManager = $userManager;
$this->timeFactory = $timeFactory;
Expand All @@ -166,6 +156,7 @@ public function __construct(
$this->ldapService = $ldapService;
$this->authTokenProvider = $authTokenProvider;
$this->sessionMapper = $sessionMapper;
$this->provisioningService = $provisioningService;
$this->request = $request;
}

Expand Down Expand Up @@ -228,6 +219,7 @@ public function login(int $providerId, string $redirectUrl = null) {
$emailAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_EMAIL, 'email');
$displaynameAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_DISPLAYNAME, 'name');
$quotaAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_QUOTA, 'quota');
$groupsAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_GROUPS, 'groups');

$claims = [
// more details about requesting claims:
Expand All @@ -238,11 +230,13 @@ public function login(int $providerId, string $redirectUrl = null) {
$emailAttribute => null,
$displaynameAttribute => null,
$quotaAttribute => null,
$groupsAttribute => null,
],
'userinfo' => [
$emailAttribute => null,
$displaynameAttribute => null,
$quotaAttribute => null,
$groupsAttribute => null,
],
];

Expand Down Expand Up @@ -402,7 +396,24 @@ public function code(string $state = '', string $code = '', string $scope = '',
return new JSONResponse(['Failed to provision user']);
}

$user = $this->provisionUser($userId, $providerId, $idTokenPayload);
$oidcSystemConfig = $this->config->getSystemValue('user_oidc', []);
$autoProvisionAllowed = (!isset($oidcSystemConfig['auto_provision']) || $oidcSystemConfig['auto_provision']);

// Provisioning
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;
}
}

if ($user === null) {
return new JSONResponse(['Failed to provision user']);
}
Expand Down Expand Up @@ -445,94 +456,6 @@ public function code(string $state = '', string $code = '', string $scope = '',
return new RedirectResponse(\OC_Util::getDefaultPageUrl());
}

/**
* @param string $userId
* @param int $providerId
* @param object $idTokenPayload
* @return IUser|null
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function provisionUser(string $userId, int $providerId, object $idTokenPayload): ?IUser {
$oidcSystemConfig = $this->config->getSystemValue('user_oidc', []);
$autoProvisionAllowed = (!isset($oidcSystemConfig['auto_provision']) || $oidcSystemConfig['auto_provision']);
if (!$autoProvisionAllowed) {
// 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)) {
return null;
}
return $user;
}

// now we try to auto-provision

// get name/email/quota information from the token itself
$emailAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_EMAIL, 'email');
$email = $idTokenPayload->{$emailAttribute} ?? null;
$displaynameAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_DISPLAYNAME, 'name');
$userName = $idTokenPayload->{$displaynameAttribute} ?? null;
$quotaAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_QUOTA, 'quota');
$quota = $idTokenPayload->{$quotaAttribute} ?? null;

$event = new AttributeMappedEvent(ProviderService::SETTING_MAPPING_UID, $idTokenPayload, $userId);
$this->eventDispatcher->dispatchTyped($event);

$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 $user;
}

// Update displayname
if (isset($userName)) {
$newDisplayName = mb_substr($userName, 0, 255);
$event = new AttributeMappedEvent(ProviderService::SETTING_MAPPING_DISPLAYNAME, $idTokenPayload, $newDisplayName);
} else {
$event = new AttributeMappedEvent(ProviderService::SETTING_MAPPING_DISPLAYNAME, $idTokenPayload);
}
$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 !== $this->userManager->getDisplayName($user->getUID())) {
$this->eventDispatcher->dispatchTyped(new UserChangedEvent($user, 'displayName', $newDisplayName, $oldDisplayName));
}
}

// Update e-mail
$event = new AttributeMappedEvent(ProviderService::SETTING_MAPPING_EMAIL, $idTokenPayload, $email);
$this->eventDispatcher->dispatchTyped($event);
$this->logger->debug('Email mapping event dispatched');
if ($event->hasValue()) {
$user->setEMailAddress($event->getValue());
}

$event = new AttributeMappedEvent(ProviderService::SETTING_MAPPING_QUOTA, $idTokenPayload, $quota);
$this->eventDispatcher->dispatchTyped($event);
$this->logger->debug('Quota mapping event dispatched');
if ($event->hasValue()) {
$user->setQuota($event->getValue());
}

return $user;
}

/**
* Endpoint called by NC to logout in the IdP before killing the current session
*
Expand Down
26 changes: 7 additions & 19 deletions lib/Db/UserMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,22 @@

namespace OCA\UserOIDC\Db;

use OCA\UserOIDC\Service\ProviderService;
use OCA\UserOIDC\Service\LocalIdService;
use OCP\AppFramework\Db\IMapperException;
use OCP\AppFramework\Db\QBMapper;
use OCP\IDBConnection;
use OC\Cache\CappedMemoryCache;

class UserMapper extends QBMapper {
/** @var ProviderService */
private $providerService;
/** @var LocalIdService */
private $idService;

/** @var CappedMemoryCache<User> */
private $userCache;

public function __construct(IDBConnection $db, ProviderService $providerService) {
public function __construct(IDBConnection $db, LocalIdService $idService) {
parent::__construct($db, 'user_oidc', User::class);
$this->providerService = $providerService;
$this->idService = $idService;
$this->userCache = new CappedMemoryCache();
}

Expand Down Expand Up @@ -111,20 +112,7 @@ public function userExists(string $uid): bool {
}

public function getOrCreate(int $providerId, string $sub, bool $id4me = false): User {
if ($this->providerService->getSetting($providerId, ProviderService::SETTING_UNIQUE_UID, '1') === '1' || $id4me) {
$userId = $providerId . '_';

if ($id4me) {
$userId .= '1_';
} else {
$userId .= '0_';
}

$userId .= $sub;
$userId = hash('sha256', $userId);
} else {
$userId = $sub;
}
$userId = $this->idService->getId($providerId, $sub, $id4me);

if (strlen($userId) > 64) {
$userId = hash('sha256', $userId);
Expand Down
40 changes: 40 additions & 0 deletions lib/Service/LocalIdService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace OCA\UserOIDC\Service;

use OCA\UserOIDC\Db\ProviderMapper;

class LocalIdService {
/** @var ProviderService */
private $providerService;

/** @var ProviderMapper */
private $providerMapper;

public function __construct(ProviderService $providerService, ProviderMapper $providerMapper) {
$this->providerService = $providerService;
$this->providerMapper = $providerMapper;
}

public function getId(int $providerId, string $id, bool $id4me = false): string {
if ($this->providerService->getSetting($providerId, ProviderService::SETTING_UNIQUE_UID, '1') === '1' || $id4me) {
$newId = $providerId . '_';

if ($id4me) {
$newId .= '1_';
} else {
$newId .= '0_';
}

$newId .= $id;
$newId = hash('sha256', $newId);
} elseif ($this->providerService->getSetting($providerId, ProviderService::SETTING_PROVIDER_BASED_ID, '0') === '1') {
$providerName = $this->providerMapper->getProvider($providerId)->getIdentifier();
$newId = $providerName . '-' . $id;
} else {
$newId = $id;
}

return $newId;
}
}
Loading