add LDAP authentication support
This commit is contained in:
		| @@ -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] | ||||
|  | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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'), | ||||
|       }); | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user