add LDAP authentication support

This commit is contained in:
Oliver Gorwits
2013-08-23 10:55:18 +01:00
parent 1c5de08f41
commit 351617fcdc
5 changed files with 188 additions and 15 deletions

View File

@@ -1,4 +1,8 @@
2.012007 - 2013-08-23
2.013000 - 2013-08-23
[NEW FEATURES]
* LDAP authentication support - see Configuration POD for details
[ENHANCEMENTS]

View File

@@ -117,6 +117,72 @@ to Netdisco in the C<X-REMOTE_USER> HTTP Header. For example with Apache:
RequestHeader unset X-REMOTE_USER
RequestHeader set X-REMOTE_USER "%{REMOTE_USER}e" env=REMOTE_USER
=head3 C<ldap>
Value: Settings Tree. Default: None.
If set, and a user has the C<ldap> flag also set on their account, then LDAP
authentication will be used for their login. You I<must> install the
L<Net::LDAP> Perl module in order to use this feature. For example:
ldap:
servers:
- 'ad.example.com'
user_string: 'MYDOMAIN\%USER%'
opts:
debug: 3
There are several options within this setting:
=head4 C<servers>
This must be a list of one or more LDAP servers. If using Active Directory
these would be your Domain Controllers.
=head4 C<user_string>
String to construct the user portion of the DN. C<%USER%> is a variable which
will be replaced at runtime with the logon name entered on the logon page of
the application.
Active Directory users may simply use C<MYDOMAIN\%USER%> and skip all other
options except C<servers>, as this notation eliminates the need to construct
the full distinguished name.
Examples: C<cn=%USER%> or C<uid=%USER%>.
=head4 C<base>
Indicates where in the hierarchy to begin searches. If a proxy user is not
defined and anonymous binds are not enabled this value will be appended to the
C<user_string> to construct the distinguished name for authentication.
=head4 C<proxy_user>
User to bind with to perform searches. If defined as C<anonymous>, then
anonymous binds will be performed and C<proxy_pass> will be ignored. For
organizations with users in multiple OUs this option can be used to search for
the user and construct the DN based upon the result.
=head4 C<proxy_pass>
Proxy user password. Ignored if proxy user defined as anonymous.
=head4 C<opts>
Hash of options to add to the connect string. Normally only needed if server
does not support LDAPv3, or to enable debugging as in the example above.
=head4 C<tls_opts>
A hash which, when defined, causes the connection tol use Transport Layer
Security (TLS) which provides an encrypted connection. TLS is the preferred
method of encryption, ldaps (port 636) is not supported.
This is only possible if using LDAPv3 and the server supports it. These are
the options for the TLS connection. See the L<Net::LDAP> documentation under
start_tls for options, but the defaults should work in most cases.
=head3 C<path>
Value: String. Default: None.

View File

