Skip to content

Commit

Permalink
Merge pull request #31661 from nextcloud/enh/user_ldap-add-command-to…
Browse files Browse the repository at this point in the history
…-unmap-groups

Add ldap:reset-group command to unmap groups from LDAP
  • Loading branch information
blizzz authored Apr 5, 2022
2 parents 34c9b57 + d7a2910 commit 835e28d
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 32 deletions.
1 change: 1 addition & 0 deletions apps/user_ldap/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ A user logs into Nextcloud with their LDAP or AD credentials, and is granted acc
<command>OCA\User_LDAP\Command\CheckUser</command>
<command>OCA\User_LDAP\Command\CreateEmptyConfig</command>
<command>OCA\User_LDAP\Command\DeleteConfig</command>
<command>OCA\User_LDAP\Command\ResetGroup</command>
<command>OCA\User_LDAP\Command\ResetUser</command>
<command>OCA\User_LDAP\Command\Search</command>
<command>OCA\User_LDAP\Command\SetConfig</command>
Expand Down
1 change: 1 addition & 0 deletions apps/user_ldap/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
'OCA\\User_LDAP\\Command\\CheckUser' => $baseDir . '/../lib/Command/CheckUser.php',
'OCA\\User_LDAP\\Command\\CreateEmptyConfig' => $baseDir . '/../lib/Command/CreateEmptyConfig.php',
'OCA\\User_LDAP\\Command\\DeleteConfig' => $baseDir . '/../lib/Command/DeleteConfig.php',
'OCA\\User_LDAP\\Command\\ResetGroup' => $baseDir . '/../lib/Command/ResetGroup.php',
'OCA\\User_LDAP\\Command\\ResetUser' => $baseDir . '/../lib/Command/ResetUser.php',
'OCA\\User_LDAP\\Command\\Search' => $baseDir . '/../lib/Command/Search.php',
'OCA\\User_LDAP\\Command\\SetConfig' => $baseDir . '/../lib/Command/SetConfig.php',
Expand Down
1 change: 1 addition & 0 deletions apps/user_ldap/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class ComposerStaticInitUser_LDAP
'OCA\\User_LDAP\\Command\\CheckUser' => __DIR__ . '/..' . '/../lib/Command/CheckUser.php',
'OCA\\User_LDAP\\Command\\CreateEmptyConfig' => __DIR__ . '/..' . '/../lib/Command/CreateEmptyConfig.php',
'OCA\\User_LDAP\\Command\\DeleteConfig' => __DIR__ . '/..' . '/../lib/Command/DeleteConfig.php',
'OCA\\User_LDAP\\Command\\ResetGroup' => __DIR__ . '/..' . '/../lib/Command/ResetGroup.php',
'OCA\\User_LDAP\\Command\\ResetUser' => __DIR__ . '/..' . '/../lib/Command/ResetUser.php',
'OCA\\User_LDAP\\Command\\Search' => __DIR__ . '/..' . '/../lib/Command/Search.php',
'OCA\\User_LDAP\\Command\\SetConfig' => __DIR__ . '/..' . '/../lib/Command/SetConfig.php',
Expand Down
111 changes: 111 additions & 0 deletions apps/user_ldap/lib/Command/ResetGroup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\User_LDAP\Command;

use OCA\User_LDAP\Group_Proxy;
use OCA\User_LDAP\GroupPluginManager;
use OCP\IGroup;
use OCP\IGroupManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;

class ResetGroup extends Command {
private IGroupManager $groupManager;
private GroupPluginManager $pluginManager;
private Group_Proxy $backend;

public function __construct(
IGroupManager $groupManager,
GroupPluginManager $pluginManager,
Group_Proxy $backend
) {
$this->groupManager = $groupManager;
$this->pluginManager = $pluginManager;
$this->backend = $backend;
parent::__construct();
}

protected function configure(): void {
$this
->setName('ldap:reset-group')
->setDescription('deletes an LDAP group independent of the group state in the LDAP')
->addArgument(
'gid',
InputArgument::REQUIRED,
'the group name as used in Nextcloud'
)
->addOption(
'yes',
'y',
InputOption::VALUE_NONE,
'do not ask for confirmation'
);
}

protected function execute(InputInterface $input, OutputInterface $output): int {
try {
$gid = $input->getArgument('gid');
$group = $this->groupManager->get($gid);
if (!$group instanceof IGroup) {
throw new \Exception('Group not found');
}
$backends = $group->getBackendNames();
if (!in_array('LDAP', $backends)) {
throw new \Exception('The given group is not a recognized LDAP group.');
}
if ($input->getOption('yes') === false) {
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$q = new Question('Delete all local data of this group (y|N)? ');
$input->setOption('yes', $helper->ask($input, $output, $q) === 'y');
}
if ($input->getOption('yes') !== true) {
throw new \Exception('Reset cancelled by operator');
}

// Disable real deletion if a plugin supports it
$pluginManagerSuppressed = $this->pluginManager->setSuppressDeletion(true);
// Bypass groupExists test to force mapping deletion
$this->backend->getLDAPAccess($gid)->connection->writeToCache('groupExists' . $gid, false);
echo "calling delete $gid\n";
if ($group->delete()) {
$this->pluginManager->setSuppressDeletion($pluginManagerSuppressed);
return 0;
}
} catch (\Throwable $e) {
if (isset($pluginManagerSuppressed)) {
$this->pluginManager->setSuppressDeletion($pluginManagerSuppressed);
}
$output->writeln('<error>' . $e->getMessage() . '</error>');
return 1;
}
$output->writeln('<error>Error while resetting group</error>');
return 2;
}
}
28 changes: 23 additions & 5 deletions apps/user_ldap/lib/GroupPluginManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@
use OCP\GroupInterface;

