Skip to content

Commit af00399

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 c799b48 commit af00399

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
@@ -35,6 +35,7 @@
3535
use OCA\UserStatus\Exception\StatusMessageTooLongException;
3636
use OCP\AppFramework\Db\DoesNotExistException;
3737
use OCP\AppFramework\Utility\ITimeFactory;
38+
use OCP\IConfig;
3839
use OCP\UserStatus\IUserStatus;
3940

4041
/**
@@ -56,6 +57,15 @@ class StatusService {
5657
/** @var EmojiService */
5758
private $emojiService;
5859

60+
/** @var bool */
61+
private $shareeEnumeration;
62+
63+
/** @var bool */
64+
private $shareeEnumerationInGroupOnly;
65+
66+
/** @var bool */
67+
private $shareeEnumerationPhone;
68+
5969
/**
6070
* List of priorities ordered by their priority
6171
*/
@@ -88,17 +98,22 @@ class StatusService {
8898
*
8999
* @param UserStatusMapper $mapper
90100
* @param ITimeFactory $timeFactory
91-
* @param PredefinedStatusService $defaultStatusService,
101+
* @param PredefinedStatusService $defaultStatusService
92102
* @param EmojiService $emojiService
103+
* @param IConfig $config
93104
*/
94105
public function __construct(UserStatusMapper $mapper,
95106
ITimeFactory $timeFactory,
96107
PredefinedStatusService $defaultStatusService,
97-
EmojiService $emojiService) {
108+
EmojiService $emojiService,
109+
IConfig $config) {
98110
$this->mapper = $mapper;
99111
$this->timeFactory = $timeFactory;
100112
$this->predefinedStatusService = $defaultStatusService;
101113
$this->emojiService = $emojiService;
114+
$this->shareeEnumeration = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
115+
$this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
116+
$this->shareeEnumerationPhone = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
102117
}
103118

104119
/**
@@ -107,6 +122,13 @@ public function __construct(UserStatusMapper $mapper,
107122
* @return UserStatus[]
108123
*/
109124
public function findAll(?int $limit = null, ?int $offset = null): array {
125+
// Return empty array if user enumeration is disabled or limited to groups
126+
// TODO: find a solution that scales to get only users from common groups if user enumeration is limited to
127+
// groups. See discussion at https://github.com/nextcloud/server/pull/27879#discussion_r729715936
128+
if (!$this->shareeEnumeration || $this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone) {
129+
return [];
130+
}
131+
110132
return array_map(function ($status) {
111133
return $this->processStatus($status);
112134
}, $this->mapper->findAll($limit, $offset));
@@ -118,6 +140,13 @@ public function findAll(?int $limit = null, ?int $offset = null): array {
118140
* @return array
119141
*/
120142
public function findAllRecentStatusChanges(?int $limit = null, ?int $offset = null): array {
143+
// Return empty array if user enumeration is disabled or limited to groups
144+
// TODO: find a solution that scales to get only users from common groups if user enumeration is limited to
145+
// groups. See discussion at https://github.com/nextcloud/server/pull/27879#discussion_r729715936
146+
if (!$this->shareeEnumeration || $this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone) {
147+
return [];
148+
}
149+
121150
return array_map(function ($status) {
122151
return $this->processStatus($status);
123152
}, $this->mapper->findAllRecent($limit, $offset));
@@ -225,7 +254,7 @@ public function setPredefinedMessage(string $userId,
225254
/**
226255
* @param string $userId
227256
* @param string|null $statusIcon
228-
* @param string|null $message
257+
* @param string $message
229258
* @param int|null $clearAt
230259
* @return UserStatus
231260
* @throws InvalidClearAtException
@@ -333,7 +362,7 @@ public function removeUserStatus(string $userId): bool {
333362
* up to date and provides translated default status if needed
334363
*
335364
* @param UserStatus $status
336-
* @returns UserStatus
365+
* @return UserStatus
337366
*/
338367
private function processStatus(UserStatus $status): UserStatus {
339368
$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
@@ -38,6 +38,7 @@
3838
use OCA\UserStatus\Service\StatusService;
3939
use OCP\AppFramework\Db\DoesNotExistException;
4040
use OCP\AppFramework\Utility\ITimeFactory;
41+
use OCP\IConfig;
4142
use OCP\UserStatus\IUserStatus;
4243
use Test\TestCase;
4344

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

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

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

7488
public function testFindAll(): void {
@@ -101,6 +115,49 @@ public function testFindAllRecentStatusChanges(): void {
101115
], $this->service->findAllRecentStatusChanges(20, 50));
102116
}
103117

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

0 commit comments

Comments
 (0)