Skip to content

Commit bf33e22

Browse files
DeepDiver1975claude
andcommitted
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>
1 parent 09b02f8 commit bf33e22

8 files changed

Lines changed: 205 additions & 23 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

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+
private function isEnabled() {
88+
return $this->config->getSystemValue('allow_subadmins', false) !== false;
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 && $config->getSystemValue('allow_subadmins', false) !== false) {
7676
$subAdmins = \OC::$server->getGroupManager()->getSubAdmin()->getAllSubAdmins();
7777
// New class returns IUser[] so convert back
7878
$result = [];

tests/acceptance/features/bootstrap/Provisioning.php

Lines changed: 46 additions & 0 deletions
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 = [];
@@ -4262,6 +4268,9 @@ public function userMakesUserASubadminOfGroupUsingTheProvisioningApi(
42624268
string $otherUser,
42634269
string $group
42644270
):void {
4271+
// The subadmin feature is disabled by default; enable it so these
4272+
// scenarios can exercise it. Reverted in cleanupSubadminFeature().
4273+
$this->enableSubadminFeature();
42654274
$actualUser = $this->getActualUsername($user);
42664275
$actualPassword = $this->getUserPassword($actualUser);
42674276
$actualSubadminUsername = $this->getActualUsername($otherUser);
@@ -4279,6 +4288,43 @@ public function userMakesUserASubadminOfGroupUsingTheProvisioningApi(
42794288
);
42804289
}
42814290

4291+
/**
4292+
* The group admin (subadmin) feature is disabled by default. Enable it so
4293+
* scenarios that exercise subadmins keep working. Tracked so it can be
4294+
* reverted after the scenario.
4295+
*
4296+
* @return void
4297+
* @throws Exception
4298+
*/
4299+
public function enableSubadminFeature():void {
4300+
if ($this->subadminFeatureEnabled) {
4301+
return;
4302+
}
4303+
SetupHelper::setSystemConfig(
4304+
'allow_subadmins',
4305+
'true',
4306+
$this->getStepLineRef(),
4307+
'boolean'
4308+
);
4309+
$this->subadminFeatureEnabled = true;
4310+
}
4311+
4312+
/**
4313+
* @AfterScenario
4314+
*
4315+
* @return void
4316+
* @throws Exception
4317+
*/
4318+
public function cleanupSubadminFeature():void {
4319+
if ($this->subadminFeatureEnabled) {
4320+
SetupHelper::deleteSystemConfig(
4321+
'allow_subadmins',
4322+
$this->getStepLineRef()
4323+
);
4324+
$this->subadminFeatureEnabled = false;
4325+
}
4326+
}
4327+
42824328
/**
42834329
* @When the administrator gets all the groups where user :user is subadmin using the provisioning API
42844330
*

0 commit comments

Comments
 (0)