Skip to content

Commit 80b1b91

Browse files
DeepDiver1975claudephil-davis
authored
fix(subadmin): disable group-admin feature by default behind allow_subadmins (#41634)
* fix(subadmin): disable group-admin feature by default behind allow_subadmins The subadmin (group-admin) feature lets an admin delegate user management of specific groups to a non-admin user. The related code path has known security shortcomings, so the feature is now disabled by default as a risk-mitigation. Deployments that rely on it can opt back in by setting the new `allow_subadmins` system config to true. Enforcement is centralized in the OC\SubAdmin manager, the single chokepoint all consumers route through: - isSubAdmin() keeps the real-admin short-circuit, then returns false for group-admin-only users when disabled, cascading to permission bypasses, legacy guards and the settings middleware. - Read methods (isSubAdminofGroup, getSubAdminsGroups, getGroupsSubAdmins, getAllSubAdmins) behave as if no subadmins exist. - createSubAdmin throws HintException; the two write callers (togglesubadmins.php, provisioning_api addSubAdmin) surface a clean error. - deleteSubAdmin and the post-delete cleanup hooks stay enabled so admins can prune dormant assignments. The Users settings page hides the group-admin column when disabled, and the option is documented in config.sample.php with a security note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> * test: add acceptance test for when the subadmin feature is disabled * chore: add changelog for allow_subadmins setting * fix(subadmin): strict boolean gate and single-source isEnabled() Address remaining review feedback on the allow_subadmins gate: - Tighten the gate to a strict `=== true` check so only the documented boolean opt-in enables the feature; string values such as 'false' or '0' in a hand-edited config.php now fail closed instead of fail open. - Make SubAdmin::isEnabled() public and route settings/users.php through it, removing the duplicated inline config read so the gate has a single source of truth in the OC\SubAdmin manager. - Document the breaking upgrade behavior in the changelog: existing group-admin assignments are ignored until allow_subadmins => true. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --------- Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Phillip Davis <phil@jankaritech.com>
1 parent 1d1e06b commit 80b1b91

10 files changed

Lines changed: 253 additions & 24 deletions

File tree

apps/provisioning_api/lib/Users.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -600,10 +600,14 @@ public function addSubAdmin($parameters) {
600600
return new Result(null, 100);
601601
}
602602
// Go
603-
if ($subAdminManager->createSubAdmin($user, $group)) {
604-
return new Result(null, 100);
605-
} else {
606-
return new Result(null, 103, 'Unknown error occurred');
603+
try {
604+
if ($subAdminManager->createSubAdmin($user, $group)) {
605+
return new Result(null, 100);
606+
} else {
607+
return new Result(null, 103, 'Unknown error occurred');
608+
}
609+
} catch (\OC\HintException $e) {
610+
return new Result(null, 103, $e->getHint());
607611
}
608612
}
609613

changelog/unreleased/41634

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Security: disable group-admin feature by default behind allow_subadmins
2+
3+
Disable the subadmin (group-admin) feature by default behind a new
4+
allow_subadmins system config, as a security risk-mitigation.
5+
The feature's code path has known security shortcomings; deployments
6+
that rely on it can opt back in with 'allow_subadmins' => true in config.php.
7+
On upgrade, existing group-admin assignments are ignored until an admin
8+
sets 'allow_subadmins' => true in config.php.
9+
10+
https://github.com/owncloud/core/pull/41634

config/config.sample.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,19 @@
218218
*/
219219
'allow_user_to_change_mail_address' => true,
220220

221+
/**
222+
* Allow or disallow the group administrator (subadmin) feature
223+
* `true` allows administrators to delegate user management of specific groups
224+
* to non-admin users (group admins / subadmins).
225+
* `false` (default) disables the feature entirely: existing group-admin
226+
* assignments are ignored and no new ones can be created.
227+
*
228+
* SECURITY NOTE: this feature is disabled by default as a risk-mitigation.
229+
* Only enable it if you require delegated group administration and understand
230+
* that group admins can manage users within their groups.
231+
*/
232+
'allow_subadmins' => false,
233+
221234
/**
222235
* Define the lifetime of the remember-login cookie
223236
* The remember-login cookie is set when the user clicks the `remember` checkbox

lib/private/Group/Manager.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,8 @@ public function getSubAdmin() {
487487
$this->subAdmin = new \OC\SubAdmin(
488488
$this->userManager,
489489
$this,
490-
\OC::$server->getDatabaseConnection()
490+
\OC::$server->getDatabaseConnection(),
491+
\OC::$server->getConfig()
491492
);
492493
}
493494

lib/private/SubAdmin.php

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
use OC\Hooks\PublicEmitter;
3030
use OCP\Events\EventEmitterTrait;
31+
use OCP\IConfig;
3132
use OCP\IUser;
3233
use OCP\IUserManager;
3334
use OCP\IGroup;
@@ -45,21 +46,27 @@ class SubAdmin extends PublicEmitter {
4546
/** @var IDBConnection */
4647
private $dbConn;
4748

49+
/** @var IConfig */
50+
private $config;
51+
4852
/**
4953
* @param IUserManager $userManager
5054
* @param IGroupManager $groupManager
5155
* @param IDBConnection $dbConn
56+
* @param IConfig $config
5257
*/
5358
public function __construct(
5459
IUserManager $userManager,
5560
IGroupManager $groupManager,
56-
IDBConnection $dbConn
61+
IDBConnection $dbConn,
62+
IConfig $config
5763
) {
5864
'@phan-var \OC\User\Manager $userManager';
5965
$this->userManager = $userManager;
6066
'@phan-var \OC\Group\Manager $groupManager';
6167
$this->groupManager = $groupManager;
6268
$this->dbConn = $dbConn;
69+
$this->config = $config;
6370

6471
$this->userManager->listen('\OC\User', 'postDelete', function ($user) {
6572
$this->post_deleteUser($user);
@@ -69,13 +76,33 @@ public function __construct(
6976
});
7077
}
7178

79+
/**
80+
* Whether the group admin (subadmin) feature is enabled.
81+
*
82+
* Disabled by default as a security risk-mitigation; admins opt in via the
83+
* `allow_subadmins` system config.
84+
*
85+
* @return bool
86+
*/
87+
public function isEnabled() {
88+
return $this->config->getSystemValue('allow_subadmins', false) === true;
89+
}
90+
7291
/**
7392
* add a SubAdmin
7493
* @param IUser $user user to be SubAdmin
7594
* @param IGroup $group group $user becomes subadmin of
7695
* @return bool
96+
* @throws \OC\HintException if the subadmin feature is disabled
7797
*/
7898
public function createSubAdmin(IUser $user, IGroup $group) {
99+
if (!$this->isEnabled()) {
100+
$l = \OC::$server->getL10N('lib');
101+
throw new \OC\HintException(
102+
$l->t('Group admin feature is disabled'),
103+
$l->t('The group admin (subadmin) feature has been disabled by the administrator')
104+
);
105+
}
79106
return $this->emittingCall(function () use (&$user, &$group) {
80107
$qb = $this->dbConn->getQueryBuilder();
81108

@@ -125,6 +152,9 @@ public function deleteSubAdmin(IUser $user, IGroup $group) {
125152
* @return IGroup[]
126153
*/
127154
public function getSubAdminsGroups(IUser $user) {
155+
if (!$this->isEnabled()) {
156+
return [];
157+
}
128158
$qb = $this->dbConn->getQueryBuilder();
129159

130160
$result = $qb->select('gid')
@@ -150,6 +180,9 @@ public function getSubAdminsGroups(IUser $user) {
150180
* @return IUser[]
151181
*/
152182
public function getGroupsSubAdmins(IGroup $group) {
183+
if (!$this->isEnabled()) {
184+
return [];
185+
}
153186
$qb = $this->dbConn->getQueryBuilder();
154187

155188
$result = $qb->select('uid')
@@ -174,6 +207,9 @@ public function getGroupsSubAdmins(IGroup $group) {
174207
* @return array
175208
*/
176209
public function getAllSubAdmins() {
210+
if (!$this->isEnabled()) {
211+
return [];
212+
}
177213
$qb = $this->dbConn->getQueryBuilder();
178214

179215
$result = $qb->select('*')
@@ -203,6 +239,9 @@ public function getAllSubAdmins() {
203239
* @return bool
204240
*/
205241
public function isSubAdminofGroup(IUser $user, IGroup $group) {
242+
if (!$this->isEnabled()) {
243+
return false;
244+
}
206245
$qb = $this->dbConn->getQueryBuilder();
207246

208247
/*
@@ -232,6 +271,12 @@ public function isSubAdmin(IUser $user) {
232271
return true;
233272
}
234273

274+
// When the feature is disabled, group-admin-only users have no
275+
// elevated rights (real admins are handled by the short-circuit above)
276+
if (!$this->isEnabled()) {
277+
return false;
278+
}
279+
235280
$qb = $this->dbConn->getQueryBuilder();
236281

237282
$result = $qb->select('gid')

settings/ajax/togglesubadmins.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@
3939
if ($isSubAdminOfGroup) {
4040
$subAdminManager->deleteSubAdmin($targetUserObject, $targetGroupObject);
4141
} else {
42-
$subAdminManager->createSubAdmin($targetUserObject, $targetGroupObject);
42+
try {
43+
$subAdminManager->createSubAdmin($targetUserObject, $targetGroupObject);
44+
} catch (\OC\HintException $e) {
45+
OC_JSON::error(['data' => ['message' => $e->getHint()]]);
46+
exit();
47+
}
4348
}
4449

4550
OC_JSON::success();

settings/users.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
$recoveryAdminEnabled = OC_App::isEnabled('encryption') &&
7373
$config->getAppValue('encryption', 'recoveryAdminEnabled', null);
7474

75-
if ($isAdmin) {
75+
if ($isAdmin && \OC::$server->getGroupManager()->getSubAdmin()->isEnabled()) {
7676
$subAdmins = \OC::$server->getGroupManager()->getSubAdmin()->getAllSubAdmins();
7777
// New class returns IUser[] so convert back
7878
$result = [];

tests/acceptance/features/apiProvisioning-v1/createSubAdmin.feature

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ Feature: create a subadmin
2525
And the HTTP status code should be "200"
2626
And user "nonexistentuser" should not be a subadmin of group "brand-new-group"
2727

28+
@smokeTest
29+
Scenario: admin tries to create a subadmin when the subadmin feature is disabled
30+
Given user "brand-new-user" has been created with default attributes and without skeleton files
31+
And group "brand-new-group" has been created
32+
When the administrator tries to make user "brand-new-user" a subadmin of group "brand-new-group" using the provisioning API with the subadmin feature disabled
33+
Then the OCS status code should be "103"
34+
And the HTTP status code should be "200"
35+
And user "brand-new-user" should not be a subadmin of group "brand-new-group"
36+
2837

2938
Scenario: admin tries to create a subadmin using a group which does not exist
3039
Given user "brand-new-user" has been created with default attributes and without skeleton files

tests/acceptance/features/bootstrap/Provisioning.php

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ trait Provisioning {
4949
private array $createdRemoteUsers = [];
5050
private array $enabledApps = [];
5151
private array $disabledApps = [];
52+
53+
/**
54+
* Whether this scenario enabled the group admin (subadmin) feature, which
55+
* is disabled by default. Used to revert the config after the scenario.
56+
*/
57+
private bool $subadminFeatureEnabled = false;
5258
private array $startingGroups = [];
5359
private array $createdRemoteGroups = [];
5460
private array $createdGroups = [];
@@ -4247,21 +4253,52 @@ public function adminMakesUserSubadminOfGroupUsingTheProvisioningApi(
42474253
);
42484254
}
42494255

4256+
/**
4257+
* @When /^the administrator tries to make user "([^"]*)" a subadmin of group "([^"]*)" using the provisioning API with the subadmin feature disabled$/
4258+
*
4259+
* @param string $user
4260+
* @param string $group
4261+
*
4262+
* @return void
4263+
*/
4264+
public function adminTriesToMakeUserSubadminOfGroupUsingTheProvisioningApiWithSubadminFeatureDisabled(
4265+
string $user,
4266+
string $group
4267+
):void {
4268+
$user = $this->getActualUsername($user);
4269+
$this->userMakesUserASubadminOfGroupUsingTheProvisioningApi(
4270+
$this->getAdminUsername(),
4271+
$user,
4272+
$group,
4273+
false
4274+
);
4275+
}
4276+
42504277
/**
42514278
* @When user :user makes user :otherUser a subadmin of group :group using the provisioning API
42524279
*
42534280
* @param string $user
42544281
* @param string $otherUser
42554282
* @param string $group
4283+
* @param bool $enableSubadminFeature
42564284
*
42574285
* @return void
42584286
* @throws Exception
42594287
*/
42604288
public function userMakesUserASubadminOfGroupUsingTheProvisioningApi(
42614289
string $user,
42624290
string $otherUser,
4263-
string $group
4291+
string $group,
4292+
bool $enableSubadminFeature = true
42644293
):void {
4294+
if ($enableSubadminFeature) {
4295+
// The subadmin feature is disabled by default; enable it so these
4296+
// scenarios can exercise it. Reverted in cleanupSubadminFeature().
4297+
$this->enableSubadminFeature();
4298+
} else {
4299+
// Make sure that the Subadmin feature is disabled.
4300+
$this->cleanupSubadminFeature();
4301+
}
42654302
$actualUser = $this->getActualUsername($user);
42664303
$actualPassword = $this->getUserPassword($actualUser);
42674304
$actualSubadminUsername = $this->getActualUsername($otherUser);
@@ -4279,6 +4316,43 @@ public function userMakesUserASubadminOfGroupUsingTheProvisioningApi(
42794316
);
42804317
}
42814318

4319+
/**
4320+
* The group admin (subadmin) feature is disabled by default. Enable it so
4321+
* scenarios that exercise subadmins keep working. Tracked so it can be
4322+
* reverted after the scenario.
4323+
*
4324+
* @return void
4325+
* @throws Exception
4326+
*/
4327+
public function enableSubadminFeature():void {
4328+
if ($this->subadminFeatureEnabled) {
4329+
return;
4330+
}
4331+
SetupHelper::setSystemConfig(
4332+
'allow_subadmins',
4333+
'true',
4334+
$this->getStepLineRef(),
4335+
'boolean'
4336+
);
4337+
$this->subadminFeatureEnabled = true;
4338+
}
4339+
4340+
/**
4341+
* @AfterScenario
4342+
*
4343+
* @return void
4344+
* @throws Exception
4345+
*/
4346+
public function cleanupSubadminFeature():void {
4347+
if ($this->subadminFeatureEnabled) {
4348+
SetupHelper::deleteSystemConfig(
4349+
'allow_subadmins',
4350+
$this->getStepLineRef()
4351+
);
4352+
$this->subadminFeatureEnabled = false;
4353+
}
4354+
}
4355+
42824356
/**
42834357
* @When the administrator gets all the groups where user :user is subadmin using the provisioning API
42844358
*

0 commit comments

Comments
 (0)