Skip to content

Commit 02ce5c8

Browse files
committed
Respect user enumeration settings in user status lists
So far, the functions to find user statuses listed didn't respect user enumeration settings (`shareapi_allow_share_dialog_user_enumeration` and `shareapi_restrict_user_enumeration_to_group` core app settings). Fix this privacy issue by returning an empty list in case `shareapi_allow_share_dialog_user_enumeration` is unset or `shareapi_restrict_user_enumeration_to_group` is set. In the long run, we might want to return users from common groups if `shareapi_restrict_user_enumeration_to_group` is set. It's complicated to implement this in a way that scales, though. See the discussion at #27879 (review) for details. Also, don't register the user_status dashboard widget at all if `shareapi_allow_share_dialog_user_enumeration` is unset or `shareapi_restrict_user_enumeration_to_group` is set. Fixes: #27122 Signed-off-by: Jonas Meurer <[email protected]>
1 parent 082d429 commit 02ce5c8

File tree

3 files changed

+101
-7
lines changed

3 files changed

+101
-7
lines changed

apps/user_status/lib/AppInfo/Application.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use OCP\AppFramework\Bootstrap\IBootstrap;
3737
use OCP\AppFramework\Bootstrap\IRegistrationContext;
3838
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
39+
use OCP\IConfig;
3940
use OCP\User\Events\UserDeletedEvent;
4041
use OCP\User\Events\UserLiveStatusEvent;
4142
use OCP\UserStatus\IManager;
@@ -71,8 +72,15 @@ public function register(IRegistrationContext $context): void {
7172
$context->registerEventListener(UserLiveStatusEvent::class, UserLiveStatusListener::class);
7273
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
7374

74-
// Register the Dashboard panel
75-
$context->registerDashboardWidget(UserStatusWidget::class);
75+
$config = $this->getContainer()->query(IConfig::class);
76+
$shareeEnumeration = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
77+
$shareeEnumerationInGroupOnly = $shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
78+
$shareeEnumerationPhone = $shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
79+
80+
// Register the Dashboard panel if user enumeration is enabled and not limited
81+
if ($shareeEnumeration && !$shareeEnumerationInGroupOnly && !$shareeEnumerationPhone) {
82+
$context->registerDashboardWidget(UserStatusWidget::class);
83+
}
7684
}
7785

