Difference between revisions of "LdapUserLocalOverlay"

From Request Tracker Wiki
Jump to navigation Jump to search
(Undo revision 5652 by 195.16.40.50 (talk))
m (4 revisions imported)
 
(No difference)

Latest revision as of 16:14, 6 April 2016

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;