Skip to content

Commit

Permalink
feat(gss): add global scale (gss) support
Browse files Browse the repository at this point in the history
Signed-off-by: Julien Veyssier <[email protected]>
  • Loading branch information
julien-nc committed Dec 16, 2024
1 parent 138da66 commit bb583fe
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 19 deletions.
19 changes: 18 additions & 1 deletion lib/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use OCA\UserOIDC\Service\ProvisioningService;
use OCA\UserOIDC\Service\TokenService;
use OCA\UserOIDC\Vendor\Firebase\JWT\JWT;
use OCA\UserOIDC\Vendor\Firebase\JWT\Key;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Http;
Expand Down Expand Up @@ -553,6 +554,7 @@ public function code(string $state = '', string $code = '', string $scope = '',
/**
* Endpoint called by NC to logout in the IdP before killing the current session
*
* @PublicPage
* @NoAdminRequired
* @NoCSRFRequired
* @UseSession
Expand All @@ -568,7 +570,22 @@ public function singleLogoutService() {
$oidcSystemConfig = $this->config->getSystemValue('user_oidc', []);
$targetUrl = $this->urlGenerator->getAbsoluteURL('/');
if (!isset($oidcSystemConfig['single_logout']) || $oidcSystemConfig['single_logout']) {
$providerId = $this->session->get(self::PROVIDERID);
$isFromGS = ($this->config->getSystemValueBool('gs.enabled', false)
&& $this->config->getSystemValueString('gss.mode', '') === 'master');
if ($isFromGS) {
// Request is from master GlobalScale: we get the provider ID from the JWT token provided by the slave
$jwt = $this->request->getParam('jwt', '');

try {
$key = $this->config->getSystemValueString('gss.jwt.key', '');
$decoded = (array)JWT::decode($jwt, new Key($key, 'HS256'));

$providerId = $decoded['oidcProviderId'] ?? null;
} catch (\Exception) {
}
} else {
$providerId = $this->session->get(self::PROVIDERID);
}
if ($providerId) {
try {
$provider = $this->providerMapper->getProvider((int)$providerId);
Expand Down
58 changes: 40 additions & 18 deletions lib/Service/ProvisioningService.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\Image;
use OCP\ISession;
use OCP\IUser;
use OCP\IUserManager;
use OCP\User\Events\UserChangedEvent;
Expand All @@ -37,6 +38,7 @@ public function __construct(
private IClientService $clientService,
private IAvatarManager $avatarManager,
private IConfig $config,
private ISession $session,
) {
}

Expand All @@ -49,6 +51,9 @@ public function __construct(
* @throws Exception
*/
public function provisionUser(string $tokenUserId, int $providerId, object $idTokenPayload, ?IUser $existingLocalUser = null): ?IUser {
// user data potentially later used by globalsiteselector if user_oidc is used with global scale
$oidcGssUserData = get_object_vars($idTokenPayload);

// 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 @@ -146,6 +151,7 @@ public function provisionUser(string $tokenUserId, int $providerId, object $idTo
$this->eventDispatcher->dispatchTyped($event);
$this->logger->debug('Displayname mapping event dispatched');
if ($event->hasValue() && $event->getValue() !== null && $event->getValue() !== '') {
$oidcGssUserData[$displaynameAttribute] = $event->getValue();
$newDisplayName = $event->getValue();
if ($existingLocalUser === null) {
$oldDisplayName = $backendUser->getDisplayName();
Expand All @@ -172,6 +178,7 @@ public function provisionUser(string $tokenUserId, int $providerId, object $idTo
$this->eventDispatcher->dispatchTyped($event);
$this->logger->debug('Email mapping event dispatched');
if ($event->hasValue() && $event->getValue() !== null && $event->getValue() !== '') {
$oidcGssUserData[$emailAttribute] = $event->getValue();
$user->setSystemEMailAddress($event->getValue());
}

Expand All @@ -180,12 +187,21 @@ public function provisionUser(string $tokenUserId, int $providerId, object $idTo
$this->eventDispatcher->dispatchTyped($event);
$this->logger->debug('Quota mapping event dispatched');
if ($event->hasValue() && $event->getValue() !== null && $event->getValue() !== '') {
$oidcGssUserData[$quotaAttribute] = $event->getValue();
$user->setQuota($event->getValue());
}

// Update groups
if ($this->providerService->getSetting($providerId, ProviderService::SETTING_GROUP_PROVISIONING, '0') === '1') {
$this->provisionUserGroups($user, $providerId, $idTokenPayload);
$groups = $this->provisionUserGroups($user, $providerId, $idTokenPayload);
// for gss
if ($groups !== null) {
$groupIds = array_map(static function ($group) {
return $group->gid;
}, $groups);
$groupsAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_GROUPS, 'groups');
$oidcGssUserData[$groupsAttribute] = $groupIds;
}
}

// Update the phone number
Expand Down Expand Up @@ -307,6 +323,8 @@ public function provisionUser(string $tokenUserId, int $providerId, object $idTo
$account->setProperty('gender', $event->getValue(), $scope, '1', '');
}

$this->session->set('user_oidc.oidcUserData', $oidcGssUserData);

$this->accountManager->updateAccount($account);
return $user;
}
Expand Down Expand Up @@ -430,37 +448,41 @@ public function getSyncGroupsOfToken(int $providerId, object $idTokenPayload) {
return null;
}

public function provisionUserGroups(IUser $user, int $providerId, object $idTokenPayload): void {
public function provisionUserGroups(IUser $user, int $providerId, object $idTokenPayload): ?array {
$groupsWhitelistRegex = $this->getGroupWhitelistRegex($providerId);

$syncGroups = $this->getSyncGroupsOfToken($providerId, $idTokenPayload);

if ($syncGroups !== null) {
if ($syncGroups === null) {
return null;
}

$userGroups = $this->groupManager->getUserGroups($user);
foreach ($userGroups as $group) {
if (!in_array($group->getGID(), array_column($syncGroups, 'gid'))) {
if ($groupsWhitelistRegex && !preg_match($groupsWhitelistRegex, $group->getGID())) {
continue;
}
$group->removeUser($user);
$userGroups = $this->groupManager->getUserGroups($user);
foreach ($userGroups as $group) {
if (!in_array($group->getGID(), array_column($syncGroups, 'gid'))) {
if ($groupsWhitelistRegex && !preg_match($groupsWhitelistRegex, $group->getGID())) {
continue;
}
$group->removeUser($user);
}
}

foreach ($syncGroups as $group) {
// Creates a new group or return the exiting one.
if ($newGroup = $this->groupManager->createGroup($group->gid)) {
// Adds the user to the group. Does nothing if user is already in the group.
$newGroup->addUser($user);
foreach ($syncGroups as $group) {
// Creates a new group or return the exiting one.
if ($newGroup = $this->groupManager->createGroup($group->gid)) {
// Adds the user to the group. Does nothing if user is already in the group.
$newGroup->addUser($user);

if (isset($group->displayName)) {
$newGroup->setDisplayName($group->displayName);
}
if (isset($group->displayName)) {
$newGroup->setDisplayName($group->displayName);
}
}
}

return $syncGroups;
}


public function getGroupWhitelistRegex(int $providerId): string {
$regex = $this->providerService->getSetting($providerId, ProviderService::SETTING_GROUP_WHITELIST_REGEX, '');

Expand Down
46 changes: 46 additions & 0 deletions lib/User/Backend.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,52 @@ public function getLogoutUrl(): string {
);
}

/**
* Return user data from the idp
* Inspired by user_saml
*/
public function getUserData(): array {
$userData = $this->session->get('user_oidc.oidcUserData');
$providerId = (int)$this->session->get(LoginController::PROVIDERID);
$userData = $this->formatUserData($providerId, $userData);

// make sure that a valid UID is given
if (empty($userData['formatted']['uid'])) {
$this->logger->error('No valid uid given, please check your attribute mapping. Got uid: {uid}', ['app' => 'user_oidc', 'uid' => $userData['formatted']['uid']]);
throw new \InvalidArgumentException('No valid uid given, please check your attribute mapping. Got uid: ' . $userData['formatted']['uid']);
}

return $userData;
}

/**
* Format user data and map them to the configured attributes
* Inspired by user_saml
*/
private function formatUserData(int $providerId, array $attributes): array {
$result = ['formatted' => [], 'raw' => $attributes];

$emailAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_EMAIL, 'email');
$result['formatted']['email'] = $attributes[$emailAttribute] ?? null;

$displaynameAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_DISPLAYNAME, 'name');
$result['formatted']['displayName'] = $attributes[$displaynameAttribute] ?? null;

$quotaAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_QUOTA, 'quota');
$result['formatted']['quota'] = $attributes[$quotaAttribute] ?? null;
if ($result['formatted']['quota'] === '') {
$result['formatted']['quota'] = 'default';
}

$groupsAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_GROUPS, 'groups');
$result['formatted']['groups'] = $attributes[$groupsAttribute] ?? null;

$uidAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_UID, 'sub');
$result['formatted']['uid'] = $attributes[$uidAttribute] ?? null;

return $result;
}

/**
* Return the id of the current user
* @return string
Expand Down
6 changes: 6 additions & 0 deletions tests/unit/Service/ProvisioningServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\ISession;
use OCP\IUser;
use OCP\IUserManager;
use PHPUnit\Framework\MockObject\MockObject;
Expand Down Expand Up @@ -61,6 +62,9 @@ class ProvisioningServiceTest extends TestCase {
/** @var IAvatarManager | MockObject */
private $avatarManager;

/** @var ISession | MockObject */
private $session;

public function setUp(): void {
parent::setUp();
$this->idService = $this->createMock(LocalIdService::class);
Expand All @@ -75,6 +79,7 @@ public function setUp(): void {
$this->clientService = $this->createMock(IClientService::class);
$this->avatarManager = $this->createMock(IAvatarManager::class);
$this->config = $this->createMock(IConfig::class);
$this->session = $this->createMock(ISession::class);

$this->provisioningService = new ProvisioningService(
$this->idService,
Expand All @@ -88,6 +93,7 @@ public function setUp(): void {
$this->clientService,
$this->avatarManager,
$this->config,
$this->session,
);
}

Expand Down

0 comments on commit bb583fe

Please sign in to comment.