2012-06-22 11:38:01 +02:00
|
|
|
#!perl -w
|
|
|
|
|
|
|
|
=head1 NAME
|
|
|
|
|
|
|
|
auth_ldap_bind - Authenticate user via an LDAP bind
|
|
|
|
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
|
|
|
|
This plugin authenticates users against an LDAP Directory. The plugin
|
|
|
|
first performs a lookup for an entry matching the connecting user. This
|
|
|
|
lookup uses the 'ldap_auth_filter_attr' attribute to match the connecting
|
|
|
|
user to their LDAP DN. Once the plugin has found the user's DN, the plugin
|
|
|
|
will attempt to bind to the Directory as that DN with the password that has
|
|
|
|
been supplied.
|
|
|
|
|
|
|
|
=head1 CONFIGURATION
|
|
|
|
|
|
|
|
Configuration items can be held in either the 'ldap' configuration file, or as
|
|
|
|
arguments to the plugin.
|
|
|
|
|
|
|
|
Configuration items in the 'ldap' configuration file
|
|
|
|
are set one per line, starting the line with the configuration item key,
|
|
|
|
followed by a space, then the values associated with the configuration item.
|
|
|
|
|
|
|
|
Configuration items given as arguments to the plugin are keys and values
|
|
|
|
separated by spaces. Be sure to quote any values that have spaces in them.
|
|
|
|
|
|
|
|
The only configuration item which is required is 'ldap_base'. This tells the
|
|
|
|
plugin what your base DN is. The plugin will not work until it has been
|
|
|
|
configured.
|
|
|
|
|
|
|
|
The configuration items 'ldap_host' and 'ldap_port' specify the host and port
|
|
|
|
at which your Directory server may be contacted. If these are not specified,
|
|
|
|
the plugin will use port '389' on 'localhost'.
|
|
|
|
|
|
|
|
The configuration item 'ldap_timeout' specifies how long the plugin should
|
|
|
|
wait for a response from your Directory server. By default, the value is 5
|
|
|
|
seconds.
|
|
|
|
|
|
|
|
The configuration item 'ldap_auth_filter_attr' specifies how the plugin should
|
|
|
|
find the user in your Directory. By default, the plugin will look up the user
|
|
|
|
based on the 'uid' attribute.
|
|
|
|
|
|
|
|
=head1 NOTES
|
|
|
|
|
|
|
|
Each auth requires an initial lookup to find the user's DN. Ideally, the
|
|
|
|
plugin would simply bind as the user without the need for this lookup (see
|
|
|
|
FUTURE DIRECTION below).
|
|
|
|
|
|
|
|
This plugin requires that the Directory allow anonymous bind (see FUTURE
|
|
|
|
DIRECTION below).
|
|
|
|
|
|
|
|
=head1 FUTURE DIRECTION
|
|
|
|
|
|
|
|
A configurable LDAP filter should be made available, to account for users
|
|
|
|
who are over quota, have had their accounts disabled, or whatever other
|
|
|
|
arbitrary requirements.
|
|
|
|
|
|
|
|
A configurable DN template (uid=$USER,ou=$DOMAIN,$BASE). This would prevent
|
|
|
|
the need of the initial user lookup, as the DN is created from the template.
|
|
|
|
|
|
|
|
A configurable bind DN, for Directories that do not allow anonymous bind.
|
|
|
|
|
|
|
|
Another plugin ('ldap_auth_cleartext'?), to allow retrieval of plain-text
|
|
|
|
passwords from the Directory, permitting CRAM-MD5 or other hash algorithm
|
|
|
|
authentication.
|
|
|
|
|
|
|
|
=head1 AUTHOR
|
|
|
|
|
|
|
|
Elliot Foster <elliotf@gratuitous.net>
|
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
|
|
|
|
Copyright (c) 2005 Elliot Foster
|
|
|
|
|
|
|
|
This plugin is licensed under the same terms as the qpsmtpd package itself.
|
|
|
|
Please see the LICENSE file included with qpsmtpd for details.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
use warnings;
|
|
|
|
|
|
|
|
use Net::LDAP qw(:all);
|
|
|
|
use Qpsmtpd::Constants;
|
|
|
|
|
|
|
|
sub register {
|
|
|
|
my ($self, $qp, @args) = @_;
|
|
|
|
|
|
|
|
$self->register_hook("auth-plain", "authldap");
|
|
|
|
$self->register_hook("auth-login", "authldap");
|
|
|
|
|
|
|
|
# pull config defaults in from file
|
|
|
|
%{$self->{"ldconf"}} =
|
|
|
|
map { (split /\s+/, $_, 2)[0, 1] } $self->qp->config('ldap');
|
|
|
|
|
|
|
|
# override ldap config defaults with plugin args
|
|
|
|
for my $ldap_arg (@args) {
|
|
|
|
%{$self->{"ldconf"}} = map { (split /\s+/, $_, 2)[0, 1] } $ldap_arg;
|
|
|
|
}
|
|
|
|
|
|
|
|
# do light validation of ldap_host and ldap_port to satisfy -T
|
|
|
|
my $ldhost = $self->{"ldconf"}->{'ldap_host'};
|
|
|
|
my $ldport = $self->{"ldconf"}->{'ldap_port'};
|
|
|
|
if (($ldhost) && ($ldhost =~ m/^(([a-z0-9]+\.?)+)$/)) {
|
|
|
|
$self->{"ldconf"}->{'ldap_host'} = $1;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
undef $self->{"ldconf"}->{'ldap_host'};
|
|
|
|
}
|
|
|
|
if (($ldport) && ($ldport =~ m/^(\d+)$/)) {
|
|
|
|
$self->{"ldconf"}->{'ldap_port'} = $1;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
undef $self->{"ldconf"}->{'ldap_port'};
|
|
|
|
}
|
|
|
|
|
|
|
|
# set any values that are not already
|
|
|
|
$self->{"ldconf"}->{"ldap_host"} ||= "127.0.0.1";
|
|
|
|
$self->{"ldconf"}->{"ldap_port"} ||= 389;
|
|
|
|
$self->{"ldconf"}->{"ldap_timeout"} ||= 5;
|
|
|
|
$self->{"ldconf"}->{"ldap_auth_filter_attr"} ||= "uid";
|
|
|
|
}
|
|
|
|
|
|
|
|
sub authldap {
|
|
|
|
my ($self, $transaction, $method, $user, $passClear, $passHash, $ticket) =
|
|
|
|
@_;
|
|
|
|
my ($ldhost, $ldport, $ldwait, $ldbase, $ldmattr, $lduserdn, $ldh, $mesg);
|
|
|
|
|
|
|
|
# pull values in from config
|
|
|
|
$ldhost = $self->{"ldconf"}->{"ldap_host"};
|
|
|
|
$ldport = $self->{"ldconf"}->{"ldap_port"};
|
|
|
|
$ldbase = $self->{"ldconf"}->{"ldap_base"};
|
|
|
|
|
|
|
|
# log error here and DECLINE if no baseDN, because a custom baseDN is required:
|
|
|
|
unless ($ldbase) {
|
|
|
|
$self->log(LOGERROR, "skip: please configure ldap_base");
|
|
|
|
return (DECLINED, "authldap - temporary auth error");
|
2013-04-21 06:50:39 +02:00
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
$ldwait = $self->{"ldconf"}->{'ldap_timeout'};
|
|
|
|
$ldmattr = $self->{"ldconf"}->{'ldap_auth_filter_attr'};
|
|
|
|
|
|
|
|
my ($pw_name, $pw_domain) = split "@", lc($user);
|
|
|
|
|
|
|
|
# find dn of user matching supplied username
|
|
|
|
$ldh = Net::LDAP->new($ldhost, port => $ldport, timeout => $ldwait) or do {
|
|
|
|
$self->log(LOGALERT, "skip: error in initial conn");
|
|
|
|
return (DECLINED, "authldap - temporary auth error");
|
|
|
|
};
|
|
|
|
|
|
|
|
# find the user's DN
|
2013-04-21 06:50:39 +02:00
|
|
|
$mesg = $ldh->search(
|
|
|
|
base => $ldbase,
|
|
|
|
scope => 'sub',
|
|
|
|
filter => "$ldmattr=$pw_name",
|
|
|
|
attrs => ['uid'],
|
|
|
|
timeout => $ldwait,
|
|
|
|
sizelimit => '1'
|
|
|
|
)
|
|
|
|
or do {
|
2012-06-22 11:38:01 +02:00
|
|
|
$self->log(LOGALERT, "skip: err in search for user");
|
|
|
|
return (DECLINED, "authldap - temporary auth error");
|
2013-04-21 06:50:39 +02:00
|
|
|
};
|
2012-06-22 11:38:01 +02:00
|
|
|
|
|
|
|
# deal with errors if they exist
|
|
|
|
if ($mesg->code) {
|
2013-04-21 06:50:39 +02:00
|
|
|
$self->log(LOGALERT,
|
|
|
|
"skip: err " . $mesg->code . " in search for user");
|
2012-06-22 11:38:01 +02:00
|
|
|
return (DECLINED, "authldap - temporary auth error");
|
|
|
|
}
|
|
|
|
|
|
|
|
# unbind, so as to allow a rebind below
|
|
|
|
$ldh->unbind if $ldh;
|
|
|
|
|
|
|
|
# bind against directory as user with password supplied
|
2013-04-21 06:50:39 +02:00
|
|
|
if (!$mesg->count || $lduserdn = $mesg->entry->dn) {
|
2012-06-22 11:38:01 +02:00
|
|
|
$self->log(LOGALERT, "fail: user not found");
|
|
|
|
return (DECLINED, "authldap - wrong username or password");
|
2013-04-21 06:50:39 +02:00
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
|
|
|
|
$ldh = Net::LDAP->new($ldhost, port => $ldport, timeout => $ldwait) or do {
|
|
|
|
$self->log(LOGALERT, "skip: err in user conn");
|
|
|
|
return (DECLINED, "authldap - temporary auth error");
|
|
|
|
};
|
|
|
|
|
|
|
|
# here's the whole reason for the script
|
|
|
|
$mesg = $ldh->bind($lduserdn, password => $passClear, timeout => $ldwait);
|
|
|
|
$ldh->unbind if $ldh;
|
|
|
|
|
|
|
|
# deal with errors if they exist, or allow success
|
|
|
|
if ($mesg->code) {
|
|
|
|
$self->log(LOGALERT, "fail: error in user bind");
|
|
|
|
return (DECLINED, "authldap - wrong username or password");
|
|
|
|
}
|
|
|
|
|
|
|
|
$self->log(LOGINFO, "pass: $user auth success");
|
|
|
|
$self->log(LOGDEBUG, "user: $user, pass: $passClear");
|
|
|
|
return (OK, "authldap");
|
|
|
|
}
|
|
|
|
|