Skip to content

Commit fc56e13

Browse files
committed
If the passwd exop is not supported allow setting unicodePwd for AD
* Do not rely on exop_passwd but check rootDSE for support and fallback to mod_replace Signed-off-by: Ferdinand Thiessen <[email protected]>
1 parent c261638 commit fc56e13

File tree

5 files changed

+137
-37
lines changed

5 files changed

+137
-37
lines changed

lib/LDAPConnect.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ class LDAPConnect {
3333
private $ldapConfig;
3434
/** @var LoggerInterface */
3535
private $logger;
36+
/** @var bool|null */
37+
private $passwdSupport;
3638

3739
public function __construct(Helper $ldapBackendHelper, LoggerInterface $logger) {
40+
$this->passwdSupport = null;
3841
$this->logger = $logger;
3942
$ldapConfigPrefixes = $ldapBackendHelper->getServerConfigurationPrefixes(true);
4043
$prefix = array_shift($ldapConfigPrefixes);
@@ -143,4 +146,39 @@ public function hasPasswordPolicy(): bool {
143146
$ppDN = $this->ldapConfig->ldapDefaultPPolicyDN;
144147
return !empty($ppDN);
145148
}
149+
150+
/**
151+
* checks whether the LDAP server supports the passwd exop
152+
*
153+
* @param \LDAP\Connection $connection LDAP connection to check
154+
* @return boolean either the user can or cannot
155+
*/
156+
public function hasPasswdExopSupport($connection): bool {
157+
// TODO: We should cache this by ldap prefix, but currently we have no access to it.
158+
if (is_null($this->passwdSupport)) {
159+
$ret = ldap_read($connection, '', '(objectclass=*)', ['supportedExtension']);
160+
if ($ret === false) {
161+
$this->passwdSupport = false;
162+
$this->logger->debug(
163+
'Could not check passwd_exop support of LDAP host, host does not provide the supportedExtension entry.',
164+
[ 'app' => Application::APP_ID ]
165+
);
166+
return false;
167+
}
168+
169+
$ret = ldap_first_entry($connection, $ret);
170+
if ($ret === false) {
171+
$this->passwdSupport = false;
172+
$this->logger->error(
173+
'Could not check passwd_exop support of LDAP host, host returned malformed data for the supported ldap extension entry.',
174+
[ 'app' => Application::APP_ID ]
175+
);
176+
return false;
177+
}
178+
179+
$values = ldap_get_values($connection, $ret, 'supportedExtension');
180+
$this->passwdSupport = ($values !== false) && in_array(LDAP_EXOP_MODIFY_PASSWD, $values);
181+
}
182+
return $this->passwdSupport;
183+
}
146184
}

lib/LDAPUserManager.php

Lines changed: 80 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public function __construct(IUserManager $userManager, IUserSession $userSession
8484
* @return int bitwise-or'ed actions
8585
*/
8686
public function respondToActions() {
87-
$setPassword = function_exists('ldap_exop_passwd') && !$this->ldapConnect->hasPasswordPolicy()
87+
$setPassword = $this->canSetPassword() && !$this->ldapConnect->hasPasswordPolicy()
8888
? Backend::SET_PASSWORD
8989
: 0;
9090

@@ -140,7 +140,7 @@ public function setDisplayName($uid, $displayName) {
140140
* checks whether the user is allowed to change his avatar in Nextcloud
141141
*
142142
* @param string $uid the Nextcloud user name
143-
* @return boolean either the user can or cannot
143+
* @return bool either the user can or cannot
144144
*/
145145
public function canChangeAvatar($uid) {
146146
return $this->configuration->hasAvatarPermission();
@@ -222,7 +222,12 @@ public function createUser($uid, $password) {
222222
$displayNameAttribute = $this->ldapConnect->getDisplayNameAttribute();
223223
}
224224

225+
if ($connection === false) {
226+
throw new \Exception('Could not bind to LDAP server');
227+
}
228+
225229
[$newUserDN, $newUserEntry] = $this->buildNewEntry($uid, $password, $base);
230+
226231
$newUserDN = $this->ldapProvider->sanitizeDN([$newUserDN])[0];
227232
$this->ensureAttribute($newUserEntry, $displayNameAttribute, $uid);
228233

@@ -244,27 +249,8 @@ public function createUser($uid, $password) {
244249
throw new \Exception('Cannot create user: ' . ldap_error($connection), ldap_errno($connection));
245250
}
246251

247-
// Set password through ldap password exop, if supported
248252
if ($this->respondToActions() & Backend::SET_PASSWORD) {
249-
try {
250-
$ret = ldap_exop_passwd($connection, $newUserDN, '', $password);
251-
if ($ret === false) {
252-
$message = 'ldap_exop_passwd failed, falling back to ldap_mod_replace to to set password for new user';
253-
$this->logger->debug($message, ['app' => Application::APP_ID]);
254-
255-
// Fallback to `userPassword` in case the server does not support exop_passwd
256-
$ret = ldap_mod_replace($connection, $newUserDN, ['userPassword' => $password]);
257-
if ($ret === false) {
258-
$message = 'Failed to set password for new user {dn}';
259-
$this->logger->error($message, [
260-
'app' => Application::APP_ID,
261-
'dn' => $newUserDN,
262-
]);
263-
}
264-
}
265-
} catch (\Exception $e) {
266-
$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => Application::APP_ID]);
267-
}
253+
$this->handleSetPassword($newUserDN, $password, $connection);
268254
}
269255
return $ret ? $newUserDN : false;
270256
}
@@ -350,6 +336,15 @@ public function deleteUser($uid): bool {
350336
return $res;
351337
}
352338

339+
/**
340+
* checks whether the user is allowed to change their password in Nextcloud
341+
*
342+
* @return bool either the user can or cannot
343+
*/
344+
public function canSetPassword(): bool {
345+
return $this->configuration->hasPasswordPermission();
346+
}
347+
353348
/**
354349
* Set password
355350
*
@@ -360,27 +355,17 @@ public function deleteUser($uid): bool {
360355
* Change the password of a user
361356
*/
362357
public function setPassword($uid, $password) {
363-
if (!function_exists('ldap_exop_passwd')) {
364-
// since PHP 7.2 – respondToActions checked this already, this
365-
// method should not be called. Double check due to public scope.
366-
// This method can be removed when Nextcloud 16 compat is dropped.
367-
return false;
368-
}
369-
try {
370-
$cr = $this->ldapProvider->getLDAPConnection($uid);
371-
$userDN = $this->getUserDN($uid);
372-
return ldap_exop_passwd($cr, $userDN, '', $password) !== false;
373-
} catch (\Exception $e) {
374-
$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => Application::APP_ID]);
375-
}
376-
return false;
358+
$connection = $this->ldapProvider->getLDAPConnection($uid);
359+
$userDN = $this->getUserDN($uid);
360+
361+
return $this->handleSetPassword($userDN, $password, $connection);
377362
}
378363

