Browse Source

Allow to tweak default scopes for accounts

Close #6582

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
tags/v25.0.0beta1
Thomas Citharel 2 years ago
parent
commit
4d26a9afa0
No account linked to committer's email address

+ 14
- 0
config/config.sample.php View File

* the database storage. * the database storage.
*/ */
'enable_file_metadata' => true, 'enable_file_metadata' => true,

/**
* Allows to override the default scopes for Account data.
* The list of overridable properties and valid values for scopes are in
* OCP\Accounts\IAccountManager. Values added here are merged with
* default values, which are in OC\Accounts\AccountManager
*
* For instance, if the phone property should default to the private scope
* instead of the local one:
* [
* \OCP\Accounts\IAccountManager::PROPERTY_PHONE => \OCP\Accounts\IAccountManager::SCOPE_PRIVATE
* ]
*/
'account_manager.default_property_scope' => []
]; ];

+ 37
- 25
lib/private/Accounts/AccountManager.php View File

* @author Lukas Reschke <lukas@statuscode.ch> * @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de> * @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl> * @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
* @author Vincent Petry <vincent@nextcloud.com> * @author Vincent Petry <vincent@nextcloud.com>
* *
* @license AGPL-3.0 * @license AGPL-3.0
private $l10nfactory; private $l10nfactory;
private CappedMemoryCache $internalCache; private CappedMemoryCache $internalCache;


/**
* The list of default scopes for each property.
*/
public const DEFAULT_SCOPES = [
self::PROPERTY_DISPLAYNAME => self::SCOPE_FEDERATED,
self::PROPERTY_ADDRESS => self::SCOPE_LOCAL,
self::PROPERTY_WEBSITE => self::SCOPE_LOCAL,
self::PROPERTY_EMAIL => self::SCOPE_FEDERATED,
self::PROPERTY_AVATAR => self::SCOPE_FEDERATED,
self::PROPERTY_PHONE => self::SCOPE_LOCAL,
self::PROPERTY_TWITTER => self::SCOPE_LOCAL,
self::PROPERTY_ORGANISATION => self::SCOPE_LOCAL,
self::PROPERTY_ROLE => self::SCOPE_LOCAL,
self::PROPERTY_HEADLINE => self::SCOPE_LOCAL,
self::PROPERTY_BIOGRAPHY => self::SCOPE_LOCAL,
];

public function __construct( public function __construct(
IDBConnection $connection, IDBConnection $connection,
IConfig $config, IConfig $config,


/** /**
* build default user record in case not data set exists yet * build default user record in case not data set exists yet
*
* @param IUser $user
* @return array
*/ */
protected function buildDefaultUserRecord(IUser $user) {
protected function buildDefaultUserRecord(IUser $user): array {
$scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) {
return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true);
}, ARRAY_FILTER_USE_BOTH));

return [ return [
[ [
'name' => self::PROPERTY_DISPLAYNAME, 'name' => self::PROPERTY_DISPLAYNAME,
'value' => $user->getDisplayName(), 'value' => $user->getDisplayName(),
'scope' => self::SCOPE_FEDERATED,
// Display name must be at least SCOPE_LOCAL
'scope' => $scopes[self::PROPERTY_DISPLAYNAME] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_DISPLAYNAME],
'verified' => self::NOT_VERIFIED, 'verified' => self::NOT_VERIFIED,
], ],


[ [
'name' => self::PROPERTY_ADDRESS, 'name' => self::PROPERTY_ADDRESS,
'value' => '', 'value' => '',
'scope' => self::SCOPE_LOCAL,
'scope' => $scopes[self::PROPERTY_ADDRESS],
'verified' => self::NOT_VERIFIED, 'verified' => self::NOT_VERIFIED,
], ],


[ [
'name' => self::PROPERTY_WEBSITE, 'name' => self::PROPERTY_WEBSITE,
'value' => '', 'value' => '',
'scope' => self::SCOPE_LOCAL,
'scope' => $scopes[self::PROPERTY_WEBSITE],
'verified' => self::NOT_VERIFIED, 'verified' => self::NOT_VERIFIED,
], ],


[ [
'name' => self::PROPERTY_EMAIL, 'name' => self::PROPERTY_EMAIL,
'value' => $user->getEMailAddress(), 'value' => $user->getEMailAddress(),
'scope' => self::SCOPE_FEDERATED,
// Email must be at least SCOPE_LOCAL
'scope' => $scopes[self::PROPERTY_EMAIL] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_EMAIL],
'verified' => self::NOT_VERIFIED, 'verified' => self::NOT_VERIFIED,
], ],


