Skip to content

Commit 9c3ef8e

Browse files
authored
Merge pull request #50567 from nextcloud/enh/ldap-add-test-settings-command
Add LDAP test settings command
2 parents 9fffdf2 + 7fa117d commit 9c3ef8e

File tree

7 files changed

+299
-50
lines changed

7 files changed

+299
-50
lines changed

apps/user_ldap/appinfo/info.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ A user logs into Nextcloud with their LDAP or AD credentials, and is granted acc
6464
<command>OCA\User_LDAP\Command\ShowConfig</command>
6565
<command>OCA\User_LDAP\Command\ShowRemnants</command>
6666
<command>OCA\User_LDAP\Command\TestConfig</command>
67+
<command>OCA\User_LDAP\Command\TestUserSettings</command>
6768
<command>OCA\User_LDAP\Command\UpdateUUID</command>
6869
</commands>
6970

apps/user_ldap/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
'OCA\\User_LDAP\\Command\\ShowConfig' => $baseDir . '/../lib/Command/ShowConfig.php',
2424
'OCA\\User_LDAP\\Command\\ShowRemnants' => $baseDir . '/../lib/Command/ShowRemnants.php',
2525
'OCA\\User_LDAP\\Command\\TestConfig' => $baseDir . '/../lib/Command/TestConfig.php',
26+
'OCA\\User_LDAP\\Command\\TestUserSettings' => $baseDir . '/../lib/Command/TestUserSettings.php',
2627
'OCA\\User_LDAP\\Command\\UpdateUUID' => $baseDir . '/../lib/Command/UpdateUUID.php',
2728
'OCA\\User_LDAP\\Configuration' => $baseDir . '/../lib/Configuration.php',
2829
'OCA\\User_LDAP\\Connection' => $baseDir . '/../lib/Connection.php',