379364
/**
380365
* get the user's home directory
381366
*
382367
* @param string $uid the username
383-
* @return boolean
368+
* @return bool
384369
*/
385370
public function getHome($uid) {
386371
// Not implemented
@@ -444,4 +429,62 @@ public function changeUserHook(IUser $user, string $feature, $attr1, $attr2): vo
444429
private function getUserDN($uid): string {
445430
return $this->ldapProvider->getUserDN($uid);
446431
}
432+
433+
/**
434+
* Handle setting user password password
435+
*
436+
* @param string $userDN The username
437+
* @param string $password The new password
438+
* @param \LDAP\Connection $connection The LDAP connection to use
439+
* @return bool
440+
*
441+
* Change the password of a user
442+
*/
443+
private function handleSetPassword(string $userDN, string $password, \LDAP\Connection $connection): bool {
444+
try {
445+
$ret = false;
446+
447+
// try ldap_exop_passwd first
448+
if ($this->ldapConnect->hasPasswdExopSupport($connection)) {
449+
if (ldap_exop_passwd($connection, $userDN, '', $password) === true) {
450+
// `ldap_exop_passwd` is either FALSE or the password, in the later case return TRUE
451+
return true;
452+
}
453+
454+
$message = 'Failed to set password for user {dn} using ldap_exop_passwd';
455+
$this->logger->error($message, [
456+
'ldap_error' => ldap_error($connection),
457+
'app' => Application::APP_ID,
458+
'dn' => $userDN,
459+
]);
460+
} else {
461+
// Use ldap_mod_replace in case the server does not support exop_passwd
462+
$entry = [];
463+
if ($this->configuration->useUnicodePassword()) {
464+
$entry['unicodePwd'] = iconv('UTF-8', 'UTF-16LE', '"' . $password . '"');
465+
} else {
466+
$entry['userPassword'] = $password;
467+
}
468+
469+
if(ldap_mod_replace($connection, $userDN, $entry)) {
470+
return true;
471+
}
472+
473+
$message = 'Failed to set password for user {dn} using ldap_mod_replace';
474+
$this->logger->error($message, [
475+
'ldap_error' => ldap_error($connection),
476+
'app' => Application::APP_ID,
477+
'dn' => $userDN,
478+
]);
479+
}
480+
return false;
481+
} catch (\Exception $e) {
482+
$this->logger->error('Exception occured while setting the password of user {dn}', [
483+
'app' => Application::APP_ID,
484+
'exception' => $e,
485+
'dn' => $userDN,
486+
]);
487+
return false;
488+
}
489+
}
447490
}

