LdapUserLocalOverlay

From Request Tracker Wiki
Jump to navigation Jump to search

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;