apps/user_ldap/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class ComposerStaticInitUser_LDAP
3838
'OCA\\User_LDAP\\Command\\ShowConfig' => __DIR__ . '/..' . '/../lib/Command/ShowConfig.php',
3939
'OCA\\User_LDAP\\Command\\ShowRemnants' => __DIR__ . '/..' . '/../lib/Command/ShowRemnants.php',
4040
'OCA\\User_LDAP\\Command\\TestConfig' => __DIR__ . '/..' . '/../lib/Command/TestConfig.php',
41+
'OCA\\User_LDAP\\Command\\TestUserSettings' => __DIR__ . '/..' . '/../lib/Command/TestUserSettings.php',
4142
'OCA\\User_LDAP\\Command\\UpdateUUID' => __DIR__ . '/..' . '/../lib/Command/UpdateUUID.php',
4243
'OCA\\User_LDAP\\Configuration' => __DIR__ . '/..' . '/../lib/Configuration.php',
4344
'OCA\\User_LDAP\\Connection' => __DIR__ . '/..' . '/../lib/Connection.php',
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
6+
* SPDX-License-Identifier: AGPL-3.0-only
7+
*/
8+
namespace OCA\User_LDAP\Command;
9+
10+
use OCA\User_LDAP\Group_Proxy;
11+
use OCA\User_LDAP\Helper;
12+
use OCA\User_LDAP\Mapping\GroupMapping;
13+
use OCA\User_LDAP\Mapping\UserMapping;
14+
use OCA\User_LDAP\User\DeletedUsersIndex;
15+
use OCA\User_LDAP\User_Proxy;
16+
use Symfony\Component\Console\Command\Command;
17+
use Symfony\Component\Console\Input\InputArgument;
18+
use Symfony\Component\Console\Input\InputInterface;
19+
use Symfony\Component\Console\Input\InputOption;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
22+
class TestUserSettings extends Command {
23+
public function __construct(
24+
protected User_Proxy $backend,
25+
protected Group_Proxy $groupBackend,
26+
protected Helper $helper,
27+
protected DeletedUsersIndex $dui,
28+
protected UserMapping $mapping,
29+
protected GroupMapping $groupMapping,
30+
) {
31+
parent::__construct();
32+
}
33+
34+
protected function configure(): void {
35+
$this
36+
->setName('ldap:test-user-settings')
37+
->setDescription('Runs tests and show information about user related LDAP settings')
38+
->addArgument(
39+
'user',
40+
InputArgument::REQUIRED,
41+
'the user name as used in Nextcloud, or the LDAP DN'
42+
)
43+
->addOption(
44+
'group',
45+
'g',
46+
InputOption::VALUE_REQUIRED,
47+
'A group DN to check if the user is a member or not'
48+
)
49+
->addOption(
50+
'clearcache',
51+
null,
52+
InputOption::VALUE_NONE,
53+
'Clear the cache of the LDAP connection before the beginning of tests'
54+
)
55+
;
56+
}
57+
58+
protected function execute(InputInterface $input, OutputInterface $output): int {
59+
try {
60+
$uid = $input->getArgument('user');
61+
$access = $this->backend->getLDAPAccess($uid);
62+
$connection = $access->getConnection();
63+
if ($input->getOption('clearcache')) {
64+
$connection->clearCache();
65+
}
66+
$configPrefix = $connection->getConfigPrefix();
67+
$knownDn = '';
68+
if ($access->stringResemblesDN($uid)) {
69+
$knownDn = $uid;
70+
$username = $access->dn2username($uid);
71+
if ($username !== false) {
72+
$uid = $username;
73+
}
74+
}
75+
76+
$dn = $this->mapping->getDNByName($uid);
77+
if ($dn !== false) {
78+
$output->writeln("User <info>$dn</info> is mapped with account name <info>$uid</info>.");
79+
$uuid = $this->mapping->getUUIDByDN($dn);
80+
$output->writeln("Known UUID is <info>$uuid</info>.");
81+
if ($knownDn === '') {
82+
$knownDn = $dn;
83+
}
84+
} else {
85+
$output->writeln("User <info>$uid</info> is not mapped.");
86+
}
87+
88+
if ($knownDn === '') {
89+
return self::SUCCESS;
90+
}
91+
92+
if (!$access->isDNPartOfBase($knownDn, $access->getConnection()->ldapBaseUsers)) {
93+
$output->writeln(
94+
"User <info>$knownDn</info> is not in one of the configured user bases: <info>" .
95+
implode(',', $access->getConnection()->ldapBaseUsers) .
96+
'</info>.'
97+
);
98+
}
99+
100+
$output->writeln("Configuration prefix is <info>$configPrefix</info>");
101+
$output->writeln('');
102+
103+
$attributeNames = [
104+
'ldapExpertUsernameAttr',
105+
'ldapUuidUserAttribute',
106+
'ldapExpertUUIDUserAttr',
107+
'ldapQuotaAttribute',
108+
'ldapEmailAttribute',
109+
'ldapUserDisplayName',
110+
'ldapUserDisplayName2',
111+
'ldapExtStorageHomeAttribute',
112+
'ldapAttributePhone',
113+
'ldapAttributeWebsite',
114+
'ldapAttributeAddress',
115+
'ldapAttributeTwitter',
116+
'ldapAttributeFediverse',
117+
'ldapAttributeOrganisation',
118+
'ldapAttributeRole',
119+
'ldapAttributeHeadline',
120+
'ldapAttributeBiography',
121+
'ldapAttributeBirthDate',
122+
'ldapAttributePronouns',
123+
];
124+
$output->writeln('Attributes set in configuration:');
125+
foreach ($attributeNames as $attributeName) {
126+
if ($connection->$attributeName !== '') {
127+
$output->writeln("- $attributeName: <info>" . $connection->$attributeName . '</info>');
128+
}
129+
}
130+
131+
$filter = $connection->ldapUserFilter;
132+
$attrs = $access->userManager->getAttributes(true);
133+
$attrs[] = strtolower($connection->ldapExpertUsernameAttr);
134+
if ($connection->ldapUuidUserAttribute !== 'auto') {
135+
$attrs[] = strtolower($connection->ldapUuidUserAttribute);
136+
}
137+
$attrs[] = 'memberof';
138+
$attrs = array_values(array_unique($attrs));
139+
$attributes = $access->readAttributes($knownDn, $attrs, $filter);
140+
141+
if ($attributes === false) {
142+
$output->writeln(
143+
"LDAP read on <info>$knownDn</info> with filter <info>$filter</info> failed."
144+
);
145+
return self::FAILURE;
146+
}
147+
148+
$output->writeln("Attributes fetched from LDAP using filter <info>$filter</info>:");
149+
foreach ($attributes as $attribute => $value) {
150+
$output->writeln(
151+
"- $attribute: <info>" . json_encode($value) . '</info>'
152+
);
153+
}
154+
155+
$uuid = $access->getUUID($knownDn);
156+
if ($connection->ldapUuidUserAttribute === 'auto') {
157+
$output->writeln('<error>Failed to detect UUID attribute</error>');
158+
} else {
159+
$output->writeln('Detected UUID attribute: <info>' . $connection->ldapUuidUserAttribute . '</info>');
160+
}
161+
if ($uuid === false) {
162+
$output->writeln("<error>Failed to find UUID for $knownDn</error>");
163+
} else {
164+
$output->writeln("UUID for <info>$knownDn</info>: <info>$uuid</info>");
165+
}
166+
167+
$groupLdapInstance = $this->groupBackend->getBackend($configPrefix);
168+
169+
$output->writeln('');
170+
$output->writeln('Group information:');
171+
172+
$attributeNames = [
173+
'ldapDynamicGroupMemberURL',
174+
'ldapGroupFilter',
175+
'ldapGroupMemberAssocAttr',
176+
];
177+
$output->writeln('Configuration:');
178+
foreach ($attributeNames as $attributeName) {
179+
if ($connection->$attributeName !== '') {
180+
$output->writeln("- $attributeName: <info>" . $connection->$attributeName . '</info>');
181+
}
182+
}
183+
184+
$primaryGroup = $groupLdapInstance->getUserPrimaryGroup($knownDn);
185+
$output->writeln('Primary group: <info>' . ($primaryGroup !== false? $primaryGroup:'') . '</info>');
186+
187+
$groupByGid = $groupLdapInstance->getUserGroupByGid($knownDn);
188+
$output->writeln('Group from gidNumber: <info>' . ($groupByGid !== false? $groupByGid:'') . '</info>');
189+
190+
$groups = $groupLdapInstance->getUserGroups($uid);
191+
$output->writeln('All known groups: <info>' . json_encode($groups) . '</info>');
192+
193+
$memberOfUsed = ((int)$access->connection->hasMemberOfFilterSupport === 1
194+
&& (int)$access->connection->useMemberOfToDetectMembership === 1);
195+
196+
$output->writeln('MemberOf usage: <info>' . ($memberOfUsed ? 'on' : 'off') . '</info> (' . $access->connection->hasMemberOfFilterSupport . ',' . $access->connection->useMemberOfToDetectMembership . ')');
197+
198+
$gid = (string)$input->getOption('group');
199+
if ($gid === '') {
200+
return self::SUCCESS;
201+
}
202+
203+
$output->writeln('');
204+
$output->writeln("Group $gid:");
205+
$knownGroupDn = '';
206+
if ($access->stringResemblesDN($gid)) {
207+
$knownGroupDn = $gid;
208+
$groupname = $access->dn2groupname($gid);
209+
if ($groupname !== false) {
210+
$gid = $groupname;
211+
}
212+
}
213+
214+
$groupDn = $this->groupMapping->getDNByName($gid);
215+
if ($groupDn !== false) {
216+
$output->writeln("Group <info>$groupDn</info> is mapped with name <info>$gid</info>.");
217+
$groupUuid = $this->groupMapping->getUUIDByDN($groupDn);
218+
$output->writeln("Known UUID is <info>$groupUuid</info>.");
219+
if ($knownGroupDn === '') {
220+
$knownGroupDn = $groupDn;
221+
}
222+
} else {
223+
$output->writeln("Group <info>$gid</info> is not mapped.");
224+
}
225+
226+
$members = $groupLdapInstance->usersInGroup($gid);
227+
$output->writeln('Members: <info>' . json_encode($members) . '</info>');
228+
229+
return self::SUCCESS;
230+
231+
} catch (\Exception $e) {
232+
$output->writeln('<error>' . $e->getMessage() . '</error>');
233+
return self::FAILURE;
234+
}
235+
}
236+
}

