[svn:qpsmtpd] rev 404 - in trunk: . lib/Qpsmtpd plugins/auth

[email protected] 12 Apr 2005 19:59:52 -0000
Newsgroups perl.cvs.qpsmtpd
Message-ID <[email protected]>
Author: jpeacock
Date: Tue Apr 12 12:59:52 2005
New Revision: 404

Added:
   trunk/plugins/auth/auth_ldap_bind
Modified:
   trunk/   (props changed)
   trunk/lib/Qpsmtpd/Auth.pm
Log:
 *  lib/Qpsmtpd/Auth.pm
    Fix some totally egregious spelling errors
 
 *  plugins/auth/auth_ldap_bind
    New plugin to authenticate against an LDAP database
    Thanks to Elliot Foster <[email protected]>


Modified: trunk/lib/Qpsmtpd/Auth.pm
==============================================================================
--- trunk/lib/Qpsmtpd/Auth.pm	(original)
+++ trunk/lib/Qpsmtpd/Auth.pm	Tue Apr 12 12:59:52 2005
@@ -8,8 +8,8 @@
 
 Provides support for SMTP AUTH within qpsmtpd transactions, see 
 
-  L<http://www.faqs.org/rfcs/rfc2222.html>
-  L<http://www.faqs.org/rfcs/rfc2554.html>
+L<http://www.faqs.org/rfcs/rfc2222.html>
+L<http://www.faqs.org/rfcs/rfc2554.html>
 
 for more details.
 
@@ -106,23 +106,25 @@
 If the authentication has succeeded, the plugin can return this value and
 all subsequently registered hooks will be skipped.
 
-=item DECLINE
+=item DECLINED
 
 If the authentication has failed, but any additional plugins should be run, 
 this value will be returned.  If none of the registered plugins succeed, the
-overall authentication will fail.
+overall authentication will fail.  Normally an auth plugin should return
+this value for all cases which do not succeed (so that another auth plugin
+can have a chance to authenticate the user).
 
 =item DENY
 
 If the authentication has failed, and the plugin wishes this to short circuit
 any further testing, it should return this value.  For example, a plugin could
 register the L<auth-plain> hook and immediately fail any connection which is
-not trusted (i.e. not in the same network).
+not trusted (e.g. not in the same network).
 
-Another reason to return DENY over DECLINE would be if the user name matched
+Another reason to return DENY over DECLINED would be if the user name matched
 an existing account but the password failed to match.  This would make a
-dictionary-based attack much harder to accomplish.  See the example authsql
-plugin for how this might be accomplished
+dictionary-based attack much harder to accomplish.  See the included
+auth_vpopmail_sql plugin for how this might be accomplished.
 
 By returning DENY, no further authentication attempts will be made using the
 current method and data.  A remote SMTP client is free to attempt a second
@@ -138,9 +140,7 @@
 client.  There is no guarantee that the end user will see this information,
 though, since some prominent MTA's (produced by M$oft) I<helpfully>
 hide this information under the default configuration.  This message will
-be logged locally, if appropriate based on the configured log level.  If
-you are running multiple auth plugins, it is helpful to include at least
-the plugin name in the returned message (for debugging purposes).
+be logged locally, if appropriate, based on the configured log level.
 
 =head1 Auth Hooks
 
@@ -153,7 +153,7 @@
 Any plugin which registers an auth-plain hook will engage in a plaintext
 prompted negotiation.  This is the least secure authentication method since
 both the user name and password are visible in plaintext.  Most SMTP clients
-will preferentially chose a more secure method if it is advertised by the
+will preferentially choose a more secure method if it is advertised by the
 server.
 
 =item * auth-login

Added: trunk/plugins/auth/auth_ldap_bind
==============================================================================
--- (empty file)
+++ trunk/plugins/auth/auth_ldap_bind	Tue Apr 12 12:59:52 2005
@@ -0,0 +1,192 @@
+#!/usr/bin/perl -Tw
+
+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 {
+  use Net::LDAP qw(:all);
+  use Qpsmtpd::Constants;
+
+  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, "authldap/$method - please configure ldap_base" ) &&
+    return ( DECLINED, "authldap/$method - temporary auth error" );
+  }
+  $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
+    $self->log(LOGALERT, "authldap/$method - error in initial conn" ) &&
+    return ( DECLINE, "authldap/$method - temporary auth error" );
+
+  # find the user's DN
+  $mesg = $ldh->search(
+    base=>$ldbase,
+    scope=>'sub',
+    filter=>"$ldmattr=$pw_name",
+    attrs=>['uid'],
+    timeout=>$ldwait,
+    sizelimit=>'1') or 
+      $self->log(LOGALERT, "authldap/$method - err in search for user" ) &&
+      return ( DECLINE, "authldap/$method - temporary auth error" );
+
+  # deal with errors if they exist
+  if ( $mesg->code ) {
+    $self->log(LOGALERT, "authldap/$method - err " . $mesg->code . " in search for user" );
+    return ( DECLINE, "authldap/$method - temporary auth error" );
+  }
+
+  # unbind, so as to allow a rebind below
+  $ldh->unbind if ($ldh);
+
+  # bind against directory as user with password supplied
+  if (($mesg->count) && ($lduserdn = $mesg->entry->dn)) {
+    $ldh = Net::LDAP->new($ldhost, port=>$ldport, timeout=>$ldwait ) or
+      $self->log(LOGALERT, "authldap/$method - err in user conn" ) &&
+        return ( DECLINE, "authldap/$method - 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, "authldap/$method - error in user bind" );
+      return ( DENY, "authldap/$method - wrong username or password" );
+    } else {
+      $self->log( LOGINFO, "authldap/$method - $user auth success" );
+      $self->log( LOGDEBUG, "authldap/$method - user: $user, pass: $passClear" );
+      return ( OK, "authldap/$method" );
+    }
+
+  # if the plugin couldn't find user's entry
+  } else {
+    $self->log(LOGALERT, "authldap/$method - user not found" ) &&
+      return ( DECLINE, "authldap/$method - wrong username or password" );
+  }
+
+  $ldh->disconnect;
+}
+
+=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 <[email protected]>
+
+=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
+