Changes applied.  Let me know if you need anything else.

John Peacock wrote:
> Elliot F wrote:
> 
>> Done.  Configuration can be given via the 'ldap' config file, or given as
>> arguments to the plugin.  If both are set, the plugin arguments
>> override the
>> configuration file settings.
>>
>> One problem is that if a configuration value has a space (or spaces)
>> in it, it
>> breaks.  This could be a problem for many people's LDAP configs, as
>> many DNs
>> have spaces, and if/when I add the configurable filter (which could
>> very likely
>> have spaces,) that would be a problem as well.
> 
> 
> That's what quotes are for.  The parameters are not strictly whitespace
> delimited, but will respect quotes when splitting into the array.

Fair enough, good to know.

>> So then if a user can't be found (either in mysql or LDAP or whatever)
>> should
>> the plugin returned DENY (under the assumption it's the only auth
>> plugin) or
>> DECLINED (if there is more than one way for the user to AUTH)?
> 
> 
> All auth plugins should normally return DECLINED unless it is intended
> to be the only possible auth plugin (and even then, you don't need to
> return DENY, see below).
> 
>>
>> For that matter, should the auth plugin ever return DENY?  If there is
>> more than
>> one auth plugin (for whatever reason), then if the first fails, none
>> of the
>> others will run.  A corner case, yes, but interesting anyway.
> 
> 
> An auth plugin should only return DENY if you choose to immediately
> decide this attempt is invalid, for example the username matches but the
> password doesn't (say you know that users will only ever be located in a
> single database).  Even so, see below.
> 
>> If a DECLINED is returned, it simply means the relayclient variable
>> won't be set
>> for the connection, and the user won't be able to relay.  I wonder how
>> that
>> would affect the auth portion, as the client should get a success/fail
>> after
>> trying to auth against the SMTP server.
> 
> 
> For each of the phases of a SMTP transaction, there is a master process
> which actually dispatches the plugins for that phase.  In the auth case,
> it is the Qpsmtpd::Auth::SASL method.  This actually performs the
> negotiation with the client based on the possible (registered) AUTH
> methods and then passes the client responses to each of the auth plugins
> in turn.  If any plugin in the chain returns OK, the SASL() method
> responds 235 (success) and sets the relay_client flag.  If none of the
> plugins respond OK (i.e. all responded DECLINED), then the SASL() method
> responds 535 (failure) and the remote client gets to try again.

I figured as much and was trying to say so above.  I should have just looked
through the code.  Next time just let me know it's documented, and save your
hands some typing.  :)

> You can see some documentation for auth plugins here:
> 
>     $ perldoc lib/Qpsmtpd/Auth.pm
> 
> though I notice a few typos I need to fix (like DECLINED is missing the
> terminal "D")...
> 
> HTH

It did!

> John
#!/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

Reply via email to