Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LDAP password policy #5364

Merged
merged 10 commits into from Oct 10, 2016
28 changes: 28 additions & 0 deletions plugins/password/config.inc.php.dist
Expand Up @@ -270,6 +270,34 @@ $config['password_ldap_samba_pwattr'] = '';
// Whenever the password is changed, the attribute will be updated if set
$config['password_ldap_samba_lchattr'] = '';

// LDAP PPolicy Driver options
// -----------------------------------

// LDAP Change password command - filename of the perl script
// Example: 'change_ldap_pass.pl'
$config['password_ldap_ppolicy_cmd'] = 'change_ldap_pass.pl';

// LDAP URI
// Example: 'ldap://ldap.example.com/ ldaps://ldap2.example.com:636/'
$config['password_ldap_uri'] = 'ldap://localhost/ ;

// LDAP base name (root directory)
// Exemple: 'dc=exemple,dc=com'
$config['password_ldap_basedn'] = 'dc=example,dc=com';

$config['password_ldap_searchDN'] = 'cn=someuser,dc=example,dc=com';

$config['password_ldap_searchPW'] = 'secret';

// LDAP search filter
// Example: '(uid=%login)'
// Example: '(&(objectClass=posixAccount)(uid=%login))'
$config['password_ldap_search_filter'] = '(uid=%login)';

// CA Certificate file if in URI is LDAPS connection
$config['password_ldap_cafile'] = '/etc/ssl/cacert.crt';



// DirectAdmin Driver options
// --------------------------
Expand Down
10 changes: 6 additions & 4 deletions plugins/password/drivers/ldap.php
Expand Up @@ -187,12 +187,14 @@ function search_userdn($rcmail)
);

$result = $ldap->search($base, $filter, $options);
$ldap->done();
if (is_a($result, 'PEAR_Error') || ($result->count() != 1)) {
return '';
$dn = '';
} else {
$dn = $result->current()->dn();
}

return $result->current()->dn();
$ldap->done();

return $dn;
}

/**
Expand Down
93 changes: 93 additions & 0 deletions plugins/password/drivers/ldap_ppolicy.php
@@ -0,0 +1,93 @@
<?php

/**
* ldap_ppolicy driver
*
* Driver that adds functionality to change the user password via
* the 'change_ldap_pass.pl' command respecting password policy (history) in LDAP.
*
*
* @version 1.0
* @author Zbigniew Szmyd <zbigniew.szmyd@linseco.pl>
*

*/

class rcube_ldap_ppolicy_password
{
public function save($currpass, $newpass)
{
$rcmail = rcmail::get_instance();
$this->debug = $rcmail->config->get('ldap_debug');

$cmd = $rcmail->config->get('password_ldap_ppolicy_cmd');
$uri = $rcmail->config->get('password_ldap_uri');
$baseDN = $rcmail->config->get('password_ldap_basedn');
$filter = $rcmail->config->get('password_ldap_search_filter');
$bindDN = $rcmail->config->get('password_ldap_searchDN');
$bindPW = $rcmail->config->get('password_ldap_searchPW');
$cafile = $rcmail->config->get('password_ldap_cafile');

$descriptorspec = array(
0 => array("pipe", "r"), // stdin is a pipe that the child will read from
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
2 => array("file", "/tmp/ppolicy_ldap.err", "a") // stderr is a file to write to
);

$cwd = 'plugins/password/drivers/';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be a path to helpers directory? Also does the command need to be configurable at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to logs directory. This can be useful in case of any errors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all you should use log_dir option to get the logs dir location. Second, I was talking about 'plugins/password/drivers/'.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry, I missed that path is not correct and log_dir should be get in propper way now.

$cmd = $cwd.$cmd;
$this->_debug('CWD: '. $cwd);
$this->_debug("parameters:\ncmd:$cmd\nuri:$uri\nbaseDN:$baseDN\nfilter:$filter");
$process = proc_open($cmd, $descriptorspec, $pipes);

if (is_resource($process)) {
// $pipes now looks like this:
// 0 => writeable handle connected to child stdin
// 1 => readable handle connected to child stdout
// Any error output will be appended to /tmp/error-output.txt

fwrite($pipes[0], $uri."\n");
fwrite($pipes[0], $baseDN."\n");
fwrite($pipes[0], $filter."\n");
fwrite($pipes[0], $bindDN."\n");
fwrite($pipes[0], $bindPW."\n");
fwrite($pipes[0], $_SESSION['username']."\n");
fwrite($pipes[0], $currpass."\n");
fwrite($pipes[0], $newpass."\n");
fwrite($pipes[0], $cafile);
fclose($pipes[0]);

$result = stream_get_contents($pipes[1]);
fclose($pipes[1]);

$this->_debug('Result:'.$result);

switch ($result) {
case "OK":
return PASSWORD_SUCCESS;
case "Password is in history of old passwords":
return PASSWORD_IN_HISTORY;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PASSWORD_IN_HISTORY constant does not exist. Did you forget to commit password.php changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right. I've made new commit with it.


case "Cannot connect to any server":
return PASSWORD_CONNECT_ERROR;
default:
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => $result
), true, false);
}

return PASSWORD_ERROR;
}
}