[ [
'name' => self::PROPERTY_AVATAR, 'name' => self::PROPERTY_AVATAR,
'scope' => self::SCOPE_FEDERATED
'scope' => $scopes[self::PROPERTY_AVATAR],
], ],


[ [
'name' => self::PROPERTY_PHONE, 'name' => self::PROPERTY_PHONE,
'value' => '', 'value' => '',
'scope' => self::SCOPE_LOCAL,
'scope' => $scopes[self::PROPERTY_PHONE],
'verified' => self::NOT_VERIFIED, 'verified' => self::NOT_VERIFIED,
], ],


[ [
'name' => self::PROPERTY_TWITTER, 'name' => self::PROPERTY_TWITTER,
'value' => '', 'value' => '',
'scope' => self::SCOPE_LOCAL,
'scope' => $scopes[self::PROPERTY_TWITTER],
'verified' => self::NOT_VERIFIED, 'verified' => self::NOT_VERIFIED,
], ],


[ [
'name' => self::PROPERTY_ORGANISATION, 'name' => self::PROPERTY_ORGANISATION,
'value' => '', 'value' => '',
'scope' => self::SCOPE_LOCAL,
'scope' => $scopes[self::PROPERTY_ORGANISATION],
], ],


[ [
'name' => self::PROPERTY_ROLE, 'name' => self::PROPERTY_ROLE,
'value' => '', 'value' => '',
'scope' => self::SCOPE_LOCAL,
'scope' => $scopes[self::PROPERTY_ROLE],
], ],


[ [
'name' => self::PROPERTY_HEADLINE, 'name' => self::PROPERTY_HEADLINE,
'value' => '', 'value' => '',
'scope' => self::SCOPE_LOCAL,
'scope' => $scopes[self::PROPERTY_HEADLINE],
], ],


[ [
'name' => self::PROPERTY_BIOGRAPHY, 'name' => self::PROPERTY_BIOGRAPHY,
'value' => '', 'value' => '',
'scope' => self::SCOPE_LOCAL,
'scope' => $scopes[self::PROPERTY_BIOGRAPHY],
], ],


[ [
// valid case, nothing to do // valid case, nothing to do
} }


static $allowedScopes = [
self::SCOPE_PRIVATE,
self::SCOPE_LOCAL,
self::SCOPE_FEDERATED,
self::SCOPE_PUBLISHED,
self::VISIBILITY_PRIVATE,
self::VISIBILITY_CONTACTS_ONLY,
self::VISIBILITY_PUBLIC,
];
foreach ($account->getAllProperties() as $property) { foreach ($account->getAllProperties() as $property) {
$this->testPropertyScope($property, $allowedScopes, true);
$this->testPropertyScope($property, self::ALLOWED_SCOPES, true);
} }


$oldData = $this->getUser($account->getUser(), false); $oldData = $this->getUser($account->getUser(), false);

+ 36
- 0
lib/public/Accounts/IAccountManager.php View File

* @author Christoph Wurst <christoph@winzerhof-wurst.at> * @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com> * @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net> * @author Julius Härtl <jus@bitgrid.net>
* @author Thomas Citharel <nextcloud@tcit.fr>
* @author Vincent Petry <vincent@nextcloud.com> * @author Vincent Petry <vincent@nextcloud.com>
* *
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
*/ */
public const VISIBILITY_PUBLIC = 'public'; public const VISIBILITY_PUBLIC = 'public';


/**
* The list of allowed scopes
*
* @since 25.0.0
*/
public const ALLOWED_SCOPES = [
self::SCOPE_PRIVATE,
self::SCOPE_LOCAL,
self::SCOPE_FEDERATED,
self::SCOPE_PUBLISHED,
self::VISIBILITY_PRIVATE,
self::VISIBILITY_CONTACTS_ONLY,
self::VISIBILITY_PUBLIC,
];

public const PROPERTY_AVATAR = 'avatar'; public const PROPERTY_AVATAR = 'avatar';
public const PROPERTY_DISPLAYNAME = 'displayname'; public const PROPERTY_DISPLAYNAME = 'displayname';
public const PROPERTY_PHONE = 'phone'; public const PROPERTY_PHONE = 'phone';
*/ */
public const PROPERTY_PROFILE_ENABLED = 'profile_enabled'; public const PROPERTY_PROFILE_ENABLED = 'profile_enabled';


/**
* The list of allowed properties
*
* @since 25.0.0
*/
public const ALLOWED_PROPERTIES = [
self::PROPERTY_AVATAR,
self::PROPERTY_DISPLAYNAME,
self::PROPERTY_PHONE,
self::PROPERTY_EMAIL,
self::PROPERTY_WEBSITE,
self::PROPERTY_ADDRESS,
self::PROPERTY_TWITTER,
self::PROPERTY_ORGANISATION,
self::PROPERTY_ROLE,
self::PROPERTY_HEADLINE,
self::PROPERTY_BIOGRAPHY,
self::PROPERTY_PROFILE_ENABLED,
];

