2017-01-15 14:18:01 +01:00
|
|
|
|
##@file
|
|
|
|
|
# Extends Net::LDAP
|
|
|
|
|
package Lemonldap::NG::Portal::Lib::Net::LDAP;
|
|
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
|
use Net::LDAP; #inherits
|
|
|
|
|
use Net::LDAP::Util qw(escape_filter_value);
|
|
|
|
|
use base qw(Net::LDAP);
|
|
|
|
|
use Lemonldap::NG::Portal::Main::Constants ':all';
|
2021-09-29 11:36:58 +02:00
|
|
|
|
use Net::LDAP::Control::PasswordPolicy;
|
2017-01-15 14:18:01 +01:00
|
|
|
|
use Encode;
|
|
|
|
|
use Unicode::String qw(utf8);
|
|
|
|
|
use Scalar::Util 'weaken';
|
2020-10-27 11:56:38 +01:00
|
|
|
|
use IO::Socket::Timeout;
|
2018-11-14 10:15:28 +01:00
|
|
|
|
use utf8;
|
2017-01-15 14:18:01 +01:00
|
|
|
|
|
2021-09-30 23:13:04 +02:00
|
|
|
|
our $VERSION = '2.0.14';
|
2017-01-15 14:18:01 +01:00
|
|
|
|
|
|
|
|
|
# INITIALIZATION
|
|
|
|
|
|
|
|
|
|
# Build a Net::LDAP object using parameters issued from $portal
|
|
|
|
|
sub new {
|
|
|
|
|
my ( $class, $args ) = @_;
|
|
|
|
|
my $portal = $args->{p} or die "$class : p argument required !";
|
|
|
|
|
my $conf = $args->{conf} or die "$class : conf argument required !";
|
|
|
|
|
my $self;
|
|
|
|
|
my $useTls = 0;
|
|
|
|
|
my $tlsParam;
|
|
|
|
|
my @servers = ();
|
|
|
|
|
foreach my $server ( split /[\s,]+/, $conf->{ldapServer} ) {
|
|
|
|
|
|
|
|
|
|
if ( $server =~ m{^ldap\+tls://([^/]+)/?\??(.*)$} ) {
|
|
|
|
|
$useTls = 1;
|
|
|
|
|
$server = $1;
|
|
|
|
|
$tlsParam = $2 || "";
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$useTls = 0;
|
|
|
|
|
}
|
|
|
|
|
push @servers, $server;
|
|
|
|
|
}
|
|
|
|
|
$self = Net::LDAP->new(
|
|
|
|
|
\@servers,
|
2020-10-09 12:22:28 +02:00
|
|
|
|
onerror => undef,
|
|
|
|
|
keepalive => 1,
|
2017-01-15 14:18:01 +01:00
|
|
|
|
( $conf->{ldapPort} ? ( port => $conf->{ldapPort} ) : () ),
|
|
|
|
|
( $conf->{ldapTimeout} ? ( timeout => $conf->{ldapTimeout} ) : () ),
|
|
|
|
|
( $conf->{ldapVersion} ? ( version => $conf->{ldapVersion} ) : () ),
|
|
|
|
|
( $conf->{ldapRaw} ? ( raw => $conf->{ldapRaw} ) : () ),
|
2020-07-29 19:13:09 +02:00
|
|
|
|
( $conf->{ldapCAFile} ? ( cafile => $conf->{ldapCAFile} ) : () ),
|
|
|
|
|
( $conf->{ldapCAPath} ? ( capath => $conf->{ldapCAPath} ) : () ),
|
|
|
|
|
( $conf->{ldapVerify} ? ( verify => $conf->{ldapVerify} ) : () ),
|
2017-01-15 14:18:01 +01:00
|
|
|
|
);
|
|
|
|
|
unless ($self) {
|
2017-02-15 07:41:50 +01:00
|
|
|
|
$portal->logger->error($@);
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return 0;
|
|
|
|
|
}
|
2020-07-29 19:13:09 +02:00
|
|
|
|
elsif ( $Net::LDAP::VERSION < '0.64' ) {
|
|
|
|
|
|
|
|
|
|
# CentOS7 has a bug in which IO::Socket::SSL will return a broken
|
|
|
|
|
# socket when certificate validation fails. Net::LDAP does not catch
|
|
|
|
|
# it, and the process ends up crashing.
|
|
|
|
|
# As a precaution, make sure the underlying socket is doing fine:
|
2020-10-09 12:22:28 +02:00
|
|
|
|
if ( $self->socket->isa('IO::Socket::SSL')
|
2020-07-29 19:13:09 +02:00
|
|
|
|
and $self->socket->errstr < 0 )
|
|
|
|
|
{
|
|
|
|
|
$portal->logger->error(
|
|
|
|
|
"SSL connection error: " . $self->socket->errstr );
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-01-15 14:18:01 +01:00
|
|
|
|
bless $self, $class;
|
2020-10-27 11:56:38 +01:00
|
|
|
|
|
|
|
|
|
# Set socket timeouts
|
|
|
|
|
my $socket = $self->socket;
|
|
|
|
|
IO::Socket::Timeout->enable_timeouts_on($socket);
|
|
|
|
|
$socket->read_timeout( $conf->{ldapIOTimeout} );
|
|
|
|
|
$socket->write_timeout( $conf->{ldapIOTimeout} );
|
|
|
|
|
|
2017-01-15 14:18:01 +01:00
|
|
|
|
if ($useTls) {
|
|
|
|
|
my %h = split( /[&=]/, $tlsParam );
|
2020-07-29 19:13:09 +02:00
|
|
|
|
$h{cafile} ||= $conf->{ldapCAFile} if ( $conf->{ldapCAFile} );
|
|
|
|
|
$h{capath} ||= $conf->{ldapCAPath} if ( $conf->{ldapCAPath} );
|
|
|
|
|
$h{verify} ||= $conf->{ldapVerify} if ( $conf->{ldapVerify} );
|
2017-01-15 14:18:01 +01:00
|
|
|
|
my $mesg = $self->start_tls(%h);
|
|
|
|
|
if ( $mesg->code ) {
|
2020-07-29 19:13:09 +02:00
|
|
|
|
$portal->logger->error( 'StartTLS failed: ' . $mesg->error );
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$self->{portal} = $portal;
|
|
|
|
|
$self->{conf} = $conf;
|
|
|
|
|
weaken $self->{portal};
|
|
|
|
|
|
|
|
|
|
# Setting default LDAP password storage encoding to utf-8
|
|
|
|
|
return $self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# RUNNING METHODS
|
|
|
|
|
|
|
|
|
|
## @method Net::LDAP::Message bind(string dn, hash args)
|
|
|
|
|
# Reimplementation of Net::LDAP::bind(). Connection is done :
|
|
|
|
|
# - with $dn and $args->{password} as dn/password if defined,
|
|
|
|
|
# - or with Lemonldap::NG account,
|
|
|
|
|
# - or with an anonymous bind.
|
|
|
|
|
# @param $dn LDAP distinguish name
|
|
|
|
|
# @param %args See Net::LDAP(3) manpage for more
|
|
|
|
|
# @return Net::LDAP::Message
|
|
|
|
|
sub bind {
|
|
|
|
|
my ( $self, $dn, %args ) = @_;
|
2018-11-14 10:15:28 +01:00
|
|
|
|
|
2018-11-23 23:05:05 +01:00
|
|
|
|
$self->{portal}->logger->debug("Call bind for $dn") if $dn;
|
2018-11-14 10:15:28 +01:00
|
|
|
|
|
2017-01-15 14:18:01 +01:00
|
|
|
|
my $mesg;
|
|
|
|
|
unless ($dn) {
|
|
|
|
|
$dn = $self->{conf}->{managerDn};
|
|
|
|
|
$args{password} =
|
|
|
|
|
decode( 'utf-8', $self->{conf}->{managerPassword} );
|
|
|
|
|
}
|
|
|
|
|
if ( $dn && $args{password} ) {
|
|
|
|
|
if ( $self->{conf}->{ldapPwdEnc} ne 'utf-8' ) {
|
|
|
|
|
eval {
|
|
|
|
|
my $tmp = encode(
|
|
|
|
|
$self->{conf}->{ldapPwdEnc},
|
|
|
|
|
decode( 'utf-8', $args{password} )
|
|
|
|
|
);
|
|
|
|
|
$args{password} = $tmp;
|
|
|
|
|
};
|
|
|
|
|
print STDERR "$@\n" if ($@);
|
|
|
|
|
}
|
2021-09-29 11:36:58 +02:00
|
|
|
|
|
|
|
|
|
if ( $self->{conf}->{ldapPpolicyControl} ) {
|
|
|
|
|
my $pp = Net::LDAP::Control::PasswordPolicy->new();
|
|
|
|
|
$args{control} = [$pp];
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-15 14:18:01 +01:00
|
|
|
|
$mesg = $self->SUPER::bind( $dn, %args );
|
2021-09-29 11:36:58 +02:00
|
|
|
|
|
|
|
|
|
if ( $mesg->code ) {
|
|
|
|
|
my ($resp) = $mesg->control("1.3.6.1.4.1.42.2.27.8.5.1");
|
2021-09-30 23:13:04 +02:00
|
|
|
|
|
2021-09-29 11:36:58 +02:00
|
|
|
|
# Check for ppolicy error
|
2021-09-30 23:13:04 +02:00
|
|
|
|
my $pp_error = $resp->pp_error if ( defined($resp) );
|
2021-09-29 11:36:58 +02:00
|
|
|
|
if ( defined $pp_error ) {
|
|
|
|
|
my $ppolicy_error = [
|
|
|
|
|
"password expired",
|
|
|
|
|
"account locked",
|
|
|
|
|
"change after reset",
|
|
|
|
|
"password mod not allowed",
|
|
|
|
|
"supply old password",
|
|
|
|
|
"insufficient password quality",
|
|
|
|
|
"password too short",
|
|
|
|
|
"password too young",
|
2021-09-30 23:13:04 +02:00
|
|
|
|
"password in history"
|
2021-09-29 11:36:58 +02:00
|
|
|
|
]->[$pp_error];
|
|
|
|
|
|
2021-09-30 23:13:04 +02:00
|
|
|
|
$self->{portal}
|
|
|
|
|
->logger->error( "Error when binding to LDAP server: "
|
|
|
|
|
. $mesg->error
|
|
|
|
|
. " | extended ppolicy control response error: $ppolicy_error"
|
|
|
|
|
);
|
2021-09-29 11:36:58 +02:00
|
|
|
|
}
|
2021-09-30 23:13:04 +02:00
|
|
|
|
else {
|
|
|
|
|
$self->{portal}->logger->error(
|
|
|
|
|
"Error when binding to LDAP server: " . $mesg->error );
|
2021-09-29 11:36:58 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-01-15 14:18:01 +01:00
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$mesg = $self->SUPER::bind();
|
|
|
|
|
}
|
|
|
|
|
return $mesg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
## @method Net::LDAP::Message unbind()
|
|
|
|
|
# Reimplementation of Net::LDAP::unbind() to force call to disconnect()
|
|
|
|
|
# @return Net::LDAP::Message
|
|
|
|
|
sub unbind {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
my $ldap_uri = $self->uri;
|
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
|
$self->{portal}->logger->debug("Unbind and disconnect from $ldap_uri");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
|
|
|
|
|
my $mesg = $self->SUPER::unbind();
|
|
|
|
|
$self->SUPER::disconnect();
|
|
|
|
|
|
|
|
|
|
return $mesg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
## @method protected int userBind(string dn, hash args)
|
|
|
|
|
# Call bind() with dn/password and return
|
|
|
|
|
# @param $dn LDAP distinguish name
|
|
|
|
|
# @param %args See Net::LDAP(3) manpage for more
|
|
|
|
|
# @return Lemonldap::NG portal error code
|
|
|
|
|
sub userBind {
|
2019-06-06 16:00:49 +02:00
|
|
|
|
my $self = shift;
|
|
|
|
|
my $req = shift;
|
2017-01-15 14:18:01 +01:00
|
|
|
|
|
|
|
|
|
if ( $self->{conf}->{ldapPpolicyControl} ) {
|
|
|
|
|
|
|
|
|
|
# Create Control object
|
|
|
|
|
my $pp = Net::LDAP::Control::PasswordPolicy->new();
|
|
|
|
|
|
|
|
|
|
# Bind with user credentials
|
|
|
|
|
my $mesg = $self->bind( @_, control => [$pp] );
|
|
|
|
|
|
|
|
|
|
# Get server control response
|
|
|
|
|
my ($resp) = $mesg->control("1.3.6.1.4.1.42.2.27.8.5.1");
|
|
|
|
|
|
2021-09-29 11:36:58 +02:00
|
|
|
|
# Return direct unless control response
|
2017-01-15 14:18:01 +01:00
|
|
|
|
unless ( defined $resp ) {
|
|
|
|
|
if ( $mesg->code == 49 ) {
|
2020-07-29 19:13:09 +02:00
|
|
|
|
$self->{portal}->userLogger->warn(
|
|
|
|
|
"Bad password for $req->{user} (" . $req->address . ")" );
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return PE_BADCREDENTIALS;
|
|
|
|
|
}
|
2019-09-30 17:19:57 +02:00
|
|
|
|
elsif ( $mesg->code == 0 ) {
|
|
|
|
|
return PE_OK;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$self->{portal}->logger->error( "Bind failed with error "
|
|
|
|
|
. $mesg->code . ": "
|
|
|
|
|
. $mesg->error );
|
|
|
|
|
return PE_LDAPERROR;
|
|
|
|
|
}
|
2017-01-15 14:18:01 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Check for ppolicy error
|
|
|
|
|
my $pp_error = $resp->pp_error;
|
|
|
|
|
if ( defined $pp_error ) {
|
2017-02-15 15:16:59 +01:00
|
|
|
|
$self->{portal}->userLogger->error(
|
2019-02-07 09:27:56 +01:00
|
|
|
|
"Password policy error $pp_error for " . $req->user );
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return [
|
|
|
|
|
PE_PP_PASSWORD_EXPIRED,
|
|
|
|
|
PE_PP_ACCOUNT_LOCKED,
|
|
|
|
|
PE_PP_CHANGE_AFTER_RESET,
|
|
|
|
|
PE_PP_PASSWORD_MOD_NOT_ALLOWED,
|
|
|
|
|
PE_PP_MUST_SUPPLY_OLD_PASSWORD,
|
|
|
|
|
PE_PP_INSUFFICIENT_PASSWORD_QUALITY,
|
|
|
|
|
PE_PP_PASSWORD_TOO_SHORT,
|
|
|
|
|
PE_PP_PASSWORD_TOO_YOUNG,
|
|
|
|
|
PE_PP_PASSWORD_IN_HISTORY,
|
|
|
|
|
]->[$pp_error];
|
|
|
|
|
}
|
|
|
|
|
elsif ( $mesg->code == 0 ) {
|
|
|
|
|
|
|
|
|
|
# Get expiration warning and graces
|
|
|
|
|
if ( $resp->grace_authentications_remaining ) {
|
2020-03-10 10:50:02 +01:00
|
|
|
|
$self->{portal}->logger->debug(
|
|
|
|
|
"LDAP password policy - grace authentications remaining: "
|
|
|
|
|
. $resp->grace_authentications_remaining );
|
2019-04-01 09:58:56 +02:00
|
|
|
|
$req->info(
|
2019-04-01 06:52:21 +02:00
|
|
|
|
$self->{portal}->loadTemplate(
|
2019-06-28 13:40:56 +02:00
|
|
|
|
$req,
|
2017-10-10 13:04:40 +02:00
|
|
|
|
'ldapPpGrace',
|
|
|
|
|
params => {
|
|
|
|
|
number => $resp->grace_authentications_remaining
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-01-15 14:18:01 +01:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $resp->time_before_expiration ) {
|
2020-03-10 10:50:02 +01:00
|
|
|
|
$self->{portal}->logger->debug(
|
|
|
|
|
"LDAP password policy - time before expiration: "
|
|
|
|
|
. $resp->time_before_expiration );
|
2017-10-10 13:04:40 +02:00
|
|
|
|
$req->info(
|
2019-04-01 06:52:21 +02:00
|
|
|
|
$self->{portal}->loadTemplate(
|
2019-06-28 13:40:56 +02:00
|
|
|
|
$req,
|
2017-10-10 13:04:40 +02:00
|
|
|
|
'simpleInfo',
|
|
|
|
|
params => {
|
2020-03-10 11:16:43 +01:00
|
|
|
|
trspan => 'pwdWillExpire,'
|
2020-03-10 11:28:04 +01:00
|
|
|
|
. join(
|
|
|
|
|
',',
|
|
|
|
|
$self->convertSec(
|
|
|
|
|
$resp->time_before_expiration
|
|
|
|
|
)
|
2017-10-10 13:04:40 +02:00
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
2017-01-15 14:18:01 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return PE_OK;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
my $mesg = $self->bind(@_);
|
|
|
|
|
if ( $mesg->code == 0 ) {
|
|
|
|
|
return PE_OK;
|
|
|
|
|
}
|
2019-11-13 18:10:58 +01:00
|
|
|
|
else {
|
|
|
|
|
$req->data->{ldapError} = $mesg->error;
|
|
|
|
|
}
|
2017-01-15 14:18:01 +01:00
|
|
|
|
}
|
2020-07-29 19:13:09 +02:00
|
|
|
|
$self->{portal}->userLogger->warn(
|
|
|
|
|
"Bad password for $req->{user} (" . $req->address . ")" );
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return PE_BADCREDENTIALS;
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-28 18:24:55 +02:00
|
|
|
|
## @method int userModifyPassword(string dn, string newpassword, string oldpassword, boolean ad, boolean requireOldPassword)
|
2017-01-15 14:18:01 +01:00
|
|
|
|
# Change user's password.
|
|
|
|
|
# @param $dn DN
|
|
|
|
|
# @param $newpassword New password
|
|
|
|
|
# @param $oldpassword Current password
|
|
|
|
|
# @param $ad Active Directory mode
|
2020-04-28 18:24:55 +02:00
|
|
|
|
# @param $requireOldPassword Old password is needed to update
|
2017-01-15 14:18:01 +01:00
|
|
|
|
# @return Lemonldap::NG::Portal constant
|
|
|
|
|
sub userModifyPassword {
|
2020-04-27 22:08:12 +02:00
|
|
|
|
my ( $self, $dn, $newpassword, $oldpassword, $ad, $requireOldPassword ) =
|
|
|
|
|
@_;
|
|
|
|
|
my $ppolicyControl = $self->{conf}->{ldapPpolicyControl};
|
|
|
|
|
my $setPassword = $self->{conf}->{ldapSetPassword};
|
|
|
|
|
my $asUser = $self->{conf}->{ldapChangePasswordAsUser};
|
|
|
|
|
my $passwordAttribute = "userPassword";
|
2017-01-15 14:18:01 +01:00
|
|
|
|
my $err;
|
|
|
|
|
my $mesg;
|
|
|
|
|
|
2018-11-14 10:15:28 +01:00
|
|
|
|
utf8::downgrade($dn);
|
|
|
|
|
$self->{portal}->logger->debug("Call modify password for $dn");
|
|
|
|
|
|
2017-01-15 14:18:01 +01:00
|
|
|
|
# Adjust configuration for AD
|
|
|
|
|
if ($ad) {
|
|
|
|
|
$ppolicyControl = 0;
|
|
|
|
|
$setPassword = 0;
|
|
|
|
|
$passwordAttribute = "unicodePwd";
|
|
|
|
|
|
|
|
|
|
# Encode password for AD
|
|
|
|
|
$newpassword = utf8( chr(34) . $newpassword . chr(34) )->utf16le();
|
|
|
|
|
if ( $oldpassword and $asUser ) {
|
|
|
|
|
$oldpassword =
|
|
|
|
|
utf8( chr(34) . $oldpassword . chr(34) )->utf16le();
|
|
|
|
|
}
|
2017-02-15 07:41:50 +01:00
|
|
|
|
$self->{portal}->logger->debug("Active Directory mode enabled");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# First case: no ppolicy
|
|
|
|
|
if ( !$ppolicyControl ) {
|
|
|
|
|
|
|
|
|
|
if ($setPassword) {
|
|
|
|
|
|
|
|
|
|
# Bind as user if oldpassword and ldapChangePasswordAsUser
|
|
|
|
|
if ( $oldpassword and $asUser ) {
|
|
|
|
|
|
|
|
|
|
$mesg = $self->bind( $dn, password => $oldpassword );
|
|
|
|
|
if ( $mesg->code != 0 ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
|
$self->{portal}->userLogger->notice("Bad old password");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return PE_BADOLDPASSWORD;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Use SetPassword extended operation
|
|
|
|
|
require Net::LDAP::Extension::SetPassword;
|
|
|
|
|
$mesg =
|
|
|
|
|
($oldpassword)
|
|
|
|
|
? $self->set_password(
|
|
|
|
|
user => $dn,
|
|
|
|
|
oldpasswd => $oldpassword,
|
|
|
|
|
newpasswd => $newpassword
|
|
|
|
|
)
|
|
|
|
|
: $self->set_password(
|
|
|
|
|
user => $dn,
|
|
|
|
|
newpasswd => $newpassword
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
# Catch the "Unwilling to perform" error
|
|
|
|
|
if ( $mesg->code == 53 ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
|
$self->{portal}->userLogger->notice("Bad old password");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return PE_BADOLDPASSWORD;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
|
|
|
|
|
# AD specific
|
|
|
|
|
# Change password as user with a delete/add modification
|
|
|
|
|
if ( $ad and $oldpassword and $asUser ) {
|
|
|
|
|
$mesg = $self->modify(
|
|
|
|
|
$dn,
|
|
|
|
|
changes => [
|
|
|
|
|
delete => [ $passwordAttribute => $oldpassword ],
|
|
|
|
|
add => [ $passwordAttribute => $newpassword ]
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else {
|
|
|
|
|
if ($requireOldPassword) {
|
|
|
|
|
|
|
|
|
|
return PE_MUST_SUPPLY_OLD_PASSWORD if ( !$oldpassword );
|
|
|
|
|
|
|
|
|
|
# Check old password with a bind
|
|
|
|
|
$mesg = $self->bind( $dn, password => $oldpassword );
|
|
|
|
|
|
|
|
|
|
# For AD password expiration to work:
|
|
|
|
|
# ppolicy must be desactivated,
|
|
|
|
|
# and "change as user" must be desactivated
|
|
|
|
|
if ($ad) {
|
|
|
|
|
if ( $mesg->error =~ /LdapErr: .* data ([^,]+),.*/ ) {
|
|
|
|
|
|
|
|
|
|
# extended data message code:
|
|
|
|
|
# 532: password expired (but provided password is correct)
|
|
|
|
|
# 773: must change password at next connection (but provided password is correct)
|
|
|
|
|
# 52e: password is incorrect
|
|
|
|
|
unless ( ( $1 eq '532' ) || ( $1 eq '773' ) ) {
|
|
|
|
|
$self->{portal}
|
2017-02-15 07:41:50 +01:00
|
|
|
|
->userLogger->warn("Bad old password");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return PE_BADOLDPASSWORD;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# if error message has not been catched, then it IS a success
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{ # this is not AD, a 0 error code means good old password
|
|
|
|
|
if ( $mesg->code != 0 ) {
|
|
|
|
|
$self->{portal}
|
2017-02-15 07:41:50 +01:00
|
|
|
|
->userLogger->warn('Bad old password');
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return PE_BADOLDPASSWORD;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Rebind as Manager only if user is not granted to change its password
|
|
|
|
|
$self->bind() unless $asUser;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Use standard modification
|
|
|
|
|
$mesg =
|
|
|
|
|
$self->modify( $dn,
|
|
|
|
|
replace => { $passwordAttribute => $newpassword } );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$self->{portal}
|
2017-02-15 07:41:50 +01:00
|
|
|
|
->logger->debug( 'Modification return code: ' . $mesg->code );
|
2019-09-05 17:14:44 +02:00
|
|
|
|
$self->{portal}
|
|
|
|
|
->logger->debug( 'Modification return error: ' . $mesg->error );
|
|
|
|
|
|
|
|
|
|
# Manage specific errors for IBM Tivoli DS
|
2019-09-05 17:16:55 +02:00
|
|
|
|
if ( $self->{conf}->{ldapITDS} ) {
|
2019-09-05 17:14:44 +02:00
|
|
|
|
my $itds_code = $self->getITDSError($mesg);
|
|
|
|
|
return $itds_code unless ( $itds_code == PE_PASSWORD_OK );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Manage specific errors for Active Directory
|
|
|
|
|
if ($ad) {
|
|
|
|
|
return PE_PP_INSUFFICIENT_PASSWORD_QUALITY
|
|
|
|
|
if ( $mesg->code == 53 );
|
|
|
|
|
return PE_PP_PASSWORD_MOD_NOT_ALLOWED
|
|
|
|
|
if ( $mesg->code == 19 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Standard errors
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return PE_WRONGMANAGERACCOUNT
|
|
|
|
|
if ( $mesg->code == 50 || $mesg->code == 8 );
|
2019-09-30 17:19:57 +02:00
|
|
|
|
unless ( $mesg->code == 0 ) {
|
|
|
|
|
$self->{portal}
|
|
|
|
|
->logger->error( "Password modification failed with LDAP error "
|
|
|
|
|
. $mesg->code . ": "
|
|
|
|
|
. $mesg->error );
|
|
|
|
|
return PE_LDAPERROR;
|
|
|
|
|
}
|
2019-09-05 17:14:44 +02:00
|
|
|
|
|
2020-11-29 18:02:13 +01:00
|
|
|
|
$self->{portal}->logger->notice("Password changed for $dn");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
|
|
|
|
|
# Rebind as manager for next LDAP operations if we were bound as user
|
|
|
|
|
$self->bind() if $asUser;
|
|
|
|
|
|
|
|
|
|
return PE_PASSWORD_OK;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
|
|
|
|
|
# Create Control object
|
|
|
|
|
my $pp = Net::LDAP::Control::PasswordPolicy->new;
|
|
|
|
|
|
|
|
|
|
if ($setPassword) {
|
|
|
|
|
|
|
|
|
|
# Bind as user if oldpassword and ldapChangePasswordAsUser
|
|
|
|
|
if ( $oldpassword and $asUser ) {
|
|
|
|
|
|
|
|
|
|
$mesg = $self->bind(
|
|
|
|
|
$dn,
|
|
|
|
|
password => $oldpassword,
|
|
|
|
|
control => [$pp]
|
|
|
|
|
);
|
|
|
|
|
my ($bind_resp) = $mesg->control("1.3.6.1.4.1.42.2.27.8.5.1");
|
|
|
|
|
|
|
|
|
|
unless ( defined $bind_resp ) {
|
|
|
|
|
if ( $mesg->code != 0 ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
|
$self->{portal}->logger->debug("Bad old password");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return PE_BADOLDPASSWORD;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
|
|
|
|
|
# Check if password is expired
|
|
|
|
|
my $pp_error = $bind_resp->pp_error;
|
|
|
|
|
if ( defined $pp_error
|
|
|
|
|
and $pp_error == 0
|
|
|
|
|
and $self->{conf}->{ldapAllowResetExpiredPassword} )
|
|
|
|
|
{
|
2017-02-15 07:41:50 +01:00
|
|
|
|
$self->{portal}->logger->debug(
|
|
|
|
|
"Password is expired but user is allowed to change it"
|
2017-01-15 14:18:01 +01:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
if ( $mesg->code != 0 ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
|
$self->{portal}->logger->debug("Bad old password");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return PE_BADOLDPASSWORD;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Use SetPassword extended operation
|
|
|
|
|
# Warning: need a patch on Perl-LDAP
|
|
|
|
|
# See http://groups.google.com/group/perl.ldap/browse_thread/thread/5703a41ccb17b221/377a68f872cc2bb4?lnk=gst&q=setpassword#377a68f872cc2bb4
|
|
|
|
|
use Net::LDAP::Extension::SetPassword;
|
|
|
|
|
$mesg =
|
|
|
|
|
($oldpassword)
|
|
|
|
|
? $self->set_password(
|
|
|
|
|
user => $dn,
|
|
|
|
|
oldpasswd => $oldpassword,
|
|
|
|
|
newpasswd => $newpassword,
|
|
|
|
|
control => [$pp]
|
|
|
|
|
)
|
|
|
|
|
: $self->set_password(
|
|
|
|
|
user => $dn,
|
|
|
|
|
newpasswd => $newpassword,
|
|
|
|
|
control => [$pp]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
# Catch the "Unwilling to perform" error
|
|
|
|
|
if ( $mesg->code == 53 ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
|
$self->{portal}->logger->debug("Bad old password");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return PE_BADOLDPASSWORD;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
if ($oldpassword) {
|
|
|
|
|
|
|
|
|
|
# Check old password with a bind
|
|
|
|
|
$mesg = $self->bind(
|
|
|
|
|
$dn,
|
|
|
|
|
password => $oldpassword,
|
|
|
|
|
control => [$pp]
|
|
|
|
|
);
|
|
|
|
|
my ($bind_resp) = $mesg->control("1.3.6.1.4.1.42.2.27.8.5.1");
|
|
|
|
|
|
|
|
|
|
unless ( defined $bind_resp ) {
|
|
|
|
|
if ( $mesg->code != 0 ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
|
$self->{portal}->logger->debug("Bad old password");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return PE_BADOLDPASSWORD;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
|
|
|
|
|
# Check if password is expired
|
|
|
|
|
my $pp_error = $bind_resp->pp_error;
|
|
|
|
|
if ( defined $pp_error
|
|
|
|
|
and $pp_error == 0
|
|
|
|
|
and $self->{conf}->{ldapAllowResetExpiredPassword} )
|
|
|
|
|
{
|
2017-02-15 07:41:50 +01:00
|
|
|
|
$self->{portal}->logger->debug(
|
|
|
|
|
"Password is expired but user is allowed to change it"
|
2017-01-15 14:18:01 +01:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
if ( $mesg->code != 0 ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
|
$self->{portal}->logger->debug("Bad old password");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return PE_BADOLDPASSWORD;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Rebind as Manager only if user is not granted to change its password
|
|
|
|
|
$self->bind()
|
|
|
|
|
unless $asUser;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Use standard modification
|
|
|
|
|
$mesg = $self->modify(
|
|
|
|
|
$dn,
|
|
|
|
|
replace => { $passwordAttribute => $newpassword },
|
|
|
|
|
control => [$pp]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Get server control response
|
|
|
|
|
my ($resp) = $mesg->control("1.3.6.1.4.1.42.2.27.8.5.1");
|
|
|
|
|
|
|
|
|
|
$self->{portal}
|
2017-02-15 07:41:50 +01:00
|
|
|
|
->logger->debug( "Modification return code: " . $mesg->code );
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return PE_WRONGMANAGERACCOUNT
|
|
|
|
|
if ( $mesg->code == 50 || $mesg->code == 8 );
|
|
|
|
|
if ( $mesg->code == 0 ) {
|
2020-11-29 18:02:13 +01:00
|
|
|
|
$self->{portal}->logger->notice("Password changed for $dn");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
|
|
|
|
|
# Rebind as manager for next LDAP operations if we were bound as user
|
|
|
|
|
$self->bind() if $asUser;
|
|
|
|
|
|
|
|
|
|
return PE_PASSWORD_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( defined $resp ) {
|
|
|
|
|
my $pp_error = $resp->pp_error;
|
|
|
|
|
if ( defined $pp_error ) {
|
2017-02-19 12:51:58 +01:00
|
|
|
|
$self->{portal}
|
|
|
|
|
->logger->error("Password policy error $pp_error");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return [
|
|
|
|
|
PE_PP_PASSWORD_EXPIRED,
|
|
|
|
|
PE_PP_ACCOUNT_LOCKED,
|
|
|
|
|
PE_PP_CHANGE_AFTER_RESET,
|
|
|
|
|
PE_PP_PASSWORD_MOD_NOT_ALLOWED,
|
|
|
|
|
PE_PP_MUST_SUPPLY_OLD_PASSWORD,
|
|
|
|
|
PE_PP_INSUFFICIENT_PASSWORD_QUALITY,
|
|
|
|
|
PE_PP_PASSWORD_TOO_SHORT,
|
|
|
|
|
PE_PP_PASSWORD_TOO_YOUNG,
|
|
|
|
|
PE_PP_PASSWORD_IN_HISTORY,
|
|
|
|
|
]->[$pp_error];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
2019-09-30 17:19:57 +02:00
|
|
|
|
$self->{portal}->logger->error(
|
|
|
|
|
"Missing PPolicy control from server response. Code: "
|
|
|
|
|
. $mesg->code );
|
2017-01-15 14:18:01 +01:00
|
|
|
|
return PE_LDAPERROR;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-15 12:27:10 +01:00
|
|
|
|
## @method string searchGroups(string base, string key, string value, string attributes, hashref dupcheck)
|
2017-01-15 14:18:01 +01:00
|
|
|
|
# Get groups from LDAP directory
|
|
|
|
|
# @param base LDAP search base
|
|
|
|
|
# @param key Attribute name in group containing searched value
|
|
|
|
|
# @param value Searched value
|
|
|
|
|
# @param attributes to get from found groups (array ref)
|
2018-01-15 12:27:10 +01:00
|
|
|
|
# @param dupcheck to get from found groups (hash ref)
|
2017-01-15 14:18:01 +01:00
|
|
|
|
# @return hashRef groups
|
|
|
|
|
sub searchGroups {
|
2018-01-15 12:27:10 +01:00
|
|
|
|
my ( $self, $base, $key, $value, $attributes, $dupcheck ) = @_;
|
2017-01-15 14:18:01 +01:00
|
|
|
|
|
2018-01-15 12:27:10 +01:00
|
|
|
|
$dupcheck ||= {};
|
2017-01-15 14:18:01 +01:00
|
|
|
|
my $groups = {};
|
|
|
|
|
|
|
|
|
|
# Creating search filter
|
|
|
|
|
my $searchFilter =
|
|
|
|
|
"(&(objectClass=" . $self->{conf}->{ldapGroupObjectClass} . ")(|";
|
|
|
|
|
foreach ( split( $self->{conf}->{multiValuesSeparator}, $value ) ) {
|
|
|
|
|
$searchFilter .= "(" . $key . "=" . escape_filter_value($_) . ")";
|
|
|
|
|
}
|
|
|
|
|
$searchFilter .= "))";
|
|
|
|
|
|
2017-07-17 21:19:39 +02:00
|
|
|
|
$self->{portal}->logger->debug("Group search filter: $searchFilter");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
|
|
|
|
|
# Search
|
|
|
|
|
my $mesg = $self->search(
|
|
|
|
|
base => $base,
|
|
|
|
|
filter => $searchFilter,
|
|
|
|
|
attrs => $attributes,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
# Browse results
|
|
|
|
|
if ( $mesg->code() == 0 ) {
|
|
|
|
|
|
|
|
|
|
foreach my $entry ( $mesg->all_entries ) {
|
|
|
|
|
|
2017-07-17 21:19:39 +02:00
|
|
|
|
$self->{portal}
|
2017-02-15 07:41:50 +01:00
|
|
|
|
->logger->debug( "Matching group " . $entry->dn() . " found" );
|
2017-01-15 14:18:01 +01:00
|
|
|
|
|
|
|
|
|
# If recursive search is activated, do it here
|
|
|
|
|
if ( $self->{conf}->{ldapGroupRecursive} ) {
|
|
|
|
|
|
|
|
|
|
# Get searched value
|
|
|
|
|
my $group_value =
|
|
|
|
|
$self->getLdapValue( $entry,
|
|
|
|
|
$self->{conf}->{ldapGroupAttributeNameGroup} );
|
|
|
|
|
|
|
|
|
|
# Launch group search
|
|
|
|
|
if ($group_value) {
|
2020-09-03 15:59:18 +02:00
|
|
|
|
if ( $self->{conf}->{ldapGroupDecodeSearchedValue} ) {
|
|
|
|
|
utf8::decode($group_value);
|
|
|
|
|
}
|
2017-01-15 14:18:01 +01:00
|
|
|
|
|
2018-01-15 12:27:10 +01:00
|
|
|
|
if ( $dupcheck->{$group_value} ) {
|
|
|
|
|
$self->{portal}->logger->debug(
|
|
|
|
|
"Disable search for $group_value, as it was already searched"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$dupcheck->{$group_value} = 1;
|
|
|
|
|
$self->{portal}
|
|
|
|
|
->logger->debug("Recursive search for $group_value");
|
|
|
|
|
|
|
|
|
|
my $recursive_groups =
|
|
|
|
|
$self->searchGroups( $base, $key, $group_value,
|
|
|
|
|
$attributes, $dupcheck );
|
2017-01-15 14:18:01 +01:00
|
|
|
|
|
2018-01-15 12:27:10 +01:00
|
|
|
|
my %allGroups = ( %$groups, %$recursive_groups )
|
|
|
|
|
if ( ref $recursive_groups );
|
|
|
|
|
$groups = \%allGroups;
|
2017-01-15 14:18:01 +01:00
|
|
|
|
|
2018-01-15 12:27:10 +01:00
|
|
|
|
}
|
2017-01-15 14:18:01 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Use first attribute as group name
|
|
|
|
|
my $groupName = $entry->get_value( $attributes->[0] );
|
|
|
|
|
$groups->{$groupName}->{name} = $groupName;
|
|
|
|
|
|
|
|
|
|
# Now parse attributes
|
|
|
|
|
foreach (@$attributes) {
|
|
|
|
|
|
|
|
|
|
# Next if group attribute value
|
|
|
|
|
next
|
|
|
|
|
if ( $_ eq $self->{conf}->{ldapGroupAttributeValueGroup} );
|
|
|
|
|
|
|
|
|
|
my $data = $entry->get_value( $_, asref => 1 );
|
|
|
|
|
|
|
|
|
|
if ($data) {
|
2017-07-17 21:19:39 +02:00
|
|
|
|
$self->{portal}
|
2017-02-15 07:41:50 +01:00
|
|
|
|
->logger->debug("Store values of $_ in group $groupName");
|
2017-01-15 14:18:01 +01:00
|
|
|
|
$groups->{$groupName}->{$_} = $data;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $groups;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
## @method string getLdapValue(Net::LDAP::Entry entry, string attribute)
|
|
|
|
|
# Get the dn, or the attribute value with a separator for multi-valuated attributes
|
|
|
|
|
# @param entry LDAP entry
|
|
|
|
|
# @param attribute Attribute name
|
|
|
|
|
# @return string value
|
|
|
|
|
sub getLdapValue {
|
|
|
|
|
my ( $self, $entry, $attribute ) = @_;
|
|
|
|
|
|
|
|
|
|
return $entry->dn() if ( $attribute eq "dn" );
|
|
|
|
|
|
|
|
|
|
return join(
|
|
|
|
|
$self->{conf}->{multiValuesSeparator},
|
|
|
|
|
$entry->get_value($attribute)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-25 07:05:12 +01:00
|
|
|
|
# Convert seconds to hours, minutes, seconds
|
|
|
|
|
sub convertSec {
|
|
|
|
|
my ( $self, $sec ) = @_;
|
|
|
|
|
my ( $day, $hrs, $min ) = ( 0, 0, 0 );
|
|
|
|
|
|
|
|
|
|
# Calculate the minutes
|
|
|
|
|
if ( $sec > 60 ) {
|
|
|
|
|
$min = $sec / 60, $sec %= 60;
|
|
|
|
|
$min = int($min);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Calculate the hours
|
|
|
|
|
if ( $min > 60 ) {
|
|
|
|
|
$hrs = $min / 60, $min %= 60;
|
|
|
|
|
$hrs = int($hrs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Calculate the days
|
|
|
|
|
if ( $hrs > 24 ) {
|
|
|
|
|
$day = $hrs / 24, $hrs %= 24;
|
|
|
|
|
$day = int($day);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Return the date
|
|
|
|
|
return ( $day, $hrs, $min, $sec );
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 17:14:44 +02:00
|
|
|
|
## @method int getITDSError(Net::LDAP::Message mesg)
|
|
|
|
|
# Check error message to return according error code
|
|
|
|
|
# @param mesg Modification return message
|
|
|
|
|
# @return portal error code
|
|
|
|
|
sub getITDSError {
|
|
|
|
|
my ( $self, $mesg ) = @_;
|
|
|
|
|
|
|
|
|
|
return PE_PP_MUST_SUPPLY_OLD_PASSWORD
|
|
|
|
|
if ( $mesg->code == 53 && $mesg->error =~ /Must supply old password/i );
|
|
|
|
|
return PE_PP_CHANGE_AFTER_RESET
|
|
|
|
|
if ( $mesg->code == 53
|
|
|
|
|
&& $mesg->error =~ /Password must be changed after reset/i );
|
|
|
|
|
return PE_PP_PASSWORD_MOD_NOT_ALLOWED
|
|
|
|
|
if ( $mesg->code == 53
|
|
|
|
|
&& $mesg->error =~ /Password may not be modified/i );
|
|
|
|
|
return PE_PP_PASSWORD_TOO_YOUNG
|
|
|
|
|
if ( $mesg->code == 19 && $mesg->error =~ /Password too young/i );
|
|
|
|
|
return PE_PP_PASSWORD_TOO_SHORT
|
|
|
|
|
if ( $mesg->code == 19 && $mesg->error =~ /Password too short/i );
|
|
|
|
|
return PE_PP_PASSWORD_IN_HISTORY
|
|
|
|
|
if ( $mesg->code == 19 && $mesg->error =~ /Password in History/i );
|
|
|
|
|
return PE_PP_INSUFFICIENT_PASSWORD_QUALITY if ( $mesg->code == 19 );
|
|
|
|
|
|
|
|
|
|
return PE_PASSWORD_OK;
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-15 14:18:01 +01:00
|
|
|
|
1;
|