private function _debug($str)
{
if ($this->debug) {
rcube::write_log('ldap_ppolicy', $str);
}
}

}
89 changes: 89 additions & 0 deletions plugins/password/helpers/change_ldap_pass.pl
@@ -0,0 +1,89 @@
#!/usr/bin/perl
=pod
Script to change the LDAP password using the set_password method
to proper setting the password policy attributes
author: Zbigniew Szmyd (zbigniew.szmyd@linseco.pl)
version 1.0 2016-02-22
=cut

use Net::LDAP;
use Net::LDAP::Extension::SetPassword;
use URI;
use utf8;
binmode(STDOUT, ':utf8');

my %PAR = ();
if (my $param = shift @ARGV){
print "Password change in LDAP\n\n";
print "Run script without any parameter and pass the following data:\n";
print "URI\nbaseDN\nFilter\nbindDN\nbindPW\nLogin\nuserPass\nnewPass\nCAfile\n";
exit;
}

foreach my $param ('uri','base','filter','binddn','bindpw','user','pass','new_pass','ca'){
$PAR{$param} = <>;
$PAR{$param} =~ s/\r|\n//g;
}

my @servers = split (/\s+/, $PAR{'uri'});
my $active_server = 0;

my $ldap;
while ((my $serwer = shift @servers) && !($active_server)){
my $ldap_uri = URI->new($serwer);
if ($ldap_uri->secure){
$ldap = Net::LDAP->new($ldap_uri->as_string,
version => 3,
verify => 'require',
sslversion => 'tlsv1',
cafile => $PAR{'ca'});
} else {
$ldap = Net::LDAP->new($ldap_uri->as_string, version => 3);
}
$active_server = 1 if ($ldap);
}

if ($active_server){
my $mesg = $ldap->bind( $PAR{'binddn'}, password => $PAR{'bindpw'} );
if ($mesg->code != 0){
print "Cannot login: ". $mesg->error;
} else {
# Wyszukanie usera wg filtra
$PAR{'filter'} =~ s/\%login/$PAR{'user'}/;
my @search_args = (base => $PAR{'base'},
scope => 'sub',
filter => $PAR{'filter'},
attrs => ['1.1'],
);
my $result = $ldap->search( @search_args );
if ($result->code){
print $result->error;
} else {
my $count = $result->count;
if ($count == 1){
my @users = $result->entries;
my $dn = $users[0]->dn();
$result = $ldap->bind($dn, password => $PAR{'pass'});
if ($result->code){
print $result->error;
} else {
$result = $ldap->set_password(
newpasswd => $PAR{'new_pass'},
);
if ($result->code){
print $result->error;
} else {
print "OK";
}
}
} else {
print "User not found in LDAP\n" if $count == 0;
print "Found $count users\n";
}

}
}
$ldap->unbind();
} else {
print "Cannot connect to any server";
}
1 change: 1 addition & 0 deletions plugins/password/localization/en_GB.inc
Expand Up @@ -31,3 +31,4 @@ $messages['passwordweak'] = 'Password must include at least one number and one s
$messages['passwordforbidden'] = 'Password contains forbidden characters.';
$messages['firstloginchange'] = 'This is your first login. Please change your password.';
$messages['disablednotice'] = 'The system is currently under maintenance and password change is not possible at the moment. Everything should be back to normal soon. We apologise for any inconvenience.';
$messages['passwdinhistory'] = 'This password has already been used previously';
1 change: 1 addition & 0 deletions plugins/password/localization/en_US.inc
Expand Up @@ -35,3 +35,4 @@ $messages['passwordweak'] = 'Password must include at least one number and one p
$messages['passwordforbidden'] = 'Password contains forbidden characters.';
$messages['firstloginchange'] = 'This is your first login. Please change your password.';
$messages['disablednotice'] = 'The system is currently under maintenance and password change is not possible at the moment. Everything should be back to normal soon. We apologize for any inconvenience.';
$messages['passwinhistory'] = 'This password has already been used previously';
1 change: 1 addition & 0 deletions plugins/password/localization/pl_PL.inc
Expand Up @@ -31,3 +31,4 @@ $messages['passwordweak'] = 'Hasło musi zawierać co najmniej jedną cyfrę i z
$messages['passwordforbidden'] = 'Hasło zawiera niedozwolone znaki.';
$messages['firstloginchange'] = 'To jest twoje pierwsze logowanie. Proszę zmień hasło.';
$messages['disablednotice'] = 'System jest w trakcie konserwacji i zmiana hasła w tym momencie nie jest możliwa. Wszystko powinno wrócić do normy w niedługim czasie. Przepraszamy za wszelkie niedogodności';
$messages['passwdinhistory'] = 'To hasło było już użyte poprzednio.';
4 changes: 4 additions & 0 deletions plugins/password/password.php
Expand Up @@ -25,6 +25,7 @@
define('PASSWORD_CRYPT_ERROR', 1);
define('PASSWORD_ERROR', 2);
define('PASSWORD_CONNECT_ERROR', 3);
define('PASSWORD_IN_HISTORY', 4);
define('PASSWORD_SUCCESS', 0);

/**
Expand Down Expand Up @@ -333,6 +334,9 @@ private function _save($curpass, $passwd)
case PASSWORD_CONNECT_ERROR:
$reason = $this->gettext('connecterror');
break;
case PASSWORD_IN_HISTORY:
$reason = $this->gettext('passwdinhistory');
break;
case PASSWORD_ERROR:
default:
$reason = $this->gettext('internalerror');
Expand Down