class GroupPluginManager {
private $respondToActions = 0;
private int $respondToActions = 0;

private $which = [
/** @var array<int, ?ILDAPGroupPlugin> */
private array $which = [
GroupInterface::CREATE_GROUP => null,
GroupInterface::DELETE_GROUP => null,
GroupInterface::ADD_TO_GROUP => null,
Expand All @@ -37,6 +38,8 @@ class GroupPluginManager {
GroupInterface::GROUP_DETAILS => null
];

private bool $suppressDeletion = false;

/**
* @return int All implemented actions
*/
Expand Down Expand Up @@ -84,16 +87,31 @@ public function createGroup($gid) {
throw new \Exception('No plugin implements createGroup in this LDAP Backend.');
}

public function canDeleteGroup(): bool {
return !$this->suppressDeletion && $this->implementsActions(GroupInterface::DELETE_GROUP);
}

/**
* @return bool – the value before the change
*/
public function setSuppressDeletion(bool $value): bool {
$old = $this->suppressDeletion;
$this->suppressDeletion = $value;
return $old;
}

/**
* Delete a group
* @param string $gid Group Id of the group to delete
* @return bool
*
* @throws \Exception
*/
public function deleteGroup($gid) {
public function deleteGroup(string $gid): bool {
$plugin = $this->which[GroupInterface::DELETE_GROUP];

if ($plugin) {
if ($this->suppressDeletion) {
return false;
}
return $plugin->deleteGroup($gid);
}
throw new \Exception('No plugin implements deleteGroup in this LDAP Backend.');
Expand Down
27 changes: 21 additions & 6 deletions apps/user_ldap/lib/Group_LDAP.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@
use OC\Cache\CappedMemoryCache;
use OC\ServerNotAvailableException;
use OCP\Group\Backend\IGetDisplayNameBackend;
use OCP\Group\Backend\IDeleteGroupBackend;
use OCP\GroupInterface;
use Psr\Log\LoggerInterface;

class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend {
class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend, IDeleteGroupBackend {
protected $enabled = false;

/** @var string[][] $cachedGroupMembers array of users with gid as key */
Expand Down Expand Up @@ -1204,6 +1205,7 @@ protected function filterValidGroups(array $listOfGroups): array {
*/
public function implementsActions($actions) {
return (bool)((GroupInterface::COUNT_USERS |
GroupInterface::DELETE_GROUP |
$this->groupPluginManager->getImplementedActions()) & $actions);
}

Expand Down Expand Up @@ -1249,19 +1251,32 @@ public function createGroup($gid) {
* delete a group
*
* @param string $gid gid of the group to delete
* @return bool
* @throws Exception
*/
public function deleteGroup($gid) {
if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
public function deleteGroup(string $gid): bool {
if ($this->groupPluginManager->canDeleteGroup()) {
if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
#delete group in nextcloud internal db
// Delete group in nextcloud internal db
$this->access->getGroupMapper()->unmap($gid);
$this->access->connection->writeToCache("groupExists" . $gid, false);
}
return $ret;
}
throw new Exception('Could not delete group in LDAP backend.');

// Getting dn, if false the group is not mapped
$dn = $this->access->groupname2dn($gid);
if (!$dn) {
throw new Exception('Could not delete unknown group '.$gid.' in LDAP backend.');
}

if (!$this->groupExists($gid)) {
// The group does not exist in the LDAP, remove the mapping
$this->access->getGroupMapper()->unmap($gid);
$this->access->connection->writeToCache("groupExists" . $gid, false);
return true;
}

throw new Exception('Could not delete existing group '.$gid.' in LDAP backend.');
}

/**
Expand Down
10 changes: 4 additions & 6 deletions apps/user_ldap/lib/Group_Proxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@
*/
namespace OCA\User_LDAP;

use OCP\Group\Backend\INamedBackend;
use OCP\Group\Backend\IDeleteGroupBackend;
use OCP\Group\Backend\IGetDisplayNameBackend;
use OCP\Group\Backend\INamedBackend;

class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend, INamedBackend {
class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend, INamedBackend, IDeleteGroupBackend {
private $backends = [];
private $refBackend = null;

Expand Down Expand Up @@ -171,11 +172,8 @@ public function createGroup($gid) {

/**
* delete a group
*
* @param string $gid gid of the group to delete
* @return bool
*/
public function deleteGroup($gid) {
public function deleteGroup(string $gid): bool {
return $this->handleRequest(
$gid, 'deleteGroup', [$gid]);
}
Expand Down
7 changes: 3 additions & 4 deletions apps/user_ldap/lib/UserPluginManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
use OC\User\Backend;

class UserPluginManager {
private $respondToActions = 0;
private int $respondToActions = 0;

private $which = [
private array $which = [
Backend::CREATE_USER => null,
Backend::SET_PASSWORD => null,
Backend::GET_HOME => null,
Expand All @@ -41,8 +41,7 @@ class UserPluginManager {
'deleteUser' => null
];

/** @var bool */
private $suppressDeletion = false;
private bool $suppressDeletion = false;

/**
* @return int All implemented actions, except for 'deleteUser'
Expand Down
16 changes: 8 additions & 8 deletions apps/user_ldap/tests/GroupLDAPPluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public function testCreateGroup() {
$pluginManager->createGroup('group');
}


public function testCreateGroupNotRegistered() {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('No plugin implements createGroup in this LDAP Backend.');
Expand All @@ -108,13 +108,13 @@ public function testDeleteGroup() {
->method('deleteGroup')
->with(
$this->equalTo('group')
);
)->willReturn(true);

$pluginManager->register($plugin);
$pluginManager->deleteGroup('group');
$this->assertTrue($pluginManager->deleteGroup('group'));
}


public function testDeleteGroupNotRegistered() {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('No plugin implements deleteGroup in this LDAP Backend.');
Expand Down Expand Up @@ -145,7 +145,7 @@ public function testAddToGroup() {
$pluginManager->addToGroup('uid', 'gid');
}


public function testAddToGroupNotRegistered() {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('No plugin implements addToGroup in this LDAP Backend.');
Expand Down Expand Up @@ -176,7 +176,7 @@ public function testRemoveFromGroup() {
$pluginManager->removeFromGroup('uid', 'gid');
}


public function testRemoveFromGroupNotRegistered() {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('No plugin implements removeFromGroup in this LDAP Backend.');
Expand Down Expand Up @@ -207,7 +207,7 @@ public function testCountUsersInGroup() {
$pluginManager->countUsersInGroup('gid', 'search');
}


public function testCountUsersInGroupNotRegistered() {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('No plugin implements countUsersInGroup in this LDAP Backend.');
Expand Down Expand Up @@ -237,7 +237,7 @@ public function testgetGroupDetails() {
$pluginManager->getGroupDetails('gid');
}


public function testgetGroupDetailsNotRegistered() {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('No plugin implements getGroupDetails in this LDAP Backend.');
Expand Down
4 changes: 2 additions & 2 deletions apps/user_ldap/tests/Group_LDAPTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1092,7 +1092,7 @@ public function testDeleteGroupWithPlugin() {
$pluginManager->expects($this->once())
->method('deleteGroup')
->with('gid')
->willReturn('result');
->willReturn(true);

$mapper = $this->getMockBuilder(GroupMapping::class)
->setMethods(['unmap'])
Expand All @@ -1108,7 +1108,7 @@ public function testDeleteGroupWithPlugin() {

$ldap = new GroupLDAP($access, $pluginManager);

$this->assertEquals($ldap->deleteGroup('gid'), 'result');
$this->assertTrue($ldap->deleteGroup('gid'));
}


Expand Down
2 changes: 1 addition & 1 deletion core/Command/Group/Delete.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$output->writeln('<error>Group "' . $gid . '" could not be deleted.</error>');
return 1;
}
if (! $this->groupManager->groupExists($gid)) {
if (!$this->groupManager->groupExists($gid)) {
$output->writeln('<error>Group "' . $gid . '" does not exist.</error>');
return 1;
}
Expand Down

0 comments on commit 835e28d

Please sign in to comment.