7886
public function boot(IBootContext $context): void {

apps/user_status/lib/Service/StatusService.php

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use OCA\UserStatus\Exception\StatusMessageTooLongException;
3535
use OCP\AppFramework\Db\DoesNotExistException;
3636
use OCP\AppFramework\Utility\ITimeFactory;
37+
use OCP\IConfig;
3738
use OCP\UserStatus\IUserStatus;
3839

3940
/**
@@ -55,6 +56,15 @@ class StatusService {
5556
/** @var EmojiService */
5657
private $emojiService;
5758

59+
/** @var bool */
60+
private $shareeEnumeration;
61+
62+
/** @var bool */
63+
private $shareeEnumerationInGroupOnly;
64+
65+
/** @var bool */
66+
private $shareeEnumerationPhone;
67+
5868
/**
5969
* List of priorities ordered by their priority
6070
*/
@@ -87,17 +97,22 @@ class StatusService {
8797
*
8898
* @param UserStatusMapper $mapper
8999
* @param ITimeFactory $timeFactory
90-
* @param PredefinedStatusService $defaultStatusService,
100+
* @param PredefinedStatusService $defaultStatusService
91101
* @param EmojiService $emojiService
102+
* @param IConfig $config
92103
*/
93104
public function __construct(UserStatusMapper $mapper,
94105
ITimeFactory $timeFactory,
95106
PredefinedStatusService $defaultStatusService,
96-
EmojiService $emojiService) {
107+
EmojiService $emojiService,
108+
IConfig $config) {
97109
$this->mapper = $mapper;
98110
$this->timeFactory = $timeFactory;
99111
$this->predefinedStatusService = $defaultStatusService;
100112
$this->emojiService = $emojiService;
113+
$this->shareeEnumeration = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
114+
$this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
115+
$this->shareeEnumerationPhone = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
101116
}
102117

103118
/**
@@ -106,6 +121,13 @@ public function __construct(UserStatusMapper $mapper,
106121
* @return UserStatus[]
107122
*/
108123
public function findAll(?int $limit = null, ?int $offset = null): array {
124+
// Return empty array if user enumeration is disabled or limited to groups
125+
// TODO: find a solution that scales to get only users from common groups if user enumeration is limited to
126+
// groups. See discussion at https://github.com/nextcloud/server/pull/27879#discussion_r729715936
127+
if (!$this->shareeEnumeration || $this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone) {
128+
return [];
129+
}
130+
109131
return array_map(function ($status) {
110132
return $this->processStatus($status);
111133
}, $this->mapper->findAll($limit, $offset));
@@ -117,6 +139,13 @@ public function findAll(?int $limit = null, ?int $offset = null): array {
117139
* @return array
118140
*/
119141
public function findAllRecentStatusChanges(?int $limit = null, ?int $offset = null): array {
142+
// Return empty array if user enumeration is disabled or limited to groups
143+
// TODO: find a solution that scales to get only users from common groups if user enumeration is limited to
144+
// groups. See discussion at https://github.com/nextcloud/server/pull/27879#discussion_r729715936
145+
if (!$this->shareeEnumeration || $this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone) {
146+
return [];
147+
}
148+
120149
return array_map(function ($status) {
121150
return $this->processStatus($status);
122151
}, $this->mapper->findAllRecent($limit, $offset));
@@ -224,7 +253,7 @@ public function setPredefinedMessage(string $userId,
224253
/**
225254
* @param string $userId
226255
* @param string|null $statusIcon
227-
* @param string|null $message
256+
* @param string $message
228257
* @param int|null $clearAt
229258
* @return UserStatus
230259
* @throws InvalidClearAtException
@@ -332,7 +361,7 @@ public function removeUserStatus(string $userId): bool {
332361
* up to date and provides translated default status if needed
333362
*
334363
* @param UserStatus $status
335-
* @returns UserStatus
364+
* @return UserStatus
336365
*/
337366
private function processStatus(UserStatus $status): UserStatus {
338367
$clearAt = $status->getClearAt();

apps/user_status/tests/Unit/Service/StatusServiceTest.php

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
use OCA\UserStatus\Service\StatusService;
3838
use OCP\AppFramework\Db\DoesNotExistException;
3939
use OCP\AppFramework\Utility\ITimeFactory;
40+
use OCP\IConfig;
4041
use OCP\UserStatus\IUserStatus;
4142
use Test\TestCase;
4243

@@ -54,6 +55,9 @@ class StatusServiceTest extends TestCase {
5455
/** @var EmojiService|\PHPUnit\Framework\MockObject\MockObject */
5556
private $emojiService;
5657

58+
/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
59+
private $config;
60+
5761
/** @var StatusService */
5862
private $service;
5963

@@ -64,10 +68,20 @@ protected function setUp(): void {
6468
$this->timeFactory = $this->createMock(ITimeFactory::class);
6569
$this->predefinedStatusService = $this->createMock(PredefinedStatusService::class);
6670
$this->emojiService = $this->createMock(EmojiService::class);
71+
72+
$this->config = $this->createMock(IConfig::class);
73+
74+
$this->config->method('getAppValue')
75+
->willReturnMap([
76+
['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
77+
['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no']
78+
]);
79+
6780
$this->service = new StatusService($this->mapper,
6881
$this->timeFactory,
6982
$this->predefinedStatusService,
70-
$this->emojiService);
83+
$this->emojiService,
84+
$this->config);
7185
}
7286

7387
public function testFindAll(): void {
@@ -100,6 +114,49 @@ public function testFindAllRecentStatusChanges(): void {
100114
], $this->service->findAllRecentStatusChanges(20, 50));
101115
}
102116

117+
public function testFindAllRecentStatusChangesNoEnumeration(): void {
118+
$status1 = $this->createMock(UserStatus::class);
119+
$status2 = $this->createMock(UserStatus::class);
120+
121+
$this->mapper->method('findAllRecent')
122+
->with(20, 50)
123+
->willReturn([$status1, $status2]);
124+
125+
// Rebuild $this->service with user enumeration turned off
126+
$this->config = $this->createMock(IConfig::class);
127+
128+
$this->config->method('getAppValue')
129+
->willReturnMap([
130+
['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'no'],
131+
['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no']
132+
]);
133+
134+
$this->service = new StatusService($this->mapper,
135+
$this->timeFactory,
136+
$this->predefinedStatusService,
137+
$this->emojiService,
138+
$this->config);
139+
140+
$this->assertEquals([], $this->service->findAllRecentStatusChanges(20, 50));
141+
142+
// Rebuild $this->service with user enumeration limited to common groups
143+
$this->config = $this->createMock(IConfig::class);
144+
145+
$this->config->method('getAppValue')
146+
->willReturnMap([
147+
['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
148+
['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes']
149+
]);
150+
151+
$this->service = new StatusService($this->mapper,
152+
$this->timeFactory,
153+
$this->predefinedStatusService,
154+
$this->emojiService,
155+
$this->config);
156+
157+
$this->assertEquals([], $this->service->findAllRecentStatusChanges(20, 50));
158+
}
159+
103160
public function testFindByUserId(): void {
104161
$status = $this->createMock(UserStatus::class);
105162
$this->mapper->expects($this->once())

0 commit comments

Comments
 (0)