Skip to content

Commit 7e415f8

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 40af77f commit 7e415f8

File tree

5 files changed

+120
-33
lines changed

5 files changed

+120
-33
lines changed

lib/LDAPConnect.php

Lines changed: 37 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,38 @@ 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+
$this->logger->debug(
162+
'Could not check passwd_exop support of LDAP host, host does not provide the supportedExtension entry.',
163+
[ 'app' => Application::APP_ID ]
164+
);
165+
return false;
166+
}
167+
168+
$ret = ldap_first_entry($connection, $ret);
169+
if ($ret === false) {
170+
$this->passwdSupport = false;
171+
$this->logger->error(
172+
'Could not check passwd_exop support of LDAP host, host returned malformed data for the supported ldap extension entry.',
173+
[ 'app' => Application::APP_ID ]
174+
);
175+
return false;
176+
}
177+
178+
$values = ldap_get_values($connection, $ret, 'supportedExtension');
179+
$this->passwdSupport = ($values !== false) && in_array(LDAP_EXOP_MODIFY_PASSWD, $values['supportedExtension']);
180+
}
181+
return $this->passwdSupport;
182+
}
146183
}

lib/LDAPUserManager.php

Lines changed: 64 additions & 33 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->setPassword($username, $password, $connection);
268254
}
269255
return $ret ? $newUserDN : false;
270256
}
@@ -350,37 +336,82 @@ 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
*
356351
* @param string $uid The username
357352
* @param string $password The new password
353+
* @param ?\LDAP\Connection $connection LDAP connection or null to create one
358354
* @return bool
359355
*
360356
* Change the password of a user
361357
*/
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;
358+
public function setPassword($uid, $password, $connection = null) {
359+
if (is_null($connection)) {
360+
$connection = $this->ldapProvider->getLDAPConnection($uid);
368361
}
362+
369363
try {
370-
$cr = $this->ldapProvider->getLDAPConnection($uid);
371364
$userDN = $this->getUserDN($uid);
372-
return ldap_exop_passwd($cr, $userDN, '', $password) !== false;
365+
$ret = false;
366+
367+
// try ldap_exop_passwd first
368+
if ($this->ldapConnect->hasPasswdExopSupport($connection)) {
369+
$ret = ldap_exop_passwd($connection, $userDN, '', $password);
370+
if ($ret === false) {
371+
$message = 'Failed to set password for user {dn} using ldap_exop_passwd';
372+
$this->logger->error($message, [
373+
'ldap_error' => ldap_error($connection),
374+
'app' => Application::APP_ID,
375+
'dn' => $userDN,
376+
]);
377+
} else {
378+
// `ldap_exop_passwd` is either FALSE or the password, in the later case return TRUE
379+
$ret = true;
380+
}
381+
} else {
382+
// Use ldap_mod_replace in case the server does not support exop_passwd
383+
$entry = [];
384+
if ($this->configuration->useUnicodePassword()) {
385+
$entry['unicodePwd'] = iconv('UTF-8', 'UTF-16LE', '"' . $password . '"');
386+
} else {
387+
$entry['userPassword'] = $password;
388+
}
389+
$ret = ldap_mod_replace($connection, $userDN, $entry);
390+
if ($ret === false) {
391+
$message = 'Failed to set password for user {dn} using ldap_mod_replace';
392+
$this->logger->error($message, [
393+
'ldap_error' => ldap_error($connection),
394+
'app' => Application::APP_ID,
395+
'dn' => $userDN,
396+
]);
397+
}
398+
}
399+
return $ret;
373400
} catch (\Exception $e) {
374-
$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => Application::APP_ID]);
401+
$this->logger->error('Exception occured while setting the password of user {uid}', [
402+
'app' => Application::APP_ID,
403+
'exception' => $e,
404+
'uid' => $uid
405+
]);
406+
return false;
375407
}
376-
return false;
377408
}
378409

379410
/**
380411
* get the user's home directory
381412
*
382413
* @param string $uid the username
383-
* @return boolean
414+
* @return bool
384415
*/
385416
public function getHome($uid) {
386417
// Not implemented

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)