lib/Service/Configuration.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ public function hasAvatarPermission(): bool {
4848
return $this->config->getAppValue('ldap_write_support', 'hasAvatarPermission', '1') === '1';
4949
}
5050

51+
public function hasPasswordPermission(): bool {
52+
return $this->config->getAppValue('ldap_write_support', 'hasPasswordPermission', '1') === '1';
53+
}
54+
55+
public function useUnicodePassword(): bool {
56+
return $this->config->getAppValue('ldap_write_support', 'useUnicodePassword', '0') === '1';
57+
}
58+
5159
public function getUserTemplate() {
5260
return $this->config->getAppValue(
5361
Application::APP_ID,

lib/Settings/Admin.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ public function getForm() {
6161
'createRequireActorFromLdap' => $this->config->isLdapActorRequired(),
6262
'createPreventFallback' => $this->config->isPreventFallback(),
6363
'hasAvatarPermission' => $this->config->hasAvatarPermission(),
64+
'hasPasswordPermission' => $this->config->hasPasswordPermission(),
6465
'newUserRequireEmail' => $this->config->isRequireEmail(),
6566
'newUserGenerateUserID' => $this->config->isGenerateUserId(),
67+
'useUnicodePassword' => $this->config->useUnicodePassword(),
6668
]
6769
);
6870

src/components/AdminSettings.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@
4444
@change.stop.prevent="toggleSwitch('hasAvatarPermission', !switches.hasAvatarPermission)">
4545
{{ t('ldap_write_support', 'Allow users to set their avatar') }}
4646
</NcActionCheckbox>
47+
<NcActionCheckbox :checked="switches.hasPasswordPermission"
48+
@change.stop.prevent="toggleSwitch('hasPasswordPermission', !switches.hasPasswordPermission)">
49+
{{ t('ldap_write_support', 'Allow users to set their password') }}
50+
</NcActionCheckbox>
51+
<NcActionCheckbox :checked="switches.useUnicodePassword"
52+
:title="t('ldap_write_support', 'If the server does not support the modify password extended operation use the `unicodePwd` instead of the `userPassword` attribute for setting the password')"
53+
@change.stop.prevent="toggleSwitch('useUnicodePassword', !switches.useUnicodePassword)">
54+
{{ t('ldap_write_support', 'Use the `unicodePwd` attribute for setting the user password') }}
55+
</NcActionCheckbox>
4756
</ul>
4857
<h3>{{ t('ldap_write_support', 'User template') }}</h3>
4958
<p>{{ t('ldap_write_support', 'LDIF template for creating users. Following placeholders may be used') }}</p>

0 commit comments

Comments
 (0)