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

Add global scale (gss) support #1011

Merged
merged 3 commits into from
Jan 15, 2025
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
26 changes: 25 additions & 1 deletion lib/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
use OCA\UserOIDC\Service\ProviderService;
use OCA\UserOIDC\Service\ProvisioningService;
use OCA\UserOIDC\Service\TokenService;
use OCA\UserOIDC\User\Backend;
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 All @@ -47,6 +49,8 @@
use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom;
use OCP\Session\Exceptions\SessionNotAvailableException;
use OCP\User\Events\BeforeUserLoggedInEvent;
use OCP\User\Events\UserLoggedInEvent;
use Psr\Log\LoggerInterface;

class LoginController extends BaseOidcController {
Expand Down Expand Up @@ -509,6 +513,9 @@ public function code(string $state = '', string $code = '', string $scope = '',
$this->userSession->completeLogin($user, ['loginName' => $user->getUID(), 'password' => '']);
$this->userSession->createSessionToken($this->request, $user->getUID(), $user->getUID());
$this->userSession->createRememberMeToken($user);
// TODO server should/could be refactored so we don't need to manually create the user session and dispatch the login-related events
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you file an issue in server and reference that? Helps to keep track of other cases or considerations around that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue filed: nextcloud/server#50194

$this->eventDispatcher->dispatchTyped(new BeforeUserLoggedInEvent($user->getUID(), null, \OC::$server->get(Backend::class)));
$this->eventDispatcher->dispatchTyped(new UserLoggedInEvent($user, $user->getUID(), null, false));
}

// store all token information for potential token exchange requests
Expand Down Expand Up @@ -554,6 +561,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 @@ -569,7 +577,23 @@ 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 $e) {
$this->logger->debug('Failed to get the logout provider ID in the request from GSS', ['exception' => $e]);
}
} 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 @@ -19,6 +19,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 @@ -39,6 +40,7 @@ public function __construct(
private IClientService $clientService,
private IAvatarManager $avatarManager,
private IConfig $config,
private ISession $session,
) {
}

Expand All @@ -60,6 +62,9 @@ public function hasOidcUserProvisitioned(string $userId): bool {
* @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 @@ -157,6 +162,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 @@ -183,6 +189,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 @@ -191,12 +198,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 @@ -318,6 +334,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 @@ -441,37 +459,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 {
julien-nc marked this conversation as resolved.
Show resolved Hide resolved
$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
Loading