Difference between revisions of "LdapUserLocalOverlay"

From Request Tracker Wiki
Jump to navigation Jump to search
(Adding categories)
 
(Undo revision 5652 by 195.16.40.50 (talk))
Line 6: Line 6:


  <nowiki>### User_Local.pm overlay for LDAP authentication and information
  <nowiki>### User_Local.pm overlay for LDAP authentication and information
### v1.1b2  2005.08.03  purp@acm.org
  ### v1.1b2  2005.08.03  purp@acm.org
#
  #
# The latest version of this module may be found at:
  # The latest version of this module may be found at:
#  http://wiki.bestpractical.com/view/LdapUserLocalOverlay
  #  http://wiki.bestpractical.com/view/LdapUserLocalOverlay
#
  #
# THIS MODULE REQUIRES SETTINGS IN YOUR RT_SiteConfig.pm;
  # THIS MODULE REQUIRES SETTINGS IN YOUR RT_SiteConfig.pm;
# you can find these at:
  # you can find these at:
#  http://wiki.bestpractical.com/view/LdapSiteConfigSettings
  #  http://wiki.bestpractical.com/view/LdapSiteConfigSettings
#
  #
 
### CREDITS
  ### CREDITS
# IsLDAPPassword() based on implementation of IsPassword() found at:
  # IsLDAPPassword() based on implementation of IsPassword() found at:
#
  #
# http://www.justatheory.com/computers/programming/perl/rt/User_Local.pm.ldap
  # http://www.justatheory.com/computers/programming/perl/rt/User_Local.pm.ldap
#
  #
# Author's credits:
  # Author's credits:
# Modification Originally by Marcelo Bartsch &lt;bartschm_cl@hotmail.com&gt;
  # Modification Originally by Marcelo Bartsch <bartschm_cl@hotmail.com>
# Update by Stewart James &lt;stewart.james@vu.edu.au for rt3.
  # Update by Stewart James <stewart.james@vu.edu.au for rt3.
# Update by David Wheeler &lt;david@kineticode.com&gt; for TLS and
  # Update by David Wheeler <david@kineticode.com> for TLS and
#    Group membership support.
  #    Group membership support.
#
  #
#
  #
# CaonicalizeEmailAddress(), CanonicalizeUserInfo(), and LookupExternalInfo()
  # CaonicalizeEmailAddress(), CanonicalizeUserInfo(), and LookupExternalInfo()
# based on work by Phillip Cole (phillip d cole @ uk d coltgroup d com)
  # based on work by Phillip Cole (phillip d cole @ uk d coltgroup d com)
# found at:
  # found at:
#
  #
# http://wiki.bestpractical.com/view/AutoCreateAndCanonicalizeUserInfo
  # http://wiki.bestpractical.com/view/AutoCreateAndCanonicalizeUserInfo
#
  #
# His credits:
  # His credits:
#  based on CurrentUser_Local.pm and much help from the mailing lists
  #  based on CurrentUser_Local.pm and much help from the mailing lists
#
  #
# All integrated, refactored, and updated by Jim Meyer (purp@acm.org)
  # All integrated, refactored, and updated by Jim Meyer (purp@acm.org)
#
  #
# Changes:
  # Changes:
# v1.1b2 (2006.08.03) Modified by Phil Cole
  # v1.1b2 (2006.08.03) Modified by Phil Cole
#  * Add $LdapEmailAttrMatchPrefix config variable to make alternate email
  #  * Add $LdapEmailAttrMatchPrefix config variable to make alternate email
#    addresses work on Windows 2003 AD.
  #    addresses work on Windows 2003 AD.
# v1.1b1 (2006.06.05) Punch-Drunk Hamster Release
  # v1.1b1 (2006.06.05) Punch-Drunk Hamster Release
#  * Added UpdateFromLdap() to update the user's info from their LDAP entry;
  #  * Added UpdateFromLdap() to update the user's info from their LDAP entry;
#    this also uses the newly invented $RT::*DisableFilter settings to know
  #    this also uses the newly invented $RT::*DisableFilter settings to know
#    to disable their RT account when their LDAP account is disabled.
  #    to disable their RT account when their LDAP account is disabled.
#  * Added $RT::*DisableFilter settings for RT_SiteConfig.pm and refactored
  #  * Added $RT::*DisableFilter settings for RT_SiteConfig.pm and refactored
#    LdapConfigInfo() to include them. Also refactored logic to grep for
  #    LdapConfigInfo() to include them. Also refactored logic to grep for
#    filter keys rather than enumerate them
  #    filter keys rather than enumerate them
#  * Added _GetBoundLdapObj() and refactored IsLdapPassword() and
  #  * Added _GetBoundLdapObj() and refactored IsLdapPassword() and
#    LookupExternalUserInfo() to use it
  #    LookupExternalUserInfo() to use it
#  * Added LdapConfigAuthAndInfoAreSame() to compare Auth and Info settings
  #  * Added LdapConfigAuthAndInfoAreSame() to compare Auth and Info settings
# v1.0b1 (2006.01.06)
  # v1.0b1 (2006.01.06)
#  * Added $RT::AuthMethods as basis for auth method lists;
  #  * Added $RT::AuthMethods as basis for auth method lists;
#    currently supports LDAP, Internal
  #    currently supports LDAP, Internal
#  * Implemented Phillip Cole's suggested $RT::LdapAttrMap
  #  * Implemented Phillip Cole's suggested $RT::LdapAttrMap
#  * Implemented $RT::LdapRTAttrMatchList and $RT::LdapEmailAttrMatchList
  #  * Implemented $RT::LdapRTAttrMatchList and $RT::LdapEmailAttrMatchList
#    to help guide LDAP searches more effectively
  #    to help guide LDAP searches more effectively
#  * Added LdapAuth* and LdapInfo* variables to allow authentication
  #  * Added LdapAuth* and LdapInfo* variables to allow authentication
#    and information from separate LDAP servers; didn't invalidate
  #    and information from separate LDAP servers; didn't invalidate
#    older Ldap{Server,Base,User,Pass,etc} variables.
  #    older Ldap{Server,Base,User,Pass,etc} variables.
#  * Added LdapConfigInfo() to get integrated config info
  #  * Added LdapConfigInfo() to get integrated config info
 
no warnings qw(redefine);
  no warnings qw(redefine);
use strict;
  use strict;
use Net::LDAP qw(LDAP_SUCCESS LDAP_PARTIAL_RESULTS);
  use Net::LDAP qw(LDAP_SUCCESS LDAP_PARTIAL_RESULTS);
use Net::LDAP::Util qw(ldap_error_name);
  use Net::LDAP::Util qw(ldap_error_name);
use Net::LDAP::Filter;
  use Net::LDAP::Filter;
 
# We only need Net::SSLeay if we're using TLS to encrypt our LDAP connections
  # We only need Net::SSLeay if we're using TLS to encrypt our LDAP connections
require Net::SSLeay
  require Net::SSLeay
  if $RT::LdapTLS || $RT::LdapAuthTLS || $RT::LdapInfoTLS;
    if $RT::LdapTLS || $RT::LdapAuthTLS || $RT::LdapInfoTLS;
 
=head2 LdapConfigInfo
  =head2 LdapConfigInfo
 
returns the LDAP attr mapped to EmailAddress in $RT::LdapAttrMap.
  returns the LDAP attr mapped to EmailAddress in $RT::LdapAttrMap.
 
If no result is found, returns the ADDRESS passed in.
  If no result is found, returns the ADDRESS passed in.
 
=cut
  =cut
 
