Skip to content

Commit 0ea6521

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 38a0aaf commit 0ea6521

File tree

5 files changed

+108
-31
lines changed

5 files changed

+108
-31
lines changed

lib/LDAPConnect.php

Lines changed: 28 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,29 @@ 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+
if (is_null($this->passwdSupport)) {
158+
$ret = ldap_read($connection, '', '(objectclass=*)', ['supportedExtension']);
159+
if ($ret === false) {
160+
$this->passwdSupport = false;
161+
return false;
162+
}
163+
$ret = ldap_first_entry($connection, $ret);
164+
if ($ret === false) {
165+
$this->passwdSupport = false;
166+
return false;
167+
}
168+
169+
$values = ldap_get_values($connection, $ret, 'supportedExtension');
170+
$this->passwdSupport = ($values !== false) && in_array(LDAP_EXOP_MODIFY_PASSWD, $values['supportedExtension']);
171+
}
172+
return $this->passwdSupport;
173+
}
146174
}

lib/LDAPUserManager.php

Lines changed: 61 additions & 31 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

@@ -222,6 +222,10 @@ public function createUser($username, $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($username, $password, $base);
226230
$newUserDN = $this->ldapProvider->sanitizeDN([$newUserDN])[0];
227231
$this->ensureAttribute($newUserEntry, $displayNameAttribute, $username);
@@ -244,27 +248,8 @@ public function createUser($username, $password) {
244248
throw new \Exception('Cannot create user: ' . ldap_error($connection), ldap_errno($connection));
245249
}
246250

247-
// Set password through ldap password exop, if supported
248251
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-
}
252+
$this->setPassword($username, $password, $connection);
268253
}
269254
return $ret ? $newUserDN : false;
270255
}
@@ -350,30 +335,75 @@ public function deleteUser($uid): bool {
350335
return $res;
351336
}
352337

338+
/**
339+
* checks whether the user is allowed to change their password in Nextcloud
340+
*
341+
* @return boolean either the user can or cannot
342+
*/
343+
public function canSetPassword(): bool {
344+
return $this->configuration->hasPasswordPermission();
345+
}
346+
353347
/**
354348
* Set password
355349
*
356350
* @param string $uid The username
357351
* @param string $password The new password
352+
* @param ?\LDAP\Connection $connection LDAP connection or null to create one
358353
* @return bool
359354
*
360355
* Change the password of a user
361356
*/
362-
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;
357+
public function setPassword($uid, $password, $connection = null) {
358+
if (is_null($connection)) {
359+
$connection = $this->ldapProvider->getLDAPConnection($uid);
368360
}
361+
369362
try {
370-
$cr = $this->ldapProvider->getLDAPConnection($uid);
371363
$userDN = $this->getUserDN($uid);
372-
return ldap_exop_passwd($cr, $userDN, '', $password) !== false;
364+
$ret = false;
365+
366+
// try ldap_exop_passwd first
367+
if ($this->ldapConnect->hasPasswdExopSupport($connection)) {
368+
$ret = ldap_exop_passwd($connection, $userDN, '', $password);
369+
if ($ret === false) {
370+
$message = 'Failed to set password for user {dn} using ldap_exop_passwd';
371+
$this->logger->error($message, [
372+
'ldap_error' => ldap_error($connection),
373+
'app' => Application::APP_ID,
374+
'dn' => $userDN,
375+
]);
376+
} else {
377+
// `ldap_exop_passwd` is either FALSE or the password, in the later case return TRUE
378+
$ret = true;
379+
}
380+
} else {
381+
// Fallback to `userPassword` in case the server does not support exop_passwd
382+
$entry = [];
383+
if ($this->configuration->useUnicodePassword()) {
384+
$entry['unicodePwd'] = iconv('UTF-8', 'UTF-16LE', '"' . $password . '"');
385+
} else {
386+
$entry['userPassword'] = $password;
387+
}
388+
$ret = ldap_mod_replace($connection, $userDN, $entry);
389+
if ($ret === false) {
390+
$message = 'Failed to set password for user {dn} using ldap_mod_replace';
391+
$this->logger->error($message, [
392+
'ldap_error' => ldap_error($connection),
393+
'app' => Application::APP_ID,
394+
'dn' => $userDN,
395+
]);
396+
}
397+
}
398+
return $ret;
373399
} catch (\Exception $e) {
374-
$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => Application::APP_ID]);
400+
$this->logger->error('Exception occured while setting the password of user {dn}', [
401+
'app' => Application::APP_ID,
402+
'exception' => $e,
403+
'dn' => $uid
404+
]);
405+
return false;
375406
}
376-
return false;
377407
}
378408

379409
/**

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
return new TemplateResponse(Application::APP_ID, 'settings-admin');

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)