public const COLLECTION_EMAIL = 'additional_mail'; public const COLLECTION_EMAIL = 'additional_mail';


public const NOT_VERIFIED = '0'; public const NOT_VERIFIED = '0';

+ 71
- 33
tests/lib/Accounts/AccountManagerTest.php View File

<?php <?php


/** /**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Björn Schießle <schiessle@owncloud.com> * @author Björn Schießle <schiessle@owncloud.com>
* @author Thomas Citharel <nextcloud@tcit.fr>
* *
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @license AGPL-3.0 * @license AGPL-3.0
* *
* This code is free software: you can redistribute it and/or modify * This code is free software: you can redistribute it and/or modify
/** @var IFactory|MockObject */ /** @var IFactory|MockObject */
protected $l10nFactory; protected $l10nFactory;


/** @var \OCP\IDBConnection */
/** @var IDBConnection */
private $connection; private $connection;


/** @var IConfig|MockObject */
/** @var IConfig|MockObject */
private $config; private $config;


/** @var EventDispatcherInterface|MockObject */ /** @var EventDispatcherInterface|MockObject */
private $eventDispatcher; private $eventDispatcher;


/** @var IJobList|MockObject */
/** @var IJobList|MockObject */
private $jobList; private $jobList;


/** @var string accounts table name */
private $table = 'accounts';
/** accounts table name */
private string $table = 'accounts';


/** @var LoggerInterface|MockObject */ /** @var LoggerInterface|MockObject */
private $logger; private $logger;


/** @var AccountManager */
private $accountManager;
private AccountManager $accountManager;


protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
protected function tearDown(): void { protected function tearDown(): void {
parent::tearDown(); parent::tearDown();
$query = $this->connection->getQueryBuilder(); $query = $this->connection->getQueryBuilder();
$query->delete($this->table)->execute();
$query->delete($this->table)->executeStatement();
} }


protected function makeUser(string $uid, string $name, string $email = null): IUser { protected function makeUser(string $uid, string $name, string $email = null): IUser {
], ],
], ],
]; ];
$this->config->expects($this->exactly(count($users)))->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn([]);
foreach ($users as $userInfo) { foreach ($users as $userInfo) {
$this->invokePrivate($this->accountManager, 'updateUser', [$userInfo['user'], $userInfo['data'], null, false]); $this->invokePrivate($this->accountManager, 'updateUser', [$userInfo['user'], $userInfo['data'], null, false]);
} }
/** /**
* get a instance of the accountManager * get a instance of the accountManager
* *
* @param array $mockedMethods list of methods which should be mocked
* @return MockObject | AccountManager * @return MockObject | AccountManager
*/ */
public function getInstance($mockedMethods = null) {
public function getInstance(?array $mockedMethods = null) {
return $this->getMockBuilder(AccountManager::class) return $this->getMockBuilder(AccountManager::class)
->setConstructorArgs([ ->setConstructorArgs([
$this->connection, $this->connection,
$this->urlGenerator, $this->urlGenerator,
$this->crypto $this->crypto
]) ])
->setMethods($mockedMethods)
->onlyMethods($mockedMethods)
->getMock(); ->getMock();
} }


/** /**
* @dataProvider dataTrueFalse * @dataProvider dataTrueFalse
* *
* @param array $newData
* @param array $oldData
* @param bool $insertNew
* @param bool $updateExisting
*/ */
public function testUpdateUser($newData, $oldData, $insertNew, $updateExisting) {
public function testUpdateUser(array $newData, array $oldData, bool $insertNew, bool $updateExisting) {
$accountManager = $this->getInstance(['getUser', 'insertNewUser', 'updateExistingUser']); $accountManager = $this->getInstance(['getUser', 'insertNewUser', 'updateExistingUser']);
/** @var IUser $user */ /** @var IUser $user */
$user = $this->createMock(IUser::class); $user = $this->createMock(IUser::class);
function ($eventName, $event) use ($user, $newData) { function ($eventName, $event) use ($user, $newData) {
$this->assertSame('OC\AccountManager::userUpdated', $eventName); $this->assertSame('OC\AccountManager::userUpdated', $eventName);
$this->assertInstanceOf(GenericEvent::class, $event); $this->assertInstanceOf(GenericEvent::class, $event);
/** @var GenericEvent $event */
$this->assertSame($user, $event->getSubject()); $this->assertSame($user, $event->getSubject());
$this->assertSame($newData, $event->getArguments()); $this->assertSame($newData, $event->getArguments());
} }
'value' => '1', 'value' => '1',
], ],
]; ];
$this->config->expects($this->once())->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn([]);


