diff --git a/appinfo/app.php b/appinfo/app.php index 7f2865bd8..55f8e9ec7 100644 --- a/appinfo/app.php +++ b/appinfo/app.php @@ -58,6 +58,7 @@ \OC::$server->getLogger(), $userData, \OC::$server->query(\OCP\EventDispatcher\IEventDispatcher::class), + \OC::$server->getAvatarManager(), ); $userBackend->registerBackends(\OC::$server->getUserManager()->getBackends()); OC_User::useBackend($userBackend); diff --git a/lib/SAMLSettings.php b/lib/SAMLSettings.php index be047c111..0a190b74f 100644 --- a/lib/SAMLSettings.php +++ b/lib/SAMLSettings.php @@ -65,6 +65,7 @@ class SAMLSettings { 'saml-attribute-mapping-group_mapping', 'saml-attribute-mapping-home_mapping', 'saml-attribute-mapping-quota_mapping', + 'saml-attribute-mapping-avatar_mapping', 'sp-x509cert', 'sp-name-id-format', 'sp-privateKey', diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index 65022073b..f84123f3d 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -135,6 +135,11 @@ public function getForm() { 'type' => 'line', 'required' => true, ], + 'avatar_mapping' => [ + 'text' => $this->l10n->t('Attribute to map the users avatar to.'), + 'type' => 'line', + 'required' => true, + ], ]; diff --git a/lib/UserBackend.php b/lib/UserBackend.php index f8ec2be6a..81be24027 100644 --- a/lib/UserBackend.php +++ b/lib/UserBackend.php @@ -21,9 +21,12 @@ namespace OCA\User_SAML; +use OC\Files\Filesystem; +use OC\User\Backend; use OCP\Authentication\IApacheBackend; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\NotPermittedException; +use OCP\IAvatarManager; use OCP\IDBConnection; use OCP\ILogger; use OCP\IUser; @@ -61,6 +64,8 @@ class UserBackend implements IApacheBackend, UserInterface, IUserBackend { private $userData; /** @var IEventDispatcher */ private $eventDispatcher; + /** @var IAvatarManager */ + private $avatarManager; public function __construct( IConfig $config, @@ -72,7 +77,8 @@ public function __construct( SAMLSettings $settings, ILogger $logger, UserData $userData, - IEventDispatcher $eventDispatcher + IEventDispatcher $eventDispatcher, + IAvatarManager $avatarManager ) { $this->config = $config; $this->urlGenerator = $urlGenerator; @@ -84,6 +90,25 @@ public function __construct( $this->logger = $logger; $this->userData = $userData; $this->eventDispatcher = $eventDispatcher; + $this->avatarManager = $avatarManager; + } + + /** + * checks whether the user is allowed to change his avatar in Nextcloud + * + * @param string $uid the Nextcloud user name + * @return boolean either the user can or cannot + * @throws \Exception + */ + public function canChangeAvatar($uid) { + if (!$this->implementsActions(Backend::PROVIDE_AVATAR)) { + return true; + } + try { + return empty(trim($this->getAttributeKeys('saml-attribute-mapping-avatar_mapping')[0])); + } catch (\InvalidArgumentException $e) { + return true; + } } /** @@ -185,6 +210,7 @@ public function implementsActions($actions) { $availableActions |= \OC\User\Backend::GET_DISPLAYNAME; $availableActions |= \OC\User\Backend::GET_HOME; $availableActions |= \OC\User\Backend::COUNT_USERS; + $availableActions |= \OC\User\Backend::PROVIDE_AVATAR; return (bool)($availableActions & $actions); } @@ -642,6 +668,14 @@ public function updateAttributes($uid, $newGroups = null; } + try { + $newAvatar = $this->getAttributeValue('saml-attribute-mapping-avatar_mapping', $attributes); + $this->logger->debug('Avatar attribute content: {avatar}', ['app' => 'user_saml', 'avatar' => $newAvatar]); + } catch (\InvalidArgumentException $e) { + $this->logger->debug('Failed to fetch avatar attribute: {exception}', ['app' => 'user_saml', 'exception' => $e->getMessage()]); + $newAvatar = null; + } + if ($user !== null) { $currentEmail = (string)(method_exists($user, 'getSystemEMailAddress') ? $user->getSystemEMailAddress() : $user->getEMailAddress()); if ($newEmail !== null @@ -677,7 +711,54 @@ public function updateAttributes($uid, $groupManager->get($group)->removeUser($user); } } + + if ($newAvatar !== null) { + $image = new \OCP\Image(); + $fileData = file_get_contents($newAvatar); + $image->loadFromData($fileData); + + $checksum = md5($image->data()); + if ($checksum !== $this->config->getUserValue($uid, 'user_saml', 'lastAvatarChecksum')) { + // use the checksum before modifications + if ($this->setAvatarFromSamlProvider($uid, $image)) { + // save checksum only after successful setting + $this->config->setUserValue($uid, 'user_saml', 'lastAvatarChecksum', $checksum); + } + } + } + } + } + + private function setAvatarFromSamlProvider($uid, $image) { + if (!$image->valid()) { + $this->logger->debug('avatar image data from LDAP invalid for ' . $uid); + 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 ' . $uid); + return false; + } + + if (!Filesystem::$loaded) { + \OC_Util::setupFS($uid); + } + + try { + $avatar = $this->avatarManager->getAvatar($uid); + $avatar->set($image); + return true; + } catch (\Exception $e) { + $this->logger->logException($e, [ + 'message' => 'Could not set avatar for ' . $uid, + 'level' => ILogger::INFO, + 'app' => 'user_saml', + ]); } + return false; } diff --git a/tests/unit/Settings/AdminTest.php b/tests/unit/Settings/AdminTest.php index b7be3df20..a131cd9b5 100644 --- a/tests/unit/Settings/AdminTest.php +++ b/tests/unit/Settings/AdminTest.php @@ -147,6 +147,11 @@ public function formDataProvider() { 'type' => 'line', 'required' => true, ], + 'avatar_mapping' => [ + 'text' => $this->l10n->t('Attribute to map the users avatar to.'), + 'type' => 'line', + 'required' => true, + ], ]; $nameIdFormats = [ diff --git a/tests/unit/UserBackendTest.php b/tests/unit/UserBackendTest.php index bed31edcf..8866ea38d 100644 --- a/tests/unit/UserBackendTest.php +++ b/tests/unit/UserBackendTest.php @@ -25,6 +25,7 @@ use OCA\User_SAML\UserBackend; use OCA\User_SAML\UserData; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IAvatarManager; use OCP\IConfig; use OCP\IDBConnection; use OCP\IGroup; @@ -61,6 +62,8 @@ class UserBackendTest extends TestCase { private $logger; /** @var IEventDispatcher|MockObject */ private $eventDispatcher; + /** @var IAvatarManager|MockObject */ + private $avatarManager; protected function setUp(): void { parent::setUp(); @@ -75,6 +78,7 @@ protected function setUp(): void { $this->logger = $this->createMock(ILogger::class); $this->userData = $this->createMock(UserData::class); $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->avatarManager = $this->createMock(IAvatarManager::class); } public function getMockedBuilder(array $mockedFunctions = []) { @@ -90,7 +94,8 @@ public function getMockedBuilder(array $mockedFunctions = []) { $this->SAMLSettings, $this->logger, $this->userData, - $this->eventDispatcher + $this->eventDispatcher, + $this->avatarManager ]) ->setMethods($mockedFunctions) ->getMock(); @@ -105,7 +110,8 @@ public function getMockedBuilder(array $mockedFunctions = []) { $this->SAMLSettings, $this->logger, $this->userData, - $this->eventDispatcher + $this->eventDispatcher, + $this->avatarManager ); } }