sub LdapConfigInfo {
  sub LdapConfigInfo {
    my $self = shift;
      my $self = shift;
 
    my %config;
      my %config;
 
    # Figure out what's what
      # Figure out what's what
    $config{'AuthServer'}    = $RT::LdapServer    || $RT::LdapAuthServer;
      $config{'AuthServer'}    = $RT::LdapServer    || $RT::LdapAuthServer;
    $config{'AuthBase'}      = $RT::LdapBase      || $RT::LdapAuthBase;
      $config{'AuthBase'}      = $RT::LdapBase      || $RT::LdapAuthBase;
    $config{'AuthUser'}      = $RT::LdapUser      || $RT::LdapAuthUser;
      $config{'AuthUser'}      = $RT::LdapUser      || $RT::LdapAuthUser;
    $config{'AuthPass'}      = $RT::LdapPass      || $RT::LdapAuthPass;
      $config{'AuthPass'}      = $RT::LdapPass      || $RT::LdapAuthPass;
    $config{'AuthFilter'}    = $RT::LdapFilter    || $RT::LdapAuthFilter;
      $config{'AuthFilter'}    = $RT::LdapFilter    || $RT::LdapAuthFilter;
    $config{'AuthGroup'}      = $RT::LdapGroup      || $RT::LdapAuthGroup;
      $config{'AuthGroup'}      = $RT::LdapGroup      || $RT::LdapAuthGroup;
    $config{'AuthTLS'}        = $RT::LdapTLS        || $RT::LdapAuthTLS;
      $config{'AuthTLS'}        = $RT::LdapTLS        || $RT::LdapAuthTLS;
    $config{'AuthSSLVersion'} = $RT::LdapSSLVersion || $RT::LdapAuthSSLVersion;
      $config{'AuthSSLVersion'} = $RT::LdapSSLVersion || $RT::LdapAuthSSLVersion;
    $config{'AuthDisableFilter'} =
      $config{'AuthDisableFilter'} =
      $RT::LdapDisableFilter || $RT::LdapAuthDisableFilter;
        $RT::LdapDisableFilter || $RT::LdapAuthDisableFilter;
 
    # We grandfather in LdapGroupAttribute; we'll default
      # We grandfather in LdapGroupAttribute; we'll default
    # to 'uniqueMember'
      # to 'uniqueMember'
    $config{'AuthGroupAttr'} =
      $config{'AuthGroupAttr'} =
      $RT::LdapGroupAttribute || $RT::LdapGroupAttr ||
        $RT::LdapGroupAttribute || $RT::LdapGroupAttr ||
      $RT::LdapAuthGroupAttr || 'uniqueMember';
        $RT::LdapAuthGroupAttr || 'uniqueMember';
 
    # Figure out what's what
      # Figure out what's what
    $config{'InfoServer'}    = $RT::LdapServer    || $RT::LdapInfoServer;
      $config{'InfoServer'}    = $RT::LdapServer    || $RT::LdapInfoServer;
    $config{'InfoBase'}      = $RT::LdapBase      || $RT::LdapInfoBase;
      $config{'InfoBase'}      = $RT::LdapBase      || $RT::LdapInfoBase;
    $config{'InfoUser'}      = $RT::LdapUser      || $RT::LdapInfoUser;
      $config{'InfoUser'}      = $RT::LdapUser      || $RT::LdapInfoUser;
    $config{'InfoPass'}      = $RT::LdapPass      || $RT::LdapInfoPass;
      $config{'InfoPass'}      = $RT::LdapPass      || $RT::LdapInfoPass;
    $config{'InfoFilter'}    = $RT::LdapFilter    || $RT::LdapInfoFilter;
      $config{'InfoFilter'}    = $RT::LdapFilter    || $RT::LdapInfoFilter;
    $config{'InfoTLS'}        = $RT::LdapTLS        || $RT::LdapInfoTLS;
      $config{'InfoTLS'}        = $RT::LdapTLS        || $RT::LdapInfoTLS;
    $config{'InfoSSLVersion'} = $RT::LdapSSLVersion || $RT::LdapInfoSSLVersion;
      $config{'InfoSSLVersion'} = $RT::LdapSSLVersion || $RT::LdapInfoSSLVersion;
    $config{'InfoDisableFilter'} =
      $config{'InfoDisableFilter'} =
      $RT::LdapDisableFilter || $RT::LdapInfoDisableFilter;
        $RT::LdapDisableFilter || $RT::LdapInfoDisableFilter;
 
    # Filters need parens if they don't have 'em
      # Filters need parens if they don't have 'em
    foreach my $filter (grep {/Filter$/} keys(%config)) {
      foreach my $filter (grep {/Filter$/} keys(%config)) {
        $config{$filter} = "($config{$filter})"
          $config{$filter} = "($config{$filter})"
          unless $config{$filter} =~ /^\(.*\)$/;
            unless $config{$filter} =~ /^\(.*\)$/;
    }
      }
 
    return wantarray ? %config : \%config;
      return wantarray ? %config : \%config;
}
  }
 
 
sub LdapConfigAuthAndInfoAreSame {
  sub LdapConfigAuthAndInfoAreSame {
    my $self = shift;
      my $self = shift;
 
    my %ldap_config = $self-&gt;LdapConfigInfo();
      my %ldap_config = $self->LdapConfigInfo();
 
    # Quick lazy check: same number of keys for Auth and Info?
      # Quick lazy check: same number of keys for Auth and Info?
    return 0
      return 0
      unless scalar(grep {/^Info/} keys(%ldap_config)) ==
        unless scalar(grep {/^Info/} keys(%ldap_config)) ==
        scalar(grep {/^Auth/} keys(%ldap_config));
          scalar(grep {/^Auth/} keys(%ldap_config));
 
    # Longer check
      # Longer check
    foreach my $key (grep {/^Auth/} keys(%ldap_config)) {
      foreach my $key (grep {/^Auth/} keys(%ldap_config)) {
        my ($key_base) = $key =~ /^Auth(.*)/;
          my ($key_base) = $key =~ /^Auth(.*)/;
        return 0
          return 0
          unless $ldap_config{"Auth${key_base}"} eq
            unless $ldap_config{"Auth${key_base}"} eq
            $ldap_config{"Info${key_base}"};
              $ldap_config{"Info${key_base}"};
    }
      }
 
    return 1;
      return 1;
}
  }
 
 
sub IsLDAPPassword {
  sub IsLDAPPassword {
    my $self = shift;
      my $self = shift;
    my $value = shift;
      my $value = shift;
 
    # Don't ask for external authentication unless enabled in RT_SiteConfig
      # Don't ask for external authentication unless enabled in RT_SiteConfig
    unless ($RT::LdapExternalAuth) {
      unless ($RT::LdapExternalAuth) {
        $RT::Logger-&gt;warning((caller(0))[3],
          $RT::Logger->warning((caller(0))[3],
                              '$RT::LdapExternalAuth is not set');
                              '$RT::LdapExternalAuth is not set');
        return;
          return;
    }
      }
 
    $RT::Logger-&gt;debug("Trying LDAP authentication\n");
      $RT::Logger->debug("Trying LDAP authentication\n");
 
    # Figure out what's what
      # Figure out what's what
    my %ldap_config    = $self-&gt;LdapConfigInfo;
      my %ldap_config    = $self->LdapConfigInfo;
    my $ldap_base      = $ldap_config{'AuthBase'};
      my $ldap_base      = $ldap_config{'AuthBase'};
    my $ldap_filter    = $ldap_config{'AuthFilter'};
      my $ldap_filter    = $ldap_config{'AuthFilter'};
    my $ldap_group      = $ldap_config{'AuthGroup'};
      my $ldap_group      = $ldap_config{'AuthGroup'};
    my $ldap_group_attr = $ldap_config{'AuthGroupAttr'};
      my $ldap_group_attr = $ldap_config{'AuthGroupAttr'};
 
    # Now let's get connected
      # Now let's get connected
    my $ldap = $self-&gt;_GetBoundLdapObj('Auth', version=&gt;3);
      my $ldap = $self->_GetBoundLdapObj('Auth', version=>3);
    return unless ($ldap);
      return unless ($ldap);
 
    my $filter_string = '(&amp;(' . $RT::LdapAttrMap-&gt;{'Name'} . '=' .
      my $filter_string = '(&(' . $RT::LdapAttrMap->{'Name'} . '=' .
      $self-&gt;Name . ')' . $ldap_filter . ')';
        $self->Name . ')' . $ldap_filter . ')';
    my $filter = Net::LDAP::Filter-&gt;new($filter_string);
      my $filter = Net::LDAP::Filter->new($filter_string);
 
    my $ldap_msg = $ldap-&gt;search(base  =&gt; $ldap_base,
      my $ldap_msg = $ldap->search(base  => $ldap_base,
                          filter =&gt; $filter,
                          filter => $filter,
                          attrs  =&gt; ['dn']);
                          attrs  => ['dn']);
 
    unless ($ldap_msg-&gt;code == LDAP_SUCCESS ||
      unless ($ldap_msg->code == LDAP_SUCCESS ||
            $ldap_msg-&gt;code == LDAP_PARTIAL_RESULTS) {
              $ldap_msg->code == LDAP_PARTIAL_RESULTS) {
        $RT::Logger-&gt;debug((caller(0))[3], "search for", $filter-&gt;as_string,
          $RT::Logger->debug((caller(0))[3], "search for", $filter->as_string,
                          "failed:", ldap_error_name($ldap_msg-&gt;code), $ldap_msg-&gt;code);
                            "failed:", ldap_error_name($ldap_msg->code), $ldap_msg->code);
        return;
          return;
    }
      }
 
    unless ($ldap_msg-&gt;count == 1) {
      unless ($ldap_msg->count == 1) {
        $RT::Logger-&gt;info((caller(0))[3], "AUTH FAILED:", $self-&gt;Name);
          $RT::Logger->info((caller(0))[3], "AUTH FAILED:", $self->Name);
        return;
          return;
    }
      }
 
    my $ldap_dn = $ldap_msg-&gt;first_entry-&gt;dn;
      my $ldap_dn = $ldap_msg->first_entry->dn;
    $RT::Logger-&gt;debug((caller(0))[3], "Found LDAP DN:", $ldap_dn);
      $RT::Logger->debug((caller(0))[3], "Found LDAP DN:", $ldap_dn);
 
    $ldap_msg = $ldap-&gt;bind($ldap_dn, password =&gt; $value);
      $ldap_msg = $ldap->bind($ldap_dn, password => $value);
 
    unless ($ldap_msg-&gt;code == LDAP_SUCCESS) {
      unless ($ldap_msg->code == LDAP_SUCCESS) {
        $RT::Logger-&gt;info((caller(0))[3], "AUTH FAILED", $self-&gt;Name,
          $RT::Logger->info((caller(0))[3], "AUTH FAILED", $self->Name,
                          "(can't bind:", ldap_error_name($ldap_msg-&gt;code),
                            "(can't bind:", ldap_error_name($ldap_msg->code),
                          $ldap_msg-&gt;code, ")");
                            $ldap_msg->code, ")");
        return;
          return;
    }
      }
 
    # Is there an LDAP Group to check?
      # Is there an LDAP Group to check?
    if ($ldap_group) {
      if ($ldap_group) {
        $filter = Net::LDAP::Filter-&gt;new("(${ldap_group_attr}=${ldap_dn})");
          $filter = Net::LDAP::Filter->new("(${ldap_group_attr}=${ldap_dn})");
 
        $ldap_msg = $ldap-&gt;search(base  =&gt; $ldap_group,
          $ldap_msg = $ldap->search(base  => $ldap_group,
                              filter =&gt; $filter,
                              filter => $filter,
                              attrs  =&gt; ['dn'],
                              attrs  => ['dn'],
                              scope  =&gt; 'base');
                              scope  => 'base');
 
        unless ($ldap_msg-&gt;code == LDAP_SUCCESS ||
          unless ($ldap_msg->code == LDAP_SUCCESS ||
                $ldap_msg-&gt;code == LDAP_PARTIAL_RESULTS) {
                  $ldap_msg->code == LDAP_PARTIAL_RESULTS) {
            $RT::Logger-&gt;critical((caller(0))[3],
              $RT::Logger->critical((caller(0))[3],
                                  "Search for", $filter-&gt;as_string, "failed:",
                                    "Search for", $filter->as_string, "failed:",
                                  ldap_error_name($ldap_msg-&gt;code), $ldap_msg-&gt;code);
                                    ldap_error_name($ldap_msg->code), $ldap_msg->code);
            return;
              return;
        }
          }
 
        unless ($ldap_msg-&gt;count == 1) {
          unless ($ldap_msg->count == 1) {
            $RT::Logger-&gt;info((caller(0))[3], "AUTH FAILED:", $self-&gt;Name);
              $RT::Logger->info((caller(0))[3], "AUTH FAILED:", $self->Name);
            return;
              return;
        }
          }
    }
      }
 
    # If we've survived to this point, we're good.
      # If we've survived to this point, we're good.
    $RT::Logger-&gt;info((caller(0))[3], "AUTH OK:", $self-&gt;Name, "($ldap_dn)");
      $RT::Logger->info((caller(0))[3], "AUTH OK:", $self->Name, "($ldap_dn)");
 
    return 1;
      return 1;
}
  }
 
