Skip to content
Open
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 lib/SAMLSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class SAMLSettings {
'saml-attribute-mapping-mfa_mapping',
'saml-attribute-mapping-user_id_ldap_mapping',
'saml-attribute-mapping-group_mapping_prefix',
'saml-attribute-mapping-avatar_mapping',
'saml-user-filter-reject_groups',
'saml-user-filter-require_groups',
'sp-entityId',
Expand Down
5 changes: 5 additions & 0 deletions lib/Settings/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ public function getForm(): TemplateResponse {
'type' => 'line',
'required' => false,
],
'avatar_mapping' => [
'text' => $this->l10n->t('Attribute to map the users avatar to.'),
'type' => 'line',
'required' => false,
],
'user_id_ldap_mapping' => [
'text' => $this->l10n->t('Attribute to map the users to an existing LDAP user'),
'type' => 'line',
Expand Down
95 changes: 94 additions & 1 deletion lib/UserBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@

namespace OCA\User_SAML;

use OC\Files\SetupManager;
use OC\Security\CSRF\CsrfTokenManager;
use OCA\User_SAML\Model\SessionData;
use OCP\AppFramework\Services\IAppConfig;
use OCP\Authentication\IApacheBackend;
use OCP\Config\IUserConfig;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\IRootFolder;
use OCP\Files\NotPermittedException;
use OCP\IAvatarManager;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IImage;
use OCP\Image;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUser;
Expand All @@ -30,6 +35,7 @@
use OCP\User\Backend\IGetDisplayNameBackend;
use OCP\User\Backend\IGetHomeBackend;
use OCP\User\Backend\ILimitAwareCountUsersBackend;
use OCP\User\Backend\IProvideAvatarBackend;
use OCP\User\Backend\IProvideEnabledStateBackend;
use OCP\User\Backend\ISetDisplayNameBackend;
use OCP\User\Events\UserChangedEvent;
Expand All @@ -38,12 +44,23 @@
use Override;
use Psr\Log\LoggerInterface;

class UserBackend extends ABackend implements IApacheBackend, IUserBackend, IGetDisplayNameBackend, ILimitAwareCountUsersBackend, IGetHomeBackend, ICustomLogout, ISetDisplayNameBackend, IProvideEnabledStateBackend {
class UserBackend extends ABackend implements
IApacheBackend,
IUserBackend,
IGetDisplayNameBackend,
ILimitAwareCountUsersBackend,
IGetHomeBackend,
ICustomLogout,
ISetDisplayNameBackend,
IProvideEnabledStateBackend,
IProvideAvatarBackend {
/** @var \OCP\UserInterface[] */
private static array $backends = [];

/** @psalm-suppress UndefinedClass */
public function __construct(
private readonly IConfig $config,
private readonly IUserConfig $userConfig,
private readonly IAppConfig $appConfig,
private readonly IURLGenerator $urlGenerator,
private readonly ISession $session,
Expand All @@ -55,9 +72,20 @@ public function __construct(
private readonly UserData $userData,
private readonly IEventDispatcher $eventDispatcher,
private readonly string $serverRoot,
private readonly IAvatarManager $avatarManager,
private readonly SetupManager $setupManager, // TODO: replace with ISetupManager once we depends on NC34
) {
}

#[Override]
public function canChangeAvatar($uid): bool {
try {
return empty(trim($this->getAttributeKeys('saml-attribute-mapping-avatar_mapping')[0]));
} catch (\InvalidArgumentException $e) {
return true;
}
}

/**
* Whether $uid exists in the database
*/
Expand Down Expand Up @@ -523,6 +551,14 @@ public function updateAttributes(string $uid): void {
$newGroups = null;
}

try {
$newAvatar = $this->getAttributeValue('saml-attribute-mapping-avatar_mapping', $attributes);
$this->logger->debug('Avatar attribute content: {avatar}', ['avatar' => $newAvatar]);
} catch (\InvalidArgumentException $e) {
$this->logger->debug('Failed to fetch avatar attribute: {exception}', ['exception' => $e->getMessage()]);
$newAvatar = null;
}

if ($user !== null) {
$this->logger->debug('Updating attributes for existing user', ['app' => 'user_saml', 'user' => $user->getUID()]);
$currentEmail = (string)$user->getSystemEMailAddress();
Expand Down Expand Up @@ -570,7 +606,64 @@ public function updateAttributes(string $uid): void {
'user' => $user->getUID(),
'groups' => $newGroups,
]);

if ($newAvatar !== null) {
/**
* @psalm-suppress MissingDependency
* @var IImage $image
*/
$image = new Image();
$fileData = file_get_contents($newAvatar);
if ($fileData === false) {
$this->logger->warning('Unable to read content from avatar');
return;
}

$image->loadFromData($fileData);
$data = $image->data();
if ($data === null) {
$this->logger->warning('Image data is invalid');
return;
}

$checksum = md5($data);
if ($checksum !== $this->userConfig->getValueString($uid, 'user_saml', 'lastAvatarChecksum')) {
// use the checksum before modifications
if ($this->setAvatarFromSamlProvider($user, $image)) {
// save checksum only after successful setting
$this->userConfig->setValueString($uid, 'user_saml', 'lastAvatarChecksum', $checksum);
}
}
}
}
}

private function setAvatarFromSamlProvider(IUser $user, IImage $image): bool {
if (!$image->valid()) {
$this->logger->debug('avatar image data from LDAP invalid for ' . $user->getUID());
return false;
}

//make sure it is a square and not bigger than 128x128
$size = min([$image->width(), $image->height(), 128]);
if (!$image->centerCrop($size)) {
$this->logger->debug('croping image for avatar failed for ' . $user->getUID());
return false;
}

/** @psalm-suppress UndefinedClass */
$this->setupManager->setupForUser($user);

try {
$avatar = $this->avatarManager->getAvatar($user->getUID());
$avatar->set($image);
return true;
} catch (\Exception $e) {
$this->logger->info('Could not set avatar for ' . $user->getUID(), [
'exception' => $e,
]);
}
return false;
}

#[\Override]
Expand Down
1 change: 1 addition & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<file name="tests/stubs/oc_hooks_emitter.php" />
<file name="tests/stubs/oc_core_controller_clientflowloginv2controller.php" />
<file name="tests/stubs/oc_core_controller_clientflowlogincontroller.php" />
<file name="tests/stubs/oc_files_setupmanager.php" />
</stubs>
<projectFiles>
<directory name="lib" />
Expand Down
86 changes: 86 additions & 0 deletions tests/stubs/oc_files_setupmanager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OC\Files;

use OC\Files\Cache\FileAccess;
use OC\Files\Config\MountProviderCollection;
use OC\Share20\ShareDisableChecker;
use OCP\App\IAppManager;
use OCP\Diagnostics\IEventLogger;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Config\IMountProvider;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\Mount\IMountManager;
use OCP\IAppConfig;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Lockdown\ILockdownManager;
use Override;
use Psr\Log\LoggerInterface;

class SetupManager {
private const SETUP_WITH_CHILDREN = 1;
private const SETUP_WITHOUT_CHILDREN = 0;

public function __construct(
private IEventLogger $eventLogger,
private MountProviderCollection $mountProviderCollection,
private IMountManager $mountManager,
private IUserManager $userManager,
private IEventDispatcher $eventDispatcher,
private IUserMountCache $userMountCache,
private ILockdownManager $lockdownManager,
private IUserSession $userSession,
ICacheFactory $cacheFactory,
private LoggerInterface $logger,
private IConfig $config,
private ShareDisableChecker $shareDisableChecker,
private IAppManager $appManager,
private FileAccess $fileAccess,
private IAppConfig $appConfig,
) {
}

public function isSetupComplete(IUser $user): bool {
}

public function setupForUser(IUser $user): void {
}

/**
* Set up the root filesystem
*/
public function setupRoot(): void {
}

public function setupForPath(string $path, bool $includeChildren = false): void {
}

/**
* @param string $path
* @param string[] $providers
*/
public function setupForProvider(string $path, array $providers): void {
}

public function tearDown(): void {
}

/**
* Drops partially set-up mounts for the given user
*
* @param class-string<IMountProvider>[] $providers
*/
public function dropPartialMountsForUser(IUser $user, array $providers = []): void {
}
}
5 changes: 5 additions & 0 deletions tests/unit/Settings/AdminTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ public function formDataProvider(): array {
'type' => 'line',
'required' => false,
],
'avatar_mapping' => [
'text' => $this->l10n->t('Attribute to map the users avatar to.'),
'type' => 'line',
'required' => false,
],
];

$userFilterSettings = [
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/UserBackendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@

namespace OCA\User_SAML\Tests\Settings;

use OC\Files\SetupManager;
use OCA\User_SAML\GroupManager;
use OCA\User_SAML\SAMLSettings;
use OCA\User_SAML\UserBackend;
use OCA\User_SAML\UserData;
use OCP\AppFramework\Services\IAppConfig;
use OCP\Config\IUserConfig;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IAvatarManager;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\ISession;
Expand Down Expand Up @@ -45,6 +48,10 @@ class UserBackendTest extends TestCase {
private SAMLSettings&MockObject $SAMLSettings;
private LoggerInterface&MockObject $logger;
private IEventDispatcher&MockObject $eventDispatcher;
private IAvatarManager&MockObject $avatarManager;
private IUserConfig&MockObject $userConfig;
/** @psalm-suppress UndefinedClass */
private SetupManager&MockObject $setupManager;

#[Override]
protected function setUp(): void {
Expand All @@ -61,6 +68,13 @@ protected function setUp(): void {
$this->logger = $this->createMock(LoggerInterface::class);
$this->userData = $this->createMock(UserData::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->avatarManager = $this->createMock(IAvatarManager::class);
$this->userConfig = $this->createMock(IUserConfig::class);
/**
* @psalm-suppress UndefinedClass
* @psalm-suppress PropertyTypeCoercion
*/
$this->setupManager = $this->createMock(SetupManager::class);
}

/**
Expand All @@ -70,6 +84,7 @@ public function getMockedBuilder(array $mockedFunctions = []): UserBackend&MockO
return $this->getMockBuilder(UserBackend::class)
->setConstructorArgs([
$this->config,
$this->userConfig,
$this->appConfig,
$this->urlGenerator,
$this->session,
Expand All @@ -81,6 +96,8 @@ public function getMockedBuilder(array $mockedFunctions = []): UserBackend&MockO
$this->userData,
$this->eventDispatcher,
'serverRoot',
$this->avatarManager,
$this->setupManager,
])
->onlyMethods($mockedFunctions)
->getMock();
Expand All @@ -89,6 +106,7 @@ public function getMockedBuilder(array $mockedFunctions = []): UserBackend&MockO
public function getRealUserBackend(): UserBackend {
return new UserBackend(
$this->config,
$this->userConfig,
$this->appConfig,
$this->urlGenerator,
$this->session,
Expand All @@ -100,6 +118,8 @@ public function getRealUserBackend(): UserBackend {
$this->userData,
$this->eventDispatcher,
'serverRoot',
$this->avatarManager,
$this->setupManager,
);
}

Expand Down