$defaultUserRecord = $this->invokePrivate($this->accountManager, 'buildDefaultUserRecord', [$user]); $defaultUserRecord = $this->invokePrivate($this->accountManager, 'buildDefaultUserRecord', [$user]);
$result = $this->invokePrivate($this->accountManager, 'addMissingDefaultValues', [$input, $defaultUserRecord]); $result = $this->invokePrivate($this->accountManager, 'addMissingDefaultValues', [$input, $defaultUserRecord]);
$this->assertSame($expected, $result); $this->assertSame($expected, $result);
} }


private function addDummyValuesToTable($uid, $data) {
$query = $this->connection->getQueryBuilder();
$query->insert($this->table)
->values(
[
'uid' => $query->createNamedParameter($uid),
'data' => $query->createNamedParameter(json_encode($data)),
]
)
->execute();
}

public function testGetAccount() { public function testGetAccount() {
$accountManager = $this->getInstance(['getUser']); $accountManager = $this->getInstance(['getUser']);
/** @var IUser $user */ /** @var IUser $user */


/** /**
* @dataProvider dataParsePhoneNumber * @dataProvider dataParsePhoneNumber
* @param string $phoneInput
* @param string $defaultRegion
* @param string|null $phoneNumber
*/ */
public function testParsePhoneNumber(string $phoneInput, string $defaultRegion, ?string $phoneNumber): void { public function testParsePhoneNumber(string $phoneInput, string $defaultRegion, ?string $phoneNumber): void {
$this->config->method('getSystemValueString') $this->config->method('getSystemValueString')
* @dataProvider dataCheckEmailVerification * @dataProvider dataCheckEmailVerification
*/ */
public function testCheckEmailVerification(IUser $user, ?string $newEmail): void { public function testCheckEmailVerification(IUser $user, ?string $newEmail): void {
// Once because of getAccount, once because of getUser
$this->config->expects($this->exactly(2))->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn([]);
$account = $this->accountManager->getAccount($user); $account = $this->accountManager->getAccount($user);
$emailUpdated = false; $emailUpdated = false;


$oldData = $this->invokePrivate($this->accountManager, 'getUser', [$user, false]); $oldData = $this->invokePrivate($this->accountManager, 'getUser', [$user, false]);
$this->invokePrivate($this->accountManager, 'checkEmailVerification', [$account, $oldData]); $this->invokePrivate($this->accountManager, 'checkEmailVerification', [$account, $oldData]);
} }

public function dataSetDefaultPropertyScopes(): array {
return [
[
[],
[
IAccountManager::PROPERTY_DISPLAYNAME => IAccountManager::SCOPE_FEDERATED,
IAccountManager::PROPERTY_ADDRESS => IAccountManager::SCOPE_LOCAL,
IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_FEDERATED,
IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_LOCAL,
]
],
[
[
IAccountManager::PROPERTY_DISPLAYNAME => IAccountManager::SCOPE_FEDERATED,
IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_LOCAL,
IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE,
], [
IAccountManager::PROPERTY_DISPLAYNAME => IAccountManager::SCOPE_FEDERATED,
IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_LOCAL,
IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE,
]
],
[
[
IAccountManager::PROPERTY_ADDRESS => 'invalid scope',
'invalid property' => IAccountManager::SCOPE_LOCAL,
IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE,
],
[
IAccountManager::PROPERTY_ADDRESS => IAccountManager::SCOPE_LOCAL,
IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_FEDERATED,
IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE,
]
],
];
}

/**
* @dataProvider dataSetDefaultPropertyScopes
*/
public function testSetDefaultPropertyScopes(array $propertyScopes, array $expectedResultScopes): void {
$user = $this->makeUser('steve', 'Steve Smith', 'steve@steve.steve');
$this->config->expects($this->once())->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn($propertyScopes);

$result = $this->invokePrivate($this->accountManager, 'buildDefaultUserRecord', [$user]);
$resultProperties = array_column($result, 'name');

$this->assertEmpty(array_diff($resultProperties, IAccountManager::ALLOWED_PROPERTIES), "Building default user record returned non-allowed properties");
foreach ($expectedResultScopes as $expectedResultScopeKey => $expectedResultScopeValue) {
$resultScope = $result[array_search($expectedResultScopeKey, $resultProperties)]['scope'];
$this->assertEquals($expectedResultScopeValue, $resultScope, "The result scope doesn't follow the value set into the config or defaults correctly.");
}
}
} }

Loading…
Cancel
Save