apps/user_ldap/lib/Group_Proxy.php

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,10 @@
1818
use OCP\IConfig;
1919
use OCP\IUserManager;
2020

21+
/**
22+
* @template-extends Proxy<Group_LDAP>
23+
*/
2124
class Group_Proxy extends Proxy implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend, INamedBackend, IDeleteGroupBackend, IBatchMethodsBackend, IIsAdminBackend {
22-
private $backends = [];
23-
private ?Group_LDAP $refBackend = null;
24-
private bool $isSetUp = false;
25-
2625
public function __construct(
2726
private Helper $helper,
2827
ILDAPWrapper $ldap,
@@ -31,24 +30,12 @@ public function __construct(
3130
private IConfig $config,
3231
private IUserManager $ncUserManager,
3332
) {
34-
parent::__construct($ldap, $accessFactory);
33+
parent::__construct($helper, $ldap, $accessFactory);
3534
}
3635

37-
protected function setup(): void {
38-
if ($this->isSetUp) {
39-
return;
40-
}
41-
42-
$serverConfigPrefixes = $this->helper->getServerConfigurationPrefixes(true);
43-
foreach ($serverConfigPrefixes as $configPrefix) {
44-
$this->backends[$configPrefix] =
45-
new Group_LDAP($this->getAccess($configPrefix), $this->groupPluginManager, $this->config, $this->ncUserManager);
46-
if (is_null($this->refBackend)) {
47-
$this->refBackend = $this->backends[$configPrefix];
48-
}
49-
}
5036

51-
$this->isSetUp = true;
37+
protected function newInstance(string $configPrefix): Group_LDAP {
38+
return new Group_LDAP($this->getAccess($configPrefix), $this->groupPluginManager, $this->config, $this->ncUserManager);
5239
}
5340

5441
/**
@@ -144,9 +131,7 @@ public function getUserGroups($uid) {
144131
$groups = [];
145132
foreach ($this->backends as $backend) {
146133
$backendGroups = $backend->getUserGroups($uid);
147-
if (is_array($backendGroups)) {
148-
$groups = array_merge($groups, $backendGroups);
149-
}
134+
$groups = array_merge($groups, $backendGroups);
150135
}
151136

152137
return array_values(array_unique($groups));

apps/user_ldap/lib/Proxy.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,24 @@
1212
use OCP\ICache;
1313
use OCP\Server;
1414

15+
/**
16+
* @template T
17+
*/
1518
abstract class Proxy {
1619
/** @var array<string,Access> */
1720
private static array $accesses = [];
1821
private ?bool $isSingleBackend = null;
1922
private ?ICache $cache = null;
2023

24+
/** @var T[] */
25+
protected array $backends = [];
26+
/** @var ?T */
27+
protected $refBackend = null;
28+
29+
protected bool $isSetUp = false;
30+
2131
public function __construct(
32+
private Helper $helper,
2233
private ILDAPWrapper $ldap,
2334
private AccessFactory $accessFactory,
2435
) {
@@ -28,6 +39,36 @@ public function __construct(
2839
}
2940
}
3041

42+
protected function setup(): void {
43+
if ($this->isSetUp) {
44+
return;
45+
}
46+
47+
$serverConfigPrefixes = $this->helper->getServerConfigurationPrefixes(true);
48+
foreach ($serverConfigPrefixes as $configPrefix) {
49+
$this->backends[$configPrefix] = $this->newInstance($configPrefix);
50+
51+
if (is_null($this->refBackend)) {
52+
$this->refBackend = $this->backends[$configPrefix];
53+
}
54+
}
55+
56+
$this->isSetUp = true;
57+
}
58+
59+
/**
60+
* @return T
61+
*/
62+
abstract protected function newInstance(string $configPrefix): object;
63+
64+
/**
65+
* @return T
66+
*/
67+
public function getBackend(string $configPrefix): object {
68+
$this->setup();
69+
return $this->backends[$configPrefix];
70+
}
71+
3172
private function addAccess(string $configPrefix): void {
3273
$userMap = Server::get(UserMapping::class);
3374
$groupMap = Server::get(GroupMapping::class);

0 commit comments

Comments
 (0)