@@ -8,7 +8,7 @@ use base 'Dancer::Plugin::Auth::Extensible::Provider::Base';
# with thanks to yanick's patch at
# https://github.com/bigpresh/Dancer-Plugin-Auth-Extensible/pull/24
use Dancer qw(:syntax);
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Digest::MD5;
@@ -21,18 +21,6 @@ sub authenticate_user {
return $self->match_password($password, $user);
}
sub match_password {
my( $self, $password, $user ) = @_;
return unless $user and $password and $user->password;
my $settings = $self->realm_settings;
my $password_column = $settings->{users_password_column} || 'password';
my $sum = Digest::MD5::md5_hex($password);
return ($sum eq $user->$password_column ? 1 : 0);
}
sub get_user_details {
my ($self, $username) = @_;
@@ -69,4 +57,113 @@ sub get_user_roles {
return [ $user->$roles->get_column( $role_column )->all ];
}
sub match_password {
my($self, $password, $user) = @_;
return unless $user;
my $settings = $self->realm_settings;
my $username_column = $settings->{users_username_column} || 'username';
return $user->ldap
? $self->match_with_ldap($password, $user->$username_column)
: $self->match_with_local_pass($password, $user);
}
sub match_with_local_pass {
my($self, $password, $user) = @_;
my $settings = $self->realm_settings;
my $password_column = $settings->{users_password_column} || 'password';
return unless $password and $user->password_column;
my $sum = Digest::MD5::md5_hex($password);
return ($sum eq $user->$password_column ? 1 : 0);
}
sub match_with_ldap {
my($self, $pass, $user) = @_;
eval 'require Net::LDAP';
if ($@) {error $@; return}
return unless setting('ldap') and ref {} eq ref setting('ldap');
my $conf = setting('ldap');
my $ldapuser = $conf->{user_string};
$ldapuser =~ s/\%USER\%?/$user/egi;
# If we can bind as anonymous or proxy user,
# search for user's distinguished name
if ($conf->{proxy_user}) {
my $user = $conf->{proxy_user};
my $pass = $conf->{proxy_pass};
my $attrs = ['distinguishedName'];
my $result = _ldap_search($ldapuser, $attrs, $user, $pass);
$ldapuser = $result->[0] if ($result->[0]);
}
# otherwise, if we can't search and aren't using AD and then construct DN by
# appending base
elsif ($ldapuser =~ m/=/) {
$ldapuser = "$ldapuser,$conf->{base}";
}
foreach my $server (@{$conf->{servers}}) {
my $opts = $conf->{opts} || {};
my $ldap = Net::LDAP->new($server, %$opts) or next;
my $msg = undef;
if ($conf->{tls_opts} ) {
$msg = $ldap->start_tls(%{$conf->{tls_opts}});
}
$msg = $ldap->bind($ldapuser, password => $pass);
$ldap->unbind(); # take down session
return 1 unless $msg->code();
}
return undef;
}
sub _ldap_search {
my ($filter, $attrs, $user, $pass) = @_;
return undef unless defined($filter);
return undef if (defined $attrs and ref [] ne ref $attrs);
return unless setting('ldap') and ref {} eq ref setting('ldap');
my $conf = setting('ldap');
foreach my $server (@{$conf->{server}}) {
my $opts = $conf->{opts} || {};
my $ldap = Net::LDAP->new($server, %$opts) or next;
my $msg = undef;
if ($conf->{tls_opts}) {
$msg = $ldap->start_tls(%{$conf->{tls_opts}});
}
if ( $user and $user ne 'anonymous' ) {
$msg = $ldap->bind($user, password => $pass);
}
else {
$msg = $ldap->bind();
}
$msg = $ldap->search(
base => $conf->{base},
filter => "($filter)",
attrs => $attrs,
);
$ldap->unbind(); # take down session
my $entries = [$msg->entries];
return $entries unless $msg->code();
}
return undef;
}
1;

View File

@@ -30,6 +30,7 @@ ajax '/ajax/control/admin/users/add' => require_role admin => sub {
username => param('username'),
password => Digest::MD5::md5_hex(param('password')),
fullname => param('fullname'),
ldap => (param('ldap') ? \'true' : \'false'),
port_control => (param('port_control') ? \'true' : \'false'),
admin => (param('admin') ? \'true' : \'false'),
});
@@ -58,6 +59,7 @@ ajax '/ajax/control/admin/users/update' => require_role admin => sub {
? (password => Digest::MD5::md5_hex(param('password')))
: ()),
fullname => param('fullname'),
ldap => (param('ldap') ? \'true' : \'false'),
port_control => (param('port_control') ? \'true' : \'false'),
admin => (param('admin') ? \'true' : \'false'),
});

View File

@@ -4,7 +4,7 @@
<th class="nd_center-cell">Full Name</th>
<th class="nd_center-cell">Username</th>
<th class="nd_center-cell">Password</th>
<!-- <th class="nd_center-cell">External Auth</th> -->
<th class="nd_center-cell">LDAP Auth</th>
<th class="nd_center-cell">Port Control</th>
<th class="nd_center-cell">Administrator</th>
<th class="nd_center-cell">Action</th>
@@ -15,6 +15,7 @@
<td class="nd_center-cell"><input data-form="add" name="fullname" type="text"></td>
<td class="nd_center-cell"><input data-form="add" name="username" type="text"></td>
<td class="nd_center-cell"><input data-form="add" name="password" type="text"></td>
<td class="nd_center-cell"><input data-form="add" type="checkbox" name="ldap"></td>
<td class="nd_center-cell"><input data-form="add" type="checkbox" name="port_control"></td>
<td class="nd_center-cell"><input data-form="add" type="checkbox" name="admin"></td>
<td class="nd_center-cell">
@@ -33,6 +34,9 @@
<td class="nd_center-cell">
<input data-form="update" name="password" type="text" value="********">
</td>
<td class="nd_center-cell">
<input data-form="update" name="ldap" type="checkbox" [% 'checked="checked"' IF row.ldap %]>
</td>
<td class="nd_center-cell">
<input data-form="update" name="port_control" type="checkbox" [% 'checked="checked"' IF row.port_control %]>
</td>