From 351617fcdcb21e4424fa7384f4c3175a33dc6057 Mon Sep 17 00:00:00 2001 From: Oliver Gorwits Date: Fri, 23 Aug 2013 10:55:18 +0100 Subject: [PATCH] add LDAP authentication support --- Netdisco/Changes | 6 +- .../lib/App/Netdisco/Manual/Configuration.pod | 66 ++++++++++ .../App/Netdisco/Web/Auth/Provider/DBIC.pm | 123 ++++++++++++++++-- .../Netdisco/Web/Plugin/AdminTask/Users.pm | 2 + Netdisco/share/views/ajax/admintask/users.tt | 6 +- 5 files changed, 188 insertions(+), 15 deletions(-) diff --git a/Netdisco/Changes b/Netdisco/Changes index f4b67b3f..247f6e26 100644 --- a/Netdisco/Changes +++ b/Netdisco/Changes @@ -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] diff --git a/Netdisco/lib/App/Netdisco/Manual/Configuration.pod b/Netdisco/lib/App/Netdisco/Manual/Configuration.pod index a5061551..88c5c1f7 100644 --- a/Netdisco/lib/App/Netdisco/Manual/Configuration.pod +++ b/Netdisco/lib/App/Netdisco/Manual/Configuration.pod @@ -117,6 +117,72 @@ to Netdisco in the C HTTP Header. For example with Apache: RequestHeader unset X-REMOTE_USER RequestHeader set X-REMOTE_USER "%{REMOTE_USER}e" env=REMOTE_USER +=head3 C + +Value: Settings Tree. Default: None. + +If set, and a user has the C flag also set on their account, then LDAP +authentication will be used for their login. You I install the +L 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 + +This must be a list of one or more LDAP servers. If using Active Directory +these would be your Domain Controllers. + +=head4 C + +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 and skip all other +options except C, as this notation eliminates the need to construct +the full distinguished name. + +Examples: C or C. + +=head4 C + +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 to construct the distinguished name for authentication. + +=head4 C + +User to bind with to perform searches. If defined as C, then +anonymous binds will be performed and C 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 user password. Ignored if proxy user defined as anonymous. + +=head4 C + +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 + +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 documentation under +start_tls for options, but the defaults should work in most cases. + =head3 C Value: String. Default: None. diff --git a/Netdisco/lib/App/Netdisco/Web/Auth/Provider/DBIC.pm b/Netdisco/lib/App/Netdisco/Web/Auth/Provider/DBIC.pm index 375ef58f..08d42d3a 100644 --- a/Netdisco/lib/App/Netdisco/Web/Auth/Provider/DBIC.pm +++ b/Netdisco/lib/App/Netdisco/Web/Auth/Provider/DBIC.pm @@ -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; diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/Users.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/Users.pm index af2c591b..2a34c579 100644 --- a/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/Users.pm +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/Users.pm @@ -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'), }); diff --git a/Netdisco/share/views/ajax/admintask/users.tt b/Netdisco/share/views/ajax/admintask/users.tt index faff2000..5c6c649d 100644 --- a/Netdisco/share/views/ajax/admintask/users.tt +++ b/Netdisco/share/views/ajax/admintask/users.tt @@ -4,7 +4,7 @@ Full Name Username Password - + LDAP Auth Port Control Administrator Action @@ -15,6 +15,7 @@ + @@ -33,6 +34,9 @@ + + +