sub IsInternalPassword {
  sub IsInternalPassword {
    my $self = shift;
      my $self = shift;
    my $value = shift;
      my $value = shift;
 
    unless ($self-&gt;HasPassword) {
      unless ($self->HasPassword) {
        $RT::Logger-&gt;info((caller(0))[3],
          $RT::Logger->info((caller(0))[3],
                          "AUTH FAILED (no passwd):", $self-&gt;Name);
                            "AUTH FAILED (no passwd):", $self->Name);
        return undef;
          return undef;
    }
      }
 
    # generate an md5 password
      # generate an md5 password
    if ($self-&gt;_GeneratePassword($value) eq $self-&gt;__Value('Password')) {
      if ($self->_GeneratePassword($value) eq $self->__Value('Password')) {
        $RT::Logger-&gt;info((caller(0))[3],
          $RT::Logger->info((caller(0))[3],
                          "AUTH OKAY:", $self-&gt;Name);
                            "AUTH OKAY:", $self->Name);
        return 1;
          return 1;
    }
      }
 
    #  if it's a historical password we say ok.
      #  if it's a historical password we say ok.
    if ($self-&gt;__Value('Password') eq crypt($value, $self-&gt;__Value('Password'))
      if ($self->__Value('Password') eq crypt($value, $self->__Value('Password'))
        or $self-&gt;_GeneratePasswordBase64($value) eq $self-&gt;__Value('Password'))
          or $self->_GeneratePasswordBase64($value) eq $self->__Value('Password'))
      {
        {
          # ...but upgrade the legacy password inplace.
            # ...but upgrade the legacy password inplace.
          $self-&gt;SUPER::SetPassword( $self-&gt;_GeneratePassword($value) );
            $self->SUPER::SetPassword( $self->_GeneratePassword($value) );
          $RT::Logger-&gt;info((caller(0))[3],
            $RT::Logger->info((caller(0))[3],
                            "AUTH OKAY:", $self-&gt;Name);
                              "AUTH OKAY:", $self->Name);
          return 1;
            return 1;
      }
    $RT::Logger-&gt;info((caller(0))[3], "AUTH FAILED:", $self-&gt;Name);
    return undef;
}
# {{{ sub IsPassword
sub IsPassword {
    my $self  = shift;
    my $value = shift;
    #TODO there isn't any apparent way to legitimately ACL this
    # RT does not allow null passwords
    if ( !defined($value) || $value eq '' ) {
        return undef;
    }
    if ( $self-&gt;PrincipalObj-&gt;Disabled ) {
        $RT::Logger-&gt;info("Disabled user " . $self-&gt;Name .
                          " tried to log in" );
        return undef;
    }
    my @auth_methods = $RT::AuthMethods ? @{$RT::AuthMethods} : ('Internal');
    my $success;
    foreach my $method (@auth_methods) {
        $method = "Is${method}Password";
        # Eval this since they might specify an auth method without
        # an "Is&lt;auth&gt;Password" method implemented
        eval {
            $success = $self-&gt;$method($value);
        };
        $RT::Logger-&gt;debug((caller(0))[3], "auth method $method",
                            ($success ? 'SUCCEEDED' : 'FAILED'));
        last if $success;
    }
    # We either got it or we didn't
    return $success;
}
# }}}
=head2 CanonicalizeEmailAddress ADDRESS
returns the LDAP attr mapped to EmailAddress in $RT::LdapAttrMap.
If no result is found, returns the ADDRESS passed in.
=cut
sub CanonicalizeEmailAddress {
    my $self = shift;
    my $email = shift;
    my $found = undef;
    my %params = ('EmailAddress' =&gt; $email);
    $self = RT::User-&gt;new($RT::SystemUser) unless $self;
    # Don't ask for external info unless enabled in RT_SiteConfig
    unless ($RT::LdapExternalInfo) {
        $RT::Logger-&gt;warning((caller(0))[3],
                              '$RT::LdapExternalInfo is not set');
        return $email;
    }
    $RT::Logger-&gt;debug((caller(0))[3], ": called with \"$email\" by", caller);
    if ($email) {
        foreach my $prefix (@{$RT::LdapEmailAttrMatchPrefix}) {
            if (!$found) {
                foreach my $attr (@{$RT::LdapEmailAttrMatchList}) {
                    ($found, %params) =
                      $self-&gt;LookupExternalUserInfo($attr, "$prefix$email");
                    if ($found) {
                        $RT::Logger-&gt;debug("FOUND OK");
                    }
                    last if $found;
                }
            }
         }
         }
    }
 
      $RT::Logger->info((caller(0))[3], "AUTH FAILED:", $self->Name);
    my $new_email = $found ? $params{'EmailAddress'} : $email;
 
      return undef;
    $RT::Logger-&gt;info((caller(0))[3], "$email =&gt;  $new_email");
  }
 
    return $new_email;
  # {{{ sub IsPassword
}
 
  sub IsPassword {
# {{{ sub CanonicalizeUserInfo
      my $self = shift;
      my $value = shift;
=head2 CanonicalizeUserInfo HASHREF
 
      #TODO there isn't any apparent way to legitimately ACL this
Get all LDAP attrs listed in $RT::LdapAttrMap and put them into
 
the hash referred to by HASHREF.
      # RT does not allow null passwords
      if ( !defined($value) || $value eq '' ) {
returns true (1) if LDAP lookup was successful, false (undef)
          return undef;
in all other cases.
      }
 
=cut
      if ( $self->PrincipalObj->Disabled ) {
          $RT::Logger->info("Disabled user " . $self->Name .
sub CanonicalizeUserInfo {
                            " tried to log in" );
    my $self = shift;
          return undef;
    my $args = shift;
      }
 
    my $found = 0;
      my @auth_methods = $RT::AuthMethods ? @{$RT::AuthMethods} : ('Internal');
    my %params;
      my $success;
 
    # Don't ask for external info unless enabled in RT_SiteConfig
      foreach my $method (@auth_methods) {
    unless ($RT::LdapExternalInfo) {
          $method = "Is${method}Password";
        $RT::Logger-&gt;warning((caller(0))[3],
 
                              '$RT::LdapExternalInfo is not set');
          # Eval this since they might specify an auth method without
        # We return true so that they'll go forward with what info they have
          # an "Is<auth>Password" method implemented
        return 1;
          eval {
    }
              $success = $self->$method($value);
          };
    $RT::Logger-&gt;debug((caller(0))[3], " called by", caller, "with:",
 
                        join(", ", map {sprintf("%s: %s", $_, $args-&gt;{$_})}
          $RT::Logger->debug((caller(0))[3], "auth method $method",
                            sort(keys(%$args))));
                            ($success ? 'SUCCEEDED' : 'FAILED'));
          last if $success;
    # $args is a hash ref; to get at its values, use $args-&gt;{&lt;key&gt;}
      }
    #
 
    # $args has keys:
      # We either got it or we didn't
    #    RealName    - User human name (e.g. Cole, Phillip)
      return $success;
    #    Name        - Username or login (e.g. ukpgc)
  }
    #    EmailAddress - Email address (e.g. phillip.cole@company.com)
 
    #    Comments    - Comments created during creation
  # }}}
 
    # How may I know thee? Let me count the ways ...
  =head2 CanonicalizeEmailAddress ADDRESS
    foreach my $rt_attr (@{$RT::LdapRTAttrMatchList}) {
 
        next unless $args-&gt;{$rt_attr};
  returns the LDAP attr mapped to EmailAddress in $RT::LdapAttrMap.
 
        ($found, %params) =
  If no result is found, returns the ADDRESS passed in.
          $self-&gt;LookupExternalUserInfo($RT::LdapAttrMap-&gt;{$rt_attr},
 
                                            $args-&gt;{$rt_attr});
  =cut
        last if $found;
 
    }
  sub CanonicalizeEmailAddress {
      my $self = shift;
    if ($found) {
      my $email = shift;
        # It's important that we always have a canonical email address
 
        if ($params{'EmailAddress'}) {
      my $found = undef;
            my $email =
      my %params = ('EmailAddress' => $email);
              $self-&gt;CanonicalizeEmailAddress($params{'EmailAddress'});
 
            $params{'EmailAddress'} = $email if $email;
      $self = RT::User->new($RT::SystemUser) unless $self;
        }
 
      # Don't ask for external info unless enabled in RT_SiteConfig
        %$args = (%$args, %params);
      unless ($RT::LdapExternalInfo) {
    }
          $RT::Logger->warning((caller(0))[3],
                              '$RT::LdapExternalInfo is not set');
    $RT::Logger-&gt;info((caller(0))[3], "returning",
          return $email;
                        join(", ", map {sprintf("%s: %s", $_, $args-&gt;{$_})}
      }
                            sort(keys(%$args))));
 
    ### HACK: The config var below is to overcome the (IMO) bug in
       $RT::Logger->debug((caller(0))[3], ": called with \"$email\" by", caller);
    ### RT::User::Create() which expects this function to always
 
    ### return true or rejects the user for creation. This should be
      if ($email) {
    ### a different config var (CreateUncanonicalizedUsers) and
         foreach my $prefix (@{$RT::LdapEmailAttrMatchPrefix}) {
    ### should be honored in RT::User::Create()
            if (!$found) {
    return $found || $RT::LdapAutoCreateNonLdapUsers;
                foreach my $attr (@{$RT::LdapEmailAttrMatchList}) {
}
                    ($found, %params) =
# }}}
                      $self->LookupExternalUserInfo($attr, "$prefix$email");
                    if ($found) {
sub _GetBoundLdapObj {
                        $RT::Logger->debug("FOUND OK");
    my $self = shift;
                    }
    my ($service, @ldap_args) = @_;
                    last if $found;
 
    # Figure out what's what
    my %ldap_config    = $self-&gt;LdapConfigInfo();
    my $ldap_server    = $ldap_config{"${service}Server"};
    my $ldap_base      = $ldap_config{"${service}Base"};
    my $ldap_user      = $ldap_config{"${service}User"};
    my $ldap_pass      = $ldap_config{"${service}Pass"};
    my $ldap_filter    = $ldap_config{"${service}Filter"};
    my $ldap_tls        = $ldap_config{"${service}TLS"};
    my $ldap_ssl_ver    = $ldap_config{"${service}SSLVersion"};
    my $ldap = new Net::LDAP($ldap_server, @ldap_args);
    unless ($ldap) {
        $RT::Logger-&gt;critical((caller(0))[3],
                              ": Cannot connect to $ldap_server");
        return undef;
    }
    if ($ldap_tls) {
        $Net::SSLeay::ssl_version = $ldap_ssl_ver;
        # Thanks to David Narayan for the fault tolerance bits
        eval { $ldap-&gt;start_tls; };
        if ($@) {
            $RT::Logger-&gt;critical((caller(0))[3], "Can't start TLS: $@");
            return;
        }
    }
    my $msg = undef;
    if ($ldap_user) {
        $msg = $ldap-&gt;bind($ldap_user, password =&gt; $ldap_pass);
    } else {
        $msg = $ldap-&gt;bind;
    }
    unless ($msg-&gt;code == LDAP_SUCCESS) {
        $RT::Logger-&gt;critical((caller(0))[3], "Can't bind:",
                              ldap_error_name($msg-&gt;code), $msg-&gt;code);
        return undef;
    } else {
        return $ldap;
    }
}
# {{{ sub LookupExternalUserInfo
=head2 LookupExternalUserInfo KEY VALUE [BASE_DN]
LookupExternalUserInfo takes a key/value pair with optional LDAP baseDN,
looks it up in LDAP, and returns a params hash containing all LDAP attrs
listed in $RT::LdapAttrMap, suitable for creating an RT::User object.
Returns a tuple, ($found, %params)
=cut
sub LookupExternalUserInfo {
    my $self = shift;
    my ($key, $value, $baseDN) = @_;
    my $found = 0;
    my %params = (Name        =&gt; undef,
                  EmailAddress =&gt; undef,
                  RealName    =&gt; undef);
    # Don't ask for external info unless enabled in RT_SiteConfig
    unless ($RT::LdapExternalInfo) {
        $RT::Logger-&gt;warning((caller(0))[3],
                              '$RT::LdapExternalInfo is not set');
        return ($found, %params);
    }
    # Figure out what's what
    my %ldap_config    = $self-&gt;LdapConfigInfo();
    my $ldap_base       = $ldap_config{'InfoBase'};
    my $ldap_filter    = $ldap_config{'InfoFilter'};
    $baseDN = $baseDN || $ldap_base;
    ### This should use Net::LDAP::Filter, too
    my $filter = ($key &amp;&amp; $value) ? "@{[ $key ]}=$value" : "";
    $RT::Logger-&gt;debug((caller(0))[3], "called with baseDN \"$baseDN\"",
                        "and filter \"$filter\" by", caller);
    unless ($baseDN) {
         $RT::Logger-&gt;critical((caller(0))[3] . " No baseDN given");
        return ($found, %params);
    }
    my $ldap = $self-&gt;_GetBoundLdapObj('Info');
    return ($found, %params) unless ($ldap);
    # Get the list of unique attrs we need
    my %ldap_attrs = map {$_ =&gt; 1} values(%{$RT::LdapAttrMap});
    my @attrs = keys(%ldap_attrs);
    my $ldap_msg = $ldap-&gt;search(base  =&gt; $baseDN,
                                  filter =&gt; $filter,
                                  attrs  =&gt; \@attrs);
    if ($ldap_msg-&gt;code != LDAP_SUCCESS and
        $ldap_msg-&gt;code != LDAP_PARTIAL_RESULTS) {
        $RT::Logger-&gt;critical((caller(0))[3],
                              ": Search for $filter failed: ",
                              ldap_error_name($ldap_msg-&gt;code), $ldap_msg-&gt;code);
        # Why on earth do we return the same RealName, just quoted?!
        $params{'RealName'} = "\"$params{'RealName'}\"";
    } else {
        # If there's only one match, we're good; more than one and
        # we don't know which is the right one so we skip it.
        if ($ldap_msg-&gt;count == 1) {
            my $entry = $ldap_msg-&gt;first_entry();
            foreach my $key (keys(%{$RT::LdapAttrMap})) {
                if ($RT::LdapAttrMap-&gt;{$key} eq 'dn') {
                    $params{$key} = $entry-&gt;dn();
                } else {
                    $params{$key} =
                      ($entry-&gt;get_value($RT::LdapAttrMap-&gt;{$key}))[0];
                 }
                 }
             }
             }
            $found = 1;
        }
    }
    $ldap_msg = $ldap-&gt;unbind();
    if ($ldap_msg-&gt;code != LDAP_SUCCESS) {
        $RT::Logger-&gt;critical((caller(0))[3],
                              ": Could not unbind: ",
                              ldap_error_name($ldap_msg-&gt;code), $ldap_msg-&gt;code);
    }
    undef $ldap;
    undef $ldap_msg;
    $RT::Logger-&gt;info((caller(0))[3],
                        ": $baseDN $filter =&gt; ",
                        join(", ", map {sprintf("%s: %s", $_, $params{$_})}
                            sort(keys(%params))));
    return ($found, %params);
}
# }}}
sub UpdateFromLdap {
    my $self = shift;
    my $updated = 0;
    my $msg = "User NOT updated";
    my %ldap_config = $self-&gt;LdapConfigInfo;
    my @services;
    push(@services, 'Auth') if $ldap_config{'AuthDisableFilter'};
    push(@services, 'Info') if $ldap_config{'InfoDisableFilter'};
    # If Auth and Info use exactly the same params, only check one
    @services = ('Auth')
      if (@services &amp;&amp; $self-&gt;LdapConfigAuthAndInfoAreSame);
    foreach my $service (@services) {
        my $ldap = $self-&gt;_GetBoundLdapObj($service);
        next unless $ldap;
        my $ldap_base    = $ldap_config{"${service}Base"};
        my $ldap_filter  = $ldap_config{"${service}Filter"};
        my $ldap_disable = $ldap_config{"${service}DisableFilter"};
        # Construct the complex filter
        my $filter = '(&amp;' . $ldap_filter . $ldap_disable .
          '(uid=' . $self-&gt;Name . '))';
        my $disabled_users = $ldap-&gt;search(base  =&gt; $ldap_base,
                                            filter =&gt; $filter,
                                            attrs  =&gt; ['uid']);
        if ($disabled_users-&gt;count) {
            my $UserObj = RT::User-&gt;new($RT::SystemUser);
            $UserObj-&gt;Load($self-&gt;Name);
            my ($val, $message) = $UserObj-&gt;SetDisabled(1);
            $RT::Logger-&gt;info("DISABLED user " . $self-&gt;Name .
                              " per LDAP ($val, $message)\n");
            $msg = "User disabled";
        } else {
            # Update their info from LDAP
            my %args = (Name =&gt; $self-&gt;Name);
            $self-&gt;CanonicalizeUserInfo(\%args);
            foreach my $key (sort(keys(%args))) {
                next unless $args{$key};
                my $method = "Set$key";
                $self-&gt;$method($args{$key});
            }
            $updated = 1;
            $RT::Logger-&gt;debug("UPDATED user " . $self-&gt;Name . " from LDAP\n");
            $msg = 'User updated';
         }
         }
    }
      }
    return ($updated, $msg);
 
  }
      my $new_email = $found ? $params{'EmailAddress'} : $email;
 
1;
      $RT::Logger->info((caller(0))[3], "$email =>  $new_email");
 
</nowiki>
      return $new_email;
[[Category:пензец]]
  }
 
  # {{{ sub CanonicalizeUserInfo
 
  =head2 CanonicalizeUserInfo HASHREF
 
  Get all LDAP attrs listed in $RT::LdapAttrMap and put them into
  the hash referred to by HASHREF.
 
  returns true (1) if LDAP lookup was successful, false (undef)
  in all other cases.
 
  =cut
 
  sub CanonicalizeUserInfo {
      my $self = shift;
      my $args = shift;
 
      my $found = 0;
      my %params;
 
      # Don't ask for external info unless enabled in RT_SiteConfig
      unless ($RT::LdapExternalInfo) {
          $RT::Logger->warning((caller(0))[3],
                              '$RT::LdapExternalInfo is not set');
          # We return true so that they'll go forward with what info they have
          return 1;
      }
 
      $RT::Logger->debug((caller(0))[3], " called by", caller, "with:",
                        join(", ", map {sprintf("%s: %s", $_, $args->{$_})}
                              sort(keys(%$args))));
 
      # $args is a hash ref; to get at its values, use $args->{<key>}
      #
      # $args has keys:
      #    RealName    - User human name (e.g. Cole, Phillip)
      #    Name        - Username or login (e.g. ukpgc)
      #    EmailAddress - Email address (e.g. phillip.cole@company.com)
      #    Comments    - Comments created during creation
 
      # How may I know thee? Let me count the ways ...
      foreach my $rt_attr (@{$RT::LdapRTAttrMatchList}) {
          next unless $args->{$rt_attr};
 
          ($found, %params) =
            $self->LookupExternalUserInfo($RT::LdapAttrMap->{$rt_attr},
                                            $args->{$rt_attr});
          last if $found;
      }
 
      if ($found) {
          # It's important that we always have a canonical email address
          if ($params{'EmailAddress'}) {
              my $email =
                $self->CanonicalizeEmailAddress($params{'EmailAddress'});
              $params{'EmailAddress'} = $email if $email;
          }
 
          %$args = (%$args, %params);
      }
 
      $RT::Logger->info((caller(0))[3], "returning",
                        join(", ", map {sprintf("%s: %s", $_, $args->{$_})}
                              sort(keys(%$args))));
      ### HACK: The config var below is to overcome the (IMO) bug in
      ### RT::User::Create() which expects this function to always
      ### return true or rejects the user for creation. This should be
      ### a different config var (CreateUncanonicalizedUsers) and
      ### should be honored in RT::User::Create()
      return $found || $RT::LdapAutoCreateNonLdapUsers;
  }
  # }}}
 
  sub _GetBoundLdapObj {
      my $self = shift;
      my ($service, @ldap_args) = @_;
 
      # Figure out what's what
      my %ldap_config    = $self->LdapConfigInfo();
      my $ldap_server    = $ldap_config{"${service}Server"};
      my $ldap_base      = $ldap_config{"${service}Base"};
      my $ldap_user      = $ldap_config{"${service}User"};
      my $ldap_pass      = $ldap_config{"${service}Pass"};
      my $ldap_filter    = $ldap_config{"${service}Filter"};
      my $ldap_tls        = $ldap_config{"${service}TLS"};
      my $ldap_ssl_ver    = $ldap_config{"${service}SSLVersion"};
 
      my $ldap = new Net::LDAP($ldap_server, @ldap_args);
      unless ($ldap) {
          $RT::Logger->critical((caller(0))[3],
                                ": Cannot connect to $ldap_server");
          return undef;
      }
 
      if ($ldap_tls) {
          $Net::SSLeay::ssl_version = $ldap_ssl_ver;
          # Thanks to David Narayan for the fault tolerance bits
          eval { $ldap->start_tls; };
          if ($@) {
              $RT::Logger->critical((caller(0))[3], "Can't start TLS: $@");
              return;
          }
 
      }
 
      my $msg = undef;
 
      if ($ldap_user) {
          $msg = $ldap->bind($ldap_user, password => $ldap_pass);
      } else {
          $msg = $ldap->bind;
      }
 
      unless ($msg->code == LDAP_SUCCESS) {
          $RT::Logger->critical((caller(0))[3], "Can't bind:",
                              ldap_error_name($msg->code), $msg->code);
          return undef;
      } else {
          return $ldap;
      }
  }
 
 
  # {{{ sub LookupExternalUserInfo
 
 
  =head2 LookupExternalUserInfo KEY VALUE [BASE_DN]
 
  LookupExternalUserInfo takes a key/value pair with optional LDAP baseDN,
  looks it up in LDAP, and returns a params hash containing all LDAP attrs
  listed in $RT::LdapAttrMap, suitable for creating an RT::User object.
 
  Returns a tuple, ($found, %params)
 
  =cut
 
  sub LookupExternalUserInfo {
      my $self = shift;
      my ($key, $value, $baseDN) = @_;
 
      my $found = 0;
      my %params = (Name        => undef,
                    EmailAddress => undef,
                    RealName    => undef);
 
 
      # Don't ask for external info unless enabled in RT_SiteConfig
      unless ($RT::LdapExternalInfo) {
          $RT::Logger->warning((caller(0))[3],
                              '$RT::LdapExternalInfo is not set');
          return ($found, %params);
      }
 
      # Figure out what's what
      my %ldap_config    = $self->LdapConfigInfo();
      my $ldap_base      = $ldap_config{'InfoBase'};
      my $ldap_filter    = $ldap_config{'InfoFilter'};
 
      $baseDN = $baseDN || $ldap_base;
 
      ### This should use Net::LDAP::Filter, too
      my $filter = ($key && $value) ? "@{[ $key ]}=$value" : "";
 
      $RT::Logger->debug((caller(0))[3], "called with baseDN \"$baseDN\"",
                        "and filter \"$filter\" by", caller);
 
      unless ($baseDN) {
          $RT::Logger->critical((caller(0))[3] . " No baseDN given");
          return ($found, %params);
      }
 
      my $ldap = $self->_GetBoundLdapObj('Info');
 
      return ($found, %params) unless ($ldap);
 
      # Get the list of unique attrs we need
      my %ldap_attrs = map {$_ => 1} values(%{$RT::LdapAttrMap});
      my @attrs = keys(%ldap_attrs);
      my $ldap_msg = $ldap->search(base  => $baseDN,
                                  filter => $filter,
                                  attrs => \@attrs);
 
      if ($ldap_msg->code != LDAP_SUCCESS and
          $ldap_msg->code != LDAP_PARTIAL_RESULTS) {
          $RT::Logger->critical((caller(0))[3],
                                ": Search for $filter failed: ",
                                ldap_error_name($ldap_msg->code), $ldap_msg->code);
 
          # Why on earth do we return the same RealName, just quoted?!
          $params{'RealName'} = "\"$params{'RealName'}\"";
      } else {
          # If there's only one match, we're good; more than one and
          # we don't know which is the right one so we skip it.
          if ($ldap_msg->count == 1) {
              my $entry = $ldap_msg->first_entry();
              foreach my $key (keys(%{$RT::LdapAttrMap})) {
                  if ($RT::LdapAttrMap->{$key} eq 'dn') {
                      $params{$key} = $entry->dn();
                  } else {
                      $params{$key} =
                        ($entry->get_value($RT::LdapAttrMap->{$key}))[0];
                  }
              }
              $found = 1;
          }
      }
      $ldap_msg = $ldap->unbind();
      if ($ldap_msg->code != LDAP_SUCCESS) {
          $RT::Logger->critical((caller(0))[3],
                                ": Could not unbind: ",
                                ldap_error_name($ldap_msg->code), $ldap_msg->code);
      }
 
      undef $ldap;
      undef $ldap_msg;
 
      $RT::Logger->info((caller(0))[3],
                        ": $baseDN $filter => ",
                        join(", ", map {sprintf("%s: %s", $_, $params{$_})}
                              sort(keys(%params))));
 
      return ($found, %params);
  }
 
  # }}}
 
  sub UpdateFromLdap {
      my $self = shift;
      my $updated = 0;
      my $msg = "User NOT updated";
 
      my %ldap_config = $self->LdapConfigInfo;
 
      my @services;
      push(@services, 'Auth') if $ldap_config{'AuthDisableFilter'};
      push(@services, 'Info') if $ldap_config{'InfoDisableFilter'};
 
      # If Auth and Info use exactly the same params, only check one
      @services = ('Auth')
        if (@services && $self->LdapConfigAuthAndInfoAreSame);
 
      foreach my $service (@services) {
          my $ldap = $self->_GetBoundLdapObj($service);
          next unless $ldap;
 
          my $ldap_base    = $ldap_config{"${service}Base"};
          my $ldap_filter  = $ldap_config{"${service}Filter"};
          my $ldap_disable = $ldap_config{"${service}DisableFilter"};
 
          # Construct the complex filter
          my $filter = '(&' . $ldap_filter . $ldap_disable .
            '(uid=' . $self->Name . '))';
 
          my $disabled_users = $ldap->search(base  => $ldap_base,
                                            filter => $filter,
                                            attrs  => ['uid']);
 
          if ($disabled_users->count) {
              my $UserObj = RT::User->new($RT::SystemUser);
              $UserObj->Load($self->Name);
              my ($val, $message) = $UserObj->SetDisabled(1);
 
              $RT::Logger->info("DISABLED user " . $self->Name .
                                " per LDAP ($val, $message)\n");
              $msg = "User disabled";
          } else {
              # Update their info from LDAP
              my %args = (Name => $self->Name);
              $self->CanonicalizeUserInfo(\%args);
 
              foreach my $key (sort(keys(%args))) {
                  next unless $args{$key};
                  my $method = "Set$key";
                  $self->$method($args{$key});
              }
 
              $updated = 1;
              $RT::Logger->debug("UPDATED user " . $self->Name . " from LDAP\n");
              $msg = 'User updated';
          }
      }
      return ($updated, $msg);
  }
 
  1;
 
  </nowiki>

Revision as of 11:58, 29 November 2010

This code is now deprecated. Please see the new LDAP page instead.

This code is part of the LDAP integration overlay; you'll also need LdapSiteConfigSettings and, optionally, LdapAutocreateAuthCallback.

Put this in [=${RTHOME}/local/lib/RT/User_Local.pm]:

### User_Local.pm overlay for LDAP authentication and information
  ### v1.1b2  2005.08.03  purp@acm.org
  #
  # The latest version of this module may be found at:
  #   http://wiki.bestpractical.com/view/LdapUserLocalOverlay
  #
  # THIS MODULE REQUIRES SETTINGS IN YOUR RT_SiteConfig.pm;
  # you can find these at:
  #   http://wiki.bestpractical.com/view/LdapSiteConfigSettings
  #
  
  ### CREDITS
  # IsLDAPPassword() based on implementation of IsPassword() found at:
  #
  # http://www.justatheory.com/computers/programming/perl/rt/User_Local.pm.ldap
  #
  # Author's credits:
  # Modification Originally by Marcelo Bartsch <bartschm_cl@hotmail.com>
  # Update by Stewart James <stewart.james@vu.edu.au for rt3.
  # Update by David Wheeler <david@kineticode.com> for TLS and
  #    Group membership support.
  #
  #
  # CaonicalizeEmailAddress(), CanonicalizeUserInfo(), and LookupExternalInfo()
  # based on work by Phillip Cole (phillip d cole @ uk d coltgroup d com)
  # found at:
  #
  # http://wiki.bestpractical.com/view/AutoCreateAndCanonicalizeUserInfo
  #
  # His credits:
  #   based on CurrentUser_Local.pm and much help from the mailing lists
  #
  # All integrated, refactored, and updated by Jim Meyer (purp@acm.org)
  #
  # Changes:
  # v1.1b2 (2006.08.03) Modified by Phil Cole
  #  * Add $LdapEmailAttrMatchPrefix config variable to make alternate email
  #    addresses work on Windows 2003 AD.
  # v1.1b1 (2006.06.05) Punch-Drunk Hamster Release
  #  * Added UpdateFromLdap() to update the user's info from their LDAP entry;
  #    this also uses the newly invented $RT::*DisableFilter settings to know
  #    to disable their RT account when their LDAP account is disabled.
  #  * Added $RT::*DisableFilter settings for RT_SiteConfig.pm and refactored
  #    LdapConfigInfo() to include them. Also refactored logic to grep for
  #    filter keys rather than enumerate them
  #  * Added _GetBoundLdapObj() and refactored IsLdapPassword() and
  #    LookupExternalUserInfo() to use it
  #  * Added LdapConfigAuthAndInfoAreSame() to compare Auth and Info settings
  # v1.0b1 (2006.01.06)
  #  * Added $RT::AuthMethods as basis for auth method lists;
  #    currently supports LDAP, Internal
  #  * Implemented Phillip Cole's suggested $RT::LdapAttrMap
  #  * Implemented $RT::LdapRTAttrMatchList and $RT::LdapEmailAttrMatchList
  #    to help guide LDAP searches more effectively
  #  * Added LdapAuth* and LdapInfo* variables to allow authentication
  #    and information from separate LDAP servers; didn't invalidate
  #    older Ldap{Server,Base,User,Pass,etc} variables.
  #  * Added LdapConfigInfo() to get integrated config info
  
  no warnings qw(redefine);
  use strict;
  use Net::LDAP qw(LDAP_SUCCESS LDAP_PARTIAL_RESULTS);
  use Net::LDAP::Util qw(ldap_error_name);
  use Net::LDAP::Filter;
  
  # We only need Net::SSLeay if we're using TLS to encrypt our LDAP connections
  require Net::SSLeay
    if $RT::LdapTLS || $RT::LdapAuthTLS || $RT::LdapInfoTLS;
  
  =head2 LdapConfigInfo
  
  returns the LDAP attr mapped to EmailAddress in $RT::LdapAttrMap.
  
  If no result is found, returns the ADDRESS passed in.
  
  =cut
  
  sub LdapConfigInfo {
      my $self = shift;
  
      my %config;
  
      # Figure out what's what
      $config{'AuthServer'}     = $RT::LdapServer     || $RT::LdapAuthServer;
      $config{'AuthBase'}       = $RT::LdapBase       || $RT::LdapAuthBase;
      $config{'AuthUser'}       = $RT::LdapUser       || $RT::LdapAuthUser;
      $config{'AuthPass'}       = $RT::LdapPass       || $RT::LdapAuthPass;
      $config{'AuthFilter'}     = $RT::LdapFilter     || $RT::LdapAuthFilter;
      $config{'AuthGroup'}      = $RT::LdapGroup      || $RT::LdapAuthGroup;
      $config{'AuthTLS'}        = $RT::LdapTLS        || $RT::LdapAuthTLS;
      $config{'AuthSSLVersion'} = $RT::LdapSSLVersion || $RT::LdapAuthSSLVersion;
      $config{'AuthDisableFilter'} =
        $RT::LdapDisableFilter || $RT::LdapAuthDisableFilter;
  
      # We grandfather in LdapGroupAttribute; we'll default
      # to 'uniqueMember'
      $config{'AuthGroupAttr'} =
        $RT::LdapGroupAttribute || $RT::LdapGroupAttr ||
        $RT::LdapAuthGroupAttr || 'uniqueMember';
  
      # Figure out what's what
      $config{'InfoServer'}     = $RT::LdapServer     || $RT::LdapInfoServer;
      $config{'InfoBase'}       = $RT::LdapBase       || $RT::LdapInfoBase;
      $config{'InfoUser'}       = $RT::LdapUser       || $RT::LdapInfoUser;
      $config{'InfoPass'}       = $RT::LdapPass       || $RT::LdapInfoPass;
      $config{'InfoFilter'}     = $RT::LdapFilter     || $RT::LdapInfoFilter;
      $config{'InfoTLS'}        = $RT::LdapTLS        || $RT::LdapInfoTLS;
      $config{'InfoSSLVersion'} = $RT::LdapSSLVersion || $RT::LdapInfoSSLVersion;
      $config{'InfoDisableFilter'} =
        $RT::LdapDisableFilter || $RT::LdapInfoDisableFilter;
  
      # Filters need parens if they don't have 'em
      foreach my $filter (grep {/Filter$/} keys(%config)) {
          $config{$filter} = "($config{$filter})"
            unless $config{$filter} =~ /^\(.*\)$/;
      }
  
      return wantarray ? %config : \%config;
  }
  
  
  sub LdapConfigAuthAndInfoAreSame {
      my $self = shift;
  
      my %ldap_config = $self->LdapConfigInfo();
  
      # Quick lazy check: same number of keys for Auth and Info?
      return 0
        unless scalar(grep {/^Info/} keys(%ldap_config)) ==
          scalar(grep {/^Auth/} keys(%ldap_config));
  
      # Longer check
      foreach my $key (grep {/^Auth/} keys(%ldap_config)) {
          my ($key_base) = $key =~ /^Auth(.*)/;
          return 0
            unless $ldap_config{"Auth${key_base}"} eq
              $ldap_config{"Info${key_base}"};
      }
  
      return 1;
  }
  
  
  sub IsLDAPPassword {
      my $self = shift;
      my $value = shift;
  
      # Don't ask for external authentication unless enabled in RT_SiteConfig
      unless ($RT::LdapExternalAuth) {
          $RT::Logger->warning((caller(0))[3],
                               '$RT::LdapExternalAuth is not set');
          return;
      }
  
      $RT::Logger->debug("Trying LDAP authentication\n");
  
      # Figure out what's what
      my %ldap_config     = $self->LdapConfigInfo;
      my $ldap_base       = $ldap_config{'AuthBase'};
      my $ldap_filter     = $ldap_config{'AuthFilter'};
      my $ldap_group      = $ldap_config{'AuthGroup'};
      my $ldap_group_attr = $ldap_config{'AuthGroupAttr'};
  
      # Now let's get connected
      my $ldap = $self->_GetBoundLdapObj('Auth', version=>3);
      return unless ($ldap);
  
      my $filter_string = '(&(' . $RT::LdapAttrMap->{'Name'} . '=' .
        $self->Name . ')' . $ldap_filter . ')';
      my $filter = Net::LDAP::Filter->new($filter_string);
  
      my $ldap_msg = $ldap->search(base   => $ldap_base,
                           filter => $filter,
                           attrs  => ['dn']);
  
      unless ($ldap_msg->code == LDAP_SUCCESS ||
              $ldap_msg->code == LDAP_PARTIAL_RESULTS) {
          $RT::Logger->debug((caller(0))[3], "search for", $filter->as_string,
                            "failed:", ldap_error_name($ldap_msg->code), $ldap_msg->code);
          return;
      }
  
      unless ($ldap_msg->count == 1) {
          $RT::Logger->info((caller(0))[3], "AUTH FAILED:", $self->Name);
          return;
      }
  
      my $ldap_dn = $ldap_msg->first_entry->dn;
      $RT::Logger->debug((caller(0))[3], "Found LDAP DN:", $ldap_dn);
  
      $ldap_msg = $ldap->bind($ldap_dn, password => $value);
  
      unless ($ldap_msg->code == LDAP_SUCCESS) {
          $RT::Logger->info((caller(0))[3], "AUTH FAILED", $self->Name,
                            "(can't bind:", ldap_error_name($ldap_msg->code),
                            $ldap_msg->code, ")");
          return;
      }
  
      # Is there an LDAP Group to check?
      if ($ldap_group) {
          $filter = Net::LDAP::Filter->new("(${ldap_group_attr}=${ldap_dn})");
  
          $ldap_msg = $ldap->search(base   => $ldap_group,
                               filter => $filter,
                               attrs  => ['dn'],
                               scope  => 'base');
  
          unless ($ldap_msg->code == LDAP_SUCCESS ||
                  $ldap_msg->code == LDAP_PARTIAL_RESULTS) {
              $RT::Logger->critical((caller(0))[3],
                                    "Search for", $filter->as_string, "failed:",
                                    ldap_error_name($ldap_msg->code), $ldap_msg->code);
              return;
          }
  
          unless ($ldap_msg->count == 1) {
              $RT::Logger->info((caller(0))[3], "AUTH FAILED:", $self->Name);
              return;
          }
      }
  
      # If we've survived to this point, we're good.
      $RT::Logger->info((caller(0))[3], "AUTH OK:", $self->Name, "($ldap_dn)");
  
      return 1;
  }
  
  sub IsInternalPassword {
      my $self = shift;
      my $value = shift;
  
      unless ($self->HasPassword) {
          $RT::Logger->info((caller(0))[3],
                            "AUTH FAILED (no passwd):", $self->Name);
          return undef;
      }
  
      # generate an md5 password
      if ($self->_GeneratePassword($value) eq $self->__Value('Password')) {
          $RT::Logger->info((caller(0))[3],
                            "AUTH OKAY:", $self->Name);
          return 1;
      }
  
      #  if it's a historical password we say ok.
      if ($self->__Value('Password') eq crypt($value, $self->__Value('Password'))
          or $self->_GeneratePasswordBase64($value) eq $self->__Value('Password'))
        {
            # ...but upgrade the legacy password inplace.
            $self->SUPER::SetPassword( $self->_GeneratePassword($value) );
            $RT::Logger->info((caller(0))[3],
                              "AUTH OKAY:", $self->Name);
            return 1;
        }
  
      $RT::Logger->info((caller(0))[3], "AUTH FAILED:", $self->Name);
  
      return undef;
  }
  
  # {{{ sub IsPassword
  
  sub IsPassword {
      my $self  = shift;
      my $value = shift;
  
      #TODO there isn't any apparent way to legitimately ACL this
  
      # RT does not allow null passwords
      if ( !defined($value) || $value eq '' ) {
          return undef;
      }
  
      if ( $self->PrincipalObj->Disabled ) {
          $RT::Logger->info("Disabled user " . $self->Name .
                            " tried to log in" );
          return undef;
      }
  
      my @auth_methods = $RT::AuthMethods ? @{$RT::AuthMethods} : ('Internal');
      my $success;
  
      foreach my $method (@auth_methods) {
          $method = "Is${method}Password";
  
          # Eval this since they might specify an auth method without
          # an "Is<auth>Password" method implemented
          eval {
              $success = $self->$method($value);
          };
  
          $RT::Logger->debug((caller(0))[3], "auth method $method",
                             ($success ? 'SUCCEEDED' : 'FAILED'));
          last if $success;
      }
  
      # We either got it or we didn't
      return $success;
  }
  
  # }}}
  
  =head2 CanonicalizeEmailAddress ADDRESS
  
  returns the LDAP attr mapped to EmailAddress in $RT::LdapAttrMap.
  
  If no result is found, returns the ADDRESS passed in.
  
  =cut
  
  sub CanonicalizeEmailAddress {
      my $self = shift;
      my $email = shift;
  
      my $found = undef;
      my %params = ('EmailAddress' => $email);
  
      $self = RT::User->new($RT::SystemUser) unless $self;
  
      # Don't ask for external info unless enabled in RT_SiteConfig
      unless ($RT::LdapExternalInfo) {
          $RT::Logger->warning((caller(0))[3],
                               '$RT::LdapExternalInfo is not set');
          return $email;
      }
  
      $RT::Logger->debug((caller(0))[3], ": called with \"$email\" by", caller);
  
      if ($email) {
         foreach my $prefix (@{$RT::LdapEmailAttrMatchPrefix}) {
             if (!$found) {
                 foreach my $attr (@{$RT::LdapEmailAttrMatchList}) {
                     ($found, %params) =
                       $self->LookupExternalUserInfo($attr, "$prefix$email");
                     if ($found) {
                         $RT::Logger->debug("FOUND OK");
                     }
                     last if $found;
  
                 }
             }
         }
      }
  
      my $new_email = $found ? $params{'EmailAddress'} : $email;
  
      $RT::Logger->info((caller(0))[3], "$email =>  $new_email");
  
      return $new_email;
  }
  
  # {{{ sub CanonicalizeUserInfo
  
  =head2 CanonicalizeUserInfo HASHREF
  
  Get all LDAP attrs listed in $RT::LdapAttrMap and put them into
  the hash referred to by HASHREF.
  
  returns true (1) if LDAP lookup was successful, false (undef)
  in all other cases.
  
  =cut
  
  sub CanonicalizeUserInfo {
      my $self = shift;
      my $args = shift;
  
      my $found = 0;
      my %params;
  
      # Don't ask for external info unless enabled in RT_SiteConfig
      unless ($RT::LdapExternalInfo) {
          $RT::Logger->warning((caller(0))[3],
                               '$RT::LdapExternalInfo is not set');
          # We return true so that they'll go forward with what info they have
          return 1;
      }
  
      $RT::Logger->debug((caller(0))[3], " called by", caller, "with:",
                         join(", ", map {sprintf("%s: %s", $_, $args->{$_})}
                              sort(keys(%$args))));
  
      # $args is a hash ref; to get at its values, use $args->{<key>}
      #
      # $args has keys:
      #    RealName     - User human name (e.g. Cole, Phillip)
      #    Name         - Username or login (e.g. ukpgc)
      #    EmailAddress - Email address (e.g. phillip.cole@company.com)
      #    Comments     - Comments created during creation
  
      # How may I know thee? Let me count the ways ...
      foreach my $rt_attr (@{$RT::LdapRTAttrMatchList}) {
          next unless $args->{$rt_attr};
  
          ($found, %params) =
            $self->LookupExternalUserInfo($RT::LdapAttrMap->{$rt_attr},
                                             $args->{$rt_attr});
          last if $found;
      }
  
      if ($found) {
          # It's important that we always have a canonical email address
          if ($params{'EmailAddress'}) {
              my $email =
                $self->CanonicalizeEmailAddress($params{'EmailAddress'});
              $params{'EmailAddress'} = $email if $email;
          }
  
          %$args = (%$args, %params);
      }
  
      $RT::Logger->info((caller(0))[3], "returning",
                         join(", ", map {sprintf("%s: %s", $_, $args->{$_})}
                              sort(keys(%$args))));
      ### HACK: The config var below is to overcome the (IMO) bug in
      ### RT::User::Create() which expects this function to always
      ### return true or rejects the user for creation. This should be
      ### a different config var (CreateUncanonicalizedUsers) and
      ### should be honored in RT::User::Create()
      return $found || $RT::LdapAutoCreateNonLdapUsers;
  }
  # }}}
  
  sub _GetBoundLdapObj {
      my $self = shift;
      my ($service, @ldap_args) = @_;
  
      # Figure out what's what
      my %ldap_config     = $self->LdapConfigInfo();
      my $ldap_server     = $ldap_config{"${service}Server"};
      my $ldap_base       = $ldap_config{"${service}Base"};
      my $ldap_user       = $ldap_config{"${service}User"};
      my $ldap_pass       = $ldap_config{"${service}Pass"};
      my $ldap_filter     = $ldap_config{"${service}Filter"};
      my $ldap_tls        = $ldap_config{"${service}TLS"};
      my $ldap_ssl_ver    = $ldap_config{"${service}SSLVersion"};
  
      my $ldap = new Net::LDAP($ldap_server, @ldap_args);
      unless ($ldap) {
          $RT::Logger->critical((caller(0))[3],
                                ": Cannot connect to $ldap_server");
          return undef;
      }
  
      if ($ldap_tls) {
          $Net::SSLeay::ssl_version = $ldap_ssl_ver;
          # Thanks to David Narayan for the fault tolerance bits
          eval { $ldap->start_tls; };
          if ($@) {
              $RT::Logger->critical((caller(0))[3], "Can't start TLS: $@");
              return;
          }
  
      }
  
      my $msg = undef;
  
      if ($ldap_user) {
          $msg = $ldap->bind($ldap_user, password => $ldap_pass);
      } else {
          $msg = $ldap->bind;
      }
  
      unless ($msg->code == LDAP_SUCCESS) {
          $RT::Logger->critical((caller(0))[3], "Can't bind:",
                               ldap_error_name($msg->code), $msg->code);
          return undef;
      } else {
          return $ldap;
      }
  }
  
  
  # {{{ sub LookupExternalUserInfo
  
  
  =head2 LookupExternalUserInfo KEY VALUE [BASE_DN]
  
  LookupExternalUserInfo takes a key/value pair with optional LDAP baseDN,
  looks it up in LDAP, and returns a params hash containing all LDAP attrs
  listed in $RT::LdapAttrMap, suitable for creating an RT::User object.
  
  Returns a tuple, ($found, %params)
  
  =cut
  
  sub LookupExternalUserInfo {
      my $self = shift;
      my ($key, $value, $baseDN) = @_;
  
      my $found = 0;
      my %params = (Name         => undef,
                    EmailAddress => undef,
                    RealName     => undef);
  
  
      # Don't ask for external info unless enabled in RT_SiteConfig
      unless ($RT::LdapExternalInfo) {
          $RT::Logger->warning((caller(0))[3],
                               '$RT::LdapExternalInfo is not set');
          return ($found, %params);
      }
  
      # Figure out what's what
      my %ldap_config     = $self->LdapConfigInfo();
      my $ldap_base       = $ldap_config{'InfoBase'};
      my $ldap_filter     = $ldap_config{'InfoFilter'};
  
      $baseDN = $baseDN || $ldap_base;
  
      ### This should use Net::LDAP::Filter, too
      my $filter = ($key && $value) ? "@{[ $key ]}=$value" : "";
  
      $RT::Logger->debug((caller(0))[3], "called with baseDN \"$baseDN\"",
                         "and filter \"$filter\" by", caller);
  
      unless ($baseDN) {
          $RT::Logger->critical((caller(0))[3] . " No baseDN given");
          return ($found, %params);
      }
  
      my $ldap = $self->_GetBoundLdapObj('Info');
  
      return ($found, %params) unless ($ldap);
  
      # Get the list of unique attrs we need
      my %ldap_attrs = map {$_ => 1} values(%{$RT::LdapAttrMap});
      my @attrs = keys(%ldap_attrs);
      my $ldap_msg = $ldap->search(base   => $baseDN,
                                   filter => $filter,
                                   attrs  => \@attrs);
  
      if ($ldap_msg->code != LDAP_SUCCESS and
          $ldap_msg->code != LDAP_PARTIAL_RESULTS) {
          $RT::Logger->critical((caller(0))[3],
                                ": Search for $filter failed: ",
                                ldap_error_name($ldap_msg->code), $ldap_msg->code);
  
          # Why on earth do we return the same RealName, just quoted?!
          $params{'RealName'} = "\"$params{'RealName'}\"";
      } else {
          # If there's only one match, we're good; more than one and
          # we don't know which is the right one so we skip it.
          if ($ldap_msg->count == 1) {
              my $entry = $ldap_msg->first_entry();
              foreach my $key (keys(%{$RT::LdapAttrMap})) {
                  if ($RT::LdapAttrMap->{$key} eq 'dn') {
                      $params{$key} = $entry->dn();
                  } else {
                      $params{$key} =
                        ($entry->get_value($RT::LdapAttrMap->{$key}))[0];
                  }
              }
              $found = 1;
          }
      }
      $ldap_msg = $ldap->unbind();
      if ($ldap_msg->code != LDAP_SUCCESS) {
          $RT::Logger->critical((caller(0))[3],
                                ": Could not unbind: ",
                                ldap_error_name($ldap_msg->code), $ldap_msg->code);
      }
  
      undef $ldap;
      undef $ldap_msg;
  
      $RT::Logger->info((caller(0))[3],
                         ": $baseDN $filter => ",
                         join(", ", map {sprintf("%s: %s", $_, $params{$_})}
                              sort(keys(%params))));
  
      return ($found, %params);
  }
  
  # }}}
  
  sub UpdateFromLdap {
      my $self = shift;
      my $updated = 0;
      my $msg = "User NOT updated";
  
      my %ldap_config = $self->LdapConfigInfo;
  
      my @services;
      push(@services, 'Auth') if $ldap_config{'AuthDisableFilter'};
      push(@services, 'Info') if $ldap_config{'InfoDisableFilter'};
  
      # If Auth and Info use exactly the same params, only check one
      @services = ('Auth')
        if (@services && $self->LdapConfigAuthAndInfoAreSame);
  
      foreach my $service (@services) {
          my $ldap = $self->_GetBoundLdapObj($service);
          next unless $ldap;
  
          my $ldap_base    = $ldap_config{"${service}Base"};
          my $ldap_filter  = $ldap_config{"${service}Filter"};
          my $ldap_disable = $ldap_config{"${service}DisableFilter"};
  
          # Construct the complex filter
          my $filter = '(&' . $ldap_filter . $ldap_disable .
            '(uid=' . $self->Name . '))';
  
          my $disabled_users = $ldap->search(base   => $ldap_base,
                                             filter => $filter,
                                             attrs  => ['uid']);
  
          if ($disabled_users->count) {
              my $UserObj = RT::User->new($RT::SystemUser);
              $UserObj->Load($self->Name);
              my ($val, $message) = $UserObj->SetDisabled(1);
  
              $RT::Logger->info("DISABLED user " . $self->Name .
                                " per LDAP ($val, $message)\n");
              $msg = "User disabled";
          } else {
              # Update their info from LDAP
              my %args = (Name => $self->Name);
              $self->CanonicalizeUserInfo(\%args);
  
              foreach my $key (sort(keys(%args))) {
                  next unless $args{$key};
                  my $method = "Set$key";
                  $self->$method($args{$key});
              }
  
              $updated = 1;
              $RT::Logger->debug("UPDATED user " . $self->Name . " from LDAP\n");
              $msg = 'User updated';
          }
      }
      return ($updated, $msg);
  }
  
  1;