Difference between revisions of "NtlmAuthentication"

From Request Tracker Wiki
Jump to navigation Jump to search
 
Line 110: Line 110:
  Set($WebExternalAuth , '1');
  Set($WebExternalAuth , '1');
  Set($WebFallbackToInternalAuth , '1');
  Set($WebFallbackToInternalAuth , '1');
  Set($WebExternalGecos , undef)
  Set($WebExternalGecos , undef);
  Set($WebExternalAuto , '1');
  Set($WebExternalAuto , '1');


Line 116: Line 116:


  <nowiki>&lt;VirtualHost *:80&gt;
  <nowiki>&lt;VirtualHost *:80&gt;
      ServerName rt.mydomain.com
        ServerName rt.mydomain.com
      AddDefaultCharset UTF-8
        AddDefaultCharset UTF-8
      DocumentRoot /opt/rt3/share/html
        DocumentRoot /opt/rt3/share/html
 
   
      &lt;Directory "/opt/rt3/share/html"&gt;
        &lt;Directory "/opt/rt3/share/html"&gt;
          AuthName "Request Tracker"
            AuthName "Request Tracker"
          AuthType NTLM
            AuthType NTLM
          NTLMAuth on
            NTLMAuth on
          NTLMAuthoritative on
            NTLMAuthoritative on
          NTLMDomain lhl.co.nz
            NTLMDomain lhl.co.nz
          NTLMServer dc1.mydomain.com
            NTLMServer dc1.mydomain.com
          NTLMBackup dc2.mydomain.com
            NTLMBackup dc2.mydomain.com
          require valid-user
            require valid-user
      &lt;/Directory&gt;
        &lt;/Directory&gt;
 
   
  #    RedirectMatch permanent (.*)/$ http://rt.mydomain.com/$1/index.html
    #    RedirectMatch permanent (.*)/$ http://rt.mydomain.com/$1/index.html
 
   
      # this line applies to Apache2+mod_perl2 only
        # this line applies to Apache2+mod_perl2 only
      # Below line might be incorrect, I had to use:
        # Below line might be incorrect, I had to use:
      #    PerlModule Apache2::compat
        #    PerlModule Apache2::compat
      # mod_perl 2.0.1 from FC4 Linux
        # mod_perl 2.0.1 from FC4 Linux
      #PerlModule Apache2 Apache::compat
        #PerlModule Apache2 Apache::compat
      PerlModule Apache2::compat
        PerlModule Apache2::compat
 
   
      PerlModule Apache::DBI
        PerlModule Apache::DBI
      PerlRequire /opt/rt3/bin/webmux.pl
        PerlRequire /opt/rt3/bin/webmux.pl
 
   
      &lt;Location /&gt;
        &lt;Location /&gt;
              SetHandler perl-script
              SetHandler perl-script
              PerlHandler RT::Mason
              PerlHandler RT::Mason
      &lt;/Location&gt;
        &lt;/Location&gt;
 
   
          ErrorLog logs/rt.lhl.co.nz-error_log
            ErrorLog logs/rt.lhl.co.nz-error_log
          CustomLog logs/rt.lhl.co.nz-access_log common
            CustomLog logs/rt.lhl.co.nz-access_log common
 
   
  &lt;/VirtualHost&gt;
    &lt;/VirtualHost&gt;
 
   
 
   
  </nowiki>
    </nowiki>


Create /opt/rt3/lib/[[User Local|User_Local]].pm as:
Create /opt/rt3/lib/[[User Local|User_Local]].pm as:


  <nowiki># BEGIN LICENSE BLOCK
  <nowiki># BEGIN LICENSE BLOCK
  #
    #
  # Copyright (c) 2004 Petter Reinholdtsen &lt;pere@hungry.com&gt;
    # Copyright (c) 2004 Petter Reinholdtsen &lt;pere@hungry.com&gt;
  #
    #
  # (Except where explictly superceded by other copyright notices)
    # (Except where explictly superceded by other copyright notices)
  #
    #
  # This work is made available to you under the terms of Version 2 of
    # This work is made available to you under the terms of Version 2 of
  # the GNU General Public License. A copy of that license should have
    # the GNU General Public License. A copy of that license should have
  # been provided with this software, but in any event can be snarfed
    # been provided with this software, but in any event can be snarfed
  # from www.gnu.org.
    # from www.gnu.org.
  #
    #
  # This work is distributed in the hope that it will be useful, but
    # This work is distributed in the hope that it will be useful, but
  # WITHOUT ANY WARRANTY; without even the implied warranty of
    # WITHOUT ANY WARRANTY; without even the implied warranty of
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  # General Public License for more details.
    # General Public License for more details.
  #
    #
  # Unless otherwise specified, all modifications, corrections or
    # Unless otherwise specified, all modifications, corrections or
  # extensions to this work which alter its source code become the
    # extensions to this work which alter its source code become the
  # property of Best Practical Solutions, LLC when submitted for
    # property of Best Practical Solutions, LLC when submitted for
  # inclusion in the work.
    # inclusion in the work.
  #
    #
  #
    #
  # END LICENSE BLOCK
    # END LICENSE BLOCK
 
   
 
   
  # LDAP integration in RT 3.  These overrides provide LDAP
    # LDAP integration in RT 3.  These overrides provide LDAP
  # authentication and user info syncronizing.
    # authentication and user info syncronizing.
  #
    #
  # Written by Petter Reinholdtsen &lt;pere@hungry.com&gt; based on Code from
    # Written by Petter Reinholdtsen &lt;pere@hungry.com&gt; based on Code from
  # Marcelo Bartsch &lt;bartschm_cl@hotmail.com&gt;, Stewart James
    # Marcelo Bartsch &lt;bartschm_cl@hotmail.com&gt;, Stewart James
  # &lt;stewart.james@vu.edu.au&gt; and Carl Makin &lt;carl@xena.IPAustralia.gov.au&gt;.
    # &lt;stewart.james@vu.edu.au&gt; and Carl Makin &lt;carl@xena.IPAustralia.gov.au&gt;.
  #
    #
  # Copy this file into rt3/local/lib/RT/User_Local.pm to active it.
    # Copy this file into rt3/local/lib/RT/User_Local.pm to active it.
 
   
 
   
  # Modification Originally by Marcelo Bartsch &lt;bartschm_cl@hotmail.com&gt;
    # Modification Originally by Marcelo Bartsch &lt;bartschm_cl@hotmail.com&gt;
  # Update by Stewart James &lt;stewart.james@vu.edu.au for rt3.
    # Update by Stewart James &lt;stewart.james@vu.edu.au for rt3.
  # Update with TLS support and more flexible LDAP code by Petter Reinholdtsen.
    # Update with TLS support and more flexible LDAP code by Petter Reinholdtsen.
  # Drop this file in /opt/rt3/lib/RT/User_Local.pm
    # Drop this file in /opt/rt3/lib/RT/User_Local.pm
  # Drop something like below in yout RT_SiteConfig.pm
    # Drop something like below in yout RT_SiteConfig.pm
  #
    #
  # Set($LDAPExternalAuth, 1); # Enable LDAP auth
    # Set($LDAPExternalAuth, 1); # Enable LDAP auth
  # Set($LdapServer, "ldap.domain.com");
    # Set($LdapServer, "ldap.domain.com");
  # Set($LdapCAFile, "/site/w3-sertifikater/w3_cacert.pem");
    # Set($LdapCAFile, "/site/w3-sertifikater/w3_cacert.pem");
  # Set($LdapAuthStartTLS, 1); # Need to use TLS or ldaps to check passwords
    # Set($LdapAuthStartTLS, 1); # Need to use TLS or ldaps to check passwords
  # Set($LdapUser, ""); # Can search without username and password
    # Set($LdapUser, ""); # Can search without username and password
  # Set($LdapAuthPass, "");
    # Set($LdapAuthPass, "");
  # Set($LdapAuthBase, "ou=users,dc=domain,dc=com");
    # Set($LdapAuthBase, "ou=users,dc=domain,dc=com");
  # Set($LdapAuthUidAttr, "uid");
    # Set($LdapAuthUidAttr, "uid");
  # Set($LdapAuthFilter, "(objectclass=posixAccount)");
    # Set($LdapAuthFilter, "(objectclass=posixAccount)");
 
   
 
   
  no warnings qw(redefine);
    no warnings qw(redefine);
 
   
  # {{{ sub LookupExternalUserInfo
    # {{{ sub LookupExternalUserInfo
 
   
  =item LookupExternalUserInfo
    =item LookupExternalUserInfo
 
   
    LookupExternalUserInfo is a site-definable method for synchronizing
    LookupExternalUserInfo is a site-definable method for synchronizing
    incoming users with an external data source.
    incoming users with an external data source.
 
   
    This routine takes a tuple of EmailAddress and FriendlyName
    This routine takes a tuple of EmailAddress and FriendlyName
      EmailAddress is the user's email address, ususally taken from
      EmailAddress is the user's email address, ususally taken from
          an email message's From: header.
          an email message's From: header.
      RealName is a freeform string, ususally taken from the "comment"
      RealName is a freeform string, ususally taken from the "comment"
          portion of an email message's From: header.
          portion of an email message's From: header.
 
   
    It returns (FoundInExternalDatabase, ParamHash);
    It returns (FoundInExternalDatabase, ParamHash);
 
   
      FoundInExternalDatabase must be set to 1 before return if the user
      FoundInExternalDatabase must be set to 1 before return if the user
      was found in the external database.
      was found in the external database.
 
   
      ParamHash is a Perl parameter hash which can contain at least the
      ParamHash is a Perl parameter hash which can contain at least the
      following fields. These fields are used to populate RT's users
      following fields. These fields are used to populate RT's users
      database when the user is created
      database when the user is created
 
   
        EmailAddress is the email address that RT should use for this user.
        EmailAddress is the email address that RT should use for this user.
        Name is the 'Name' attribute RT should use for this user.
        Name is the 'Name' attribute RT should use for this user.
            'Name' is used for things like access control and user lookups.
            'Name' is used for things like access control and user lookups.
        RealName is what RT should display as the user's name when displaying
        RealName is what RT should display as the user's name when displaying
            'friendly' names
            'friendly' names
 
   
  =cut
    =cut
 
   
  sub LookupExternalUserInfo {
    sub LookupExternalUserInfo {
    my %UserInfo = ();
      my %UserInfo = ();
    $UserInfo{'EmailAddress'} = shift;
      $UserInfo{'EmailAddress'} = shift;
    $UserInfo{'RealName'} = shift;
      $UserInfo{'RealName'} = shift;
    $UserInfo{'RealName'} =~ s/\"//g;
      $UserInfo{'RealName'} =~ s/\"//g;
 
   
    my $FoundInExternalDatabase = 0;
      my $FoundInExternalDatabase = 0;
 
   
    # Name is the RT username you want to use for this user.
      # Name is the RT username you want to use for this user.
    my %LdapUserInfo = LdapUserFindByMailaddr($UserInfo{'EmailAddress'});
      my %LdapUserInfo = LdapUserFindByMailaddr($UserInfo{'EmailAddress'});
    if ($LdapUserInfo{'Name'}) {
      if ($LdapUserInfo{'Name'}) {
        $FoundInExternalDatabase = 1;
          $FoundInExternalDatabase = 1;
        $RT::Logger-&gt;info("LookupExternalUserInfo: Mapping '".
          $RT::Logger-&gt;info("LookupExternalUserInfo: Mapping '".
                          $UserInfo{'EmailAddress'} .
                            $UserInfo{'EmailAddress'} .
                          "' to '" .
                            "' to '" .
                          $LdapUserInfo{'Name'} . "'");
                            $LdapUserInfo{'Name'} . "'");
        foreach my $key (keys %LdapUserInfo) {
          foreach my $key (keys %LdapUserInfo) {
            $UserInfo{$key} = $LdapUserInfo{$key};
              $UserInfo{$key} = $LdapUserInfo{$key};
        }
          }
    } else {
      } else {
        $RT::Logger-&gt;info("LookupExternalUserInfo: Fail to find username for '".
          $RT::Logger-&gt;info("LookupExternalUserInfo: Fail to find username for '".
                          $UserInfo{'EmailAddress'}."'");
                            $UserInfo{'EmailAddress'}."'");
    }
      }
 
   
    return ($FoundInExternalDatabase, %UserInfo);
      return ($FoundInExternalDatabase, %UserInfo);
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub CanonicalizeUserInfo
    # {{{ sub CanonicalizeUserInfo
 
   
  sub CanonicalizeUserInfo {
    sub CanonicalizeUserInfo {
      my $self    = shift;
        my $self    = shift;
      my $argsref = shift;
        my $argsref = shift;
      my $success = 1;
        my $success = 1;
 
   
      my ($UserFoundInExternalDatabase, %ExternalUserInfo) =
        my ($UserFoundInExternalDatabase, %ExternalUserInfo) =
          LookupExternalUserInfo( $argsref-&gt;{'EmailAddress'},
            LookupExternalUserInfo( $argsref-&gt;{'EmailAddress'},
                                  $argsref-&gt;{'RealName'} );
                                    $argsref-&gt;{'RealName'} );
      if ($UserFoundInExternalDatabase) {
        if ($UserFoundInExternalDatabase) {
          for my $key (keys %ExternalUserInfo) {
            for my $key (keys %ExternalUserInfo) {
              $argsref-&gt;{$key} = $ExternalUserInfo{$key};
                $argsref-&gt;{$key} = $ExternalUserInfo{$key};
          }
            }
      }
        }
 
   
      return ($success);
        return ($success);
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub SetPasswordExternal
    # {{{ sub SetPasswordExternal
 
   
  =head2 SetPasswordExternal
    =head2 SetPasswordExternal
 
   
  Takes a string, and try to set this string as the users password in an
    Takes a string, and try to set this string as the users password in an
  external system, if the user is listed in the external system.
    external system, if the user is listed in the external system.
 
   
  Returns 1 if the password was set successfully, undef if it failed,
    Returns 1 if the password was set successfully, undef if it failed,
  and -1 if the user is unknown to the external system.
    and -1 if the user is unknown to the external system.
 
   
  This hook is called from SetPassword.
    This hook is called from SetPassword.
 
   
  =cut
    =cut
 
   
  sub SetPasswordExternal {
    sub SetPasswordExternal {
      my $self    = shift;
        my $self    = shift;
      my $password = shift;
        my $password = shift;
 
   
      # Not allowed to set password for users in LDAP
        # Not allowed to set password for users in LDAP
      if ($RT::LDAPExternalAuth) {
        if ($RT::LDAPExternalAuth) {
          my $ldap = LdapConnect();
            my $ldap = LdapConnect();
          my $mesg;
            my $mesg;
          if ( $mesg = LdapFindUser( $ldap, $self-&gt;Name )
            if ( $mesg = LdapFindUser( $ldap, $self-&gt;Name )
                &amp;&amp; defined $mesg &amp;&amp; $mesg-&gt;count ) {
                &amp;&amp; defined $mesg &amp;&amp; $mesg-&gt;count ) {
              LdapDisconnect($ldap);
                LdapDisconnect($ldap);
              return ( undef,
                return ( undef,
                        $self-&gt;loc("LDAP users must change password in LDAP") );
                        $self-&gt;loc("LDAP users must change password in LDAP") );
          }
            }
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
      }
        }
      return (-1, "No such user in LDAP");
        return (-1, "No such user in LDAP");
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub SetPassword
    # {{{ sub SetPassword
 
   
  =head2 SetPassword
    =head2 SetPassword
 
   
  Takes a string. Checks the string's length and sets this user's password
    Takes a string. Checks the string's length and sets this user's password
  to that string.
    to that string.
 
   
  Override for function in User_Overlay.pm, with modification for LDAP
    Override for function in User_Overlay.pm, with modification for LDAP
  authentication.
    authentication.
 
   
  =cut
    =cut
 
   
  sub SetPassword {
    sub SetPassword {
      my $self    = shift;
        my $self    = shift;
      my $password = shift;
        my $password = shift;
 
   
      unless ( $self-&gt;CurrentUserCanModify('Password') ) {
        unless ( $self-&gt;CurrentUserCanModify('Password') ) {
          return ( 0, $self-&gt;loc('Permission Denied') );
            return ( 0, $self-&gt;loc('Permission Denied') );
      }
        }
 
   
      my ($code, $msg) = $self-&gt;SetPasswordExternal($password);
        my ($code, $msg) = $self-&gt;SetPasswordExternal($password);
      return ($code, $msg) unless (-1 == $code);
        return ($code, $msg) unless (-1 == $code);
 
   
      if ( !$password ) {
        if ( !$password ) {
          return ( 0, $self-&gt;loc("No password set") );
            return ( 0, $self-&gt;loc("No password set") );
      }
        }
      elsif ( length($password) &lt; $RT::MinimumPasswordLength ) {
        elsif ( length($password) &lt; $RT::MinimumPasswordLength ) {
          return ( 0, $self-&gt;loc("Password too short") );
            return ( 0, $self-&gt;loc("Password too short") );
      }
        }
      else {
        else {
          $password = $self-&gt;_GeneratePassword($password);
            $password = $self-&gt;_GeneratePassword($password);
          return ( $self-&gt;SUPER::SetPassword( $password));
            return ( $self-&gt;SUPER::SetPassword( $password));
      }
        }
 
   
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub IsPasswordExternal
    # {{{ sub IsPasswordExternal
 
   
  =head2 IsPasswordExternal
    =head2 IsPasswordExternal
 
   
  Returns true if the passed in value is this user's password.  Return
    Returns true if the passed in value is this user's password.  Return
  undef if the password don't match.  Return -1 if the user is unknown
    undef if the password don't match.  Return -1 if the user is unknown
  in the external system.
    in the external system.
 
   
  This hook is called from IsPassword.
    This hook is called from IsPassword.
 
   
  =cut
    =cut
 
   
  sub IsPasswordExternal {
    sub IsPasswordExternal {
      my $self  = shift;
        my $self  = shift;
      my $value = shift;
        my $value = shift;
          # Let LDAP be authorative for users in LDAP, and only fall
            # Let LDAP be authorative for users in LDAP, and only fall
          # through for users without LDAP entry.
            # through for users without LDAP entry.
          if ($RT::LDAPExternalAuth) {
            if ($RT::LDAPExternalAuth) {
              return IsLdapPassword($self-&gt;Name, $value);
                return IsLdapPassword($self-&gt;Name, $value);
          }
            }
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub IsPassword
    # {{{ sub IsPassword
 
   
  =head2 IsPassword
    =head2 IsPassword
 
   
  Check the users password using LDAP.  Override for function in
    Check the users password using LDAP.  Override for function in
  User_Overlay.pm, with modification for LDAP authentication.
    User_Overlay.pm, with modification for LDAP authentication.
 
   
  =cut
    =cut
 
   
  sub IsPassword {
    sub IsPassword {
          my $self  = shift;
            my $self  = shift;
          my $value = shift;
            my $value = shift;
 
   
          #TODO there isn't any apparent way to legitimately ACL this
            #TODO there isn't any apparent way to legitimately ACL this
 
   
          # RT does not allow null passwords
            # RT does not allow null passwords
          if ( ( !defined($value) ) or ( $value eq '' ) ) {
            if ( ( !defined($value) ) or ( $value eq '' ) ) {
                  return (undef);
                    return (undef);
          }
            }
 
   
          if ( $self-&gt;PrincipalObj-&gt;Disabled ) {
            if ( $self-&gt;PrincipalObj-&gt;Disabled ) {
                  $RT::Logger-&gt;info(
                    $RT::Logger-&gt;info(
                          "Disabled user " . $self-&gt;Name . " tried to log in" );
                            "Disabled user " . $self-&gt;Name . " tried to log in" );
                  return (undef);
                    return (undef);
          }
            }
 
   
          if ( ($self-&gt;__Value('Password') eq '') ||
            if ( ($self-&gt;__Value('Password') eq '') ||
                  ($self-&gt;__Value('Password') eq undef) )  {
                    ($self-&gt;__Value('Password') eq undef) )  {
                  return(undef);
                    return(undef);
          }
            }
 
   
          my $code = $self-&gt;IsPasswordExternal($value);
            my $code = $self-&gt;IsPasswordExternal($value);
          return ($code) unless (-1 == $code);
            return ($code) unless (-1 == $code);
 
   
          # is it an MD5 password
            # is it an MD5 password
          if ($self-&gt;__Value('Password') eq $self-&gt;_GeneratePassword($value)) {
            if ($self-&gt;__Value('Password') eq $self-&gt;_GeneratePassword($value)) {
                  return(1);
                    return(1);
          }
            }
 
   
          # if it's recognized by crypt, we say ok too.
            # if it's recognized by crypt, we say ok too.
          if ($self-&gt;__Value('Password') eq crypt($value,
            if ($self-&gt;__Value('Password') eq crypt($value,
                                                  $self-&gt;__Value('Password'))) {
                                                    $self-&gt;__Value('Password'))) {
              return (1);
                return (1);
          }
            }
 
   
          # no password check has succeeded. get out
            # no password check has succeeded. get out
          return (undef);
            return (undef);
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub LdapUserFindByMailaddr
    # {{{ sub LdapUserFindByMailaddr
 
   
  =head2 LdapUserFindByMailaddr
    =head2 LdapUserFindByMailaddr
 
   
  Lookup user owning a given email address on UiO, returning the
    Lookup user owning a given email address on UiO, returning the
  username or undef if not known or the search failed.
    username or undef if not known or the search failed.
 
   
  The following configure options are used by this function in addition
    The following configure options are used by this function in addition
  to the ones used by LdapConnect().
    to the ones used by LdapConnect().
 
   
    $RT::LdapMailBase
    $RT::LdapMailBase
    $RT::LdapMailFilter
    $RT::LdapMailFilter
    $RT::LdapMailScope
    $RT::LdapMailScope
    $RT::LdapMailSearchAttr
    $RT::LdapMailSearchAttr
    $RT::LdapMailMap
    $RT::LdapMailMap
 
   
  =cut
    =cut
 
   
  # Example search
    # Example search
  #  ldapsearch -x -b ou=mail,dc=uio,dc=no -ZZ -h ldap.uio.no -D uid=pre,ou=users,dc=uio,dc=no -W target=mathiasm
    #  ldapsearch -x -b ou=mail,dc=uio,dc=no -ZZ -h ldap.uio.no -D uid=pre,ou=users,dc=uio,dc=no -W target=mathiasm
 
   
  sub LdapUserFindByMailaddr {
    sub LdapUserFindByMailaddr {
      my $mailaddr = shift;
        my $mailaddr = shift;
      my %UserInfo = ();
        my %UserInfo = ();
      $ldap = LdapConnect();
        $ldap = LdapConnect();
      my $filter = "(&amp;($RT::LdapMailSearchAttr=$mailaddr)$RT::LdapMailFilter)";
        my $filter = "(&amp;($RT::LdapMailSearchAttr=$mailaddr)$RT::LdapMailFilter)";
      my @attr = keys %RT::LdapMailResultMap;
        my @attr = keys %RT::LdapMailResultMap;
      $RT::Logger-&gt;info( "LdapUserFindByMailaddr: Looking for ",
        $RT::Logger-&gt;info( "LdapUserFindByMailaddr: Looking for ",
                              join(" ", @attr), " filter=", $filter );
                              join(" ", @attr), " filter=", $filter );
      $mesg = $ldap-&gt;search(
        $mesg = $ldap-&gt;search(
                            base      =&gt; $RT::LdapMailBase,
                              base      =&gt; $RT::LdapMailBase,
                            scope      =&gt; $RT::LdapMailScope,
                              scope      =&gt; $RT::LdapMailScope,
                            filter    =&gt; $filter,
                              filter    =&gt; $filter,
                            attributes =&gt; [@attr],
                              attributes =&gt; [@attr],
                            );
                              );
      if ( ($mesg-&gt;code != LDAP_SUCCESS) and
        if ( ($mesg-&gt;code != LDAP_SUCCESS) and
            ($mesg-&gt;code != LDAP_PARTIAL_RESULTS) ) {
            ($mesg-&gt;code != LDAP_PARTIAL_RESULTS) ) {
          $RT::Logger-&gt;critical("LdapUserFindByMailaddr: Search failed: ",
            $RT::Logger-&gt;critical("LdapUserFindByMailaddr: Search failed: ",
                                "retval=", $mesg-&gt;code, " ",
                                  "retval=", $mesg-&gt;code, " ",
                                ldap_error_name($mesg-&gt;code));
                                  ldap_error_name($mesg-&gt;code));
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
          return undef;
            return undef;
      }
        }
 
   
      if (1 != $mesg-&gt;count) {
        if (1 != $mesg-&gt;count) {
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
          return undef;
            return undef;
      }
        }
 
   
      while( my $entry = $mesg-&gt;shift_entry) {
        while( my $entry = $mesg-&gt;shift_entry) {
          foreach my $attr (keys %RT::LdapMailResultMap) {
            foreach my $attr (keys %RT::LdapMailResultMap) {
              foreach my $value ($entry-&gt;get_value($attr)) {
                foreach my $value ($entry-&gt;get_value($attr)) {
                  $UserInfo{$RT::LdapMailResultMap{$attr}} = $value;
                    $UserInfo{$RT::LdapMailResultMap{$attr}} = $value;
              }
                }
          }
            }
      }
        }
      LdapDisconnect($ldap);
        LdapDisconnect($ldap);
      return %UserInfo;
        return %UserInfo;
  }
    }
 
   
  # {{{ sub LdapConnect
    # {{{ sub LdapConnect
 
   
  =head2 LdapConnect
    =head2 LdapConnect
 
   
  Connect to the LDAP databsae.
    Connect to the LDAP databsae.
 
   
  The following configure options are used by this function:
    The following configure options are used by this function:
 
   
    $RT::LdapServer
      $RT::LdapServer
    $RT::LdapUser
      $RT::LdapUser
    $RT::LdapPass
      $RT::LdapPass
 
   
  =cut
    =cut
 
   
  sub LdapConnect {
    sub LdapConnect {
      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);
 
   
      my $mesg;
        my $mesg;
      my $ldap = Net::LDAP-&gt;new($RT::LdapServer,
        my $ldap = Net::LDAP-&gt;new($RT::LdapServer,
                                version =&gt; 3);
                                  version =&gt; 3);
 
   
      unless ($ldap) {
        unless ($ldap) {
          $RT::Logger-&gt;critical("IsLdapPassword: Cannot connect to",
            $RT::Logger-&gt;critical("IsLdapPassword: Cannot connect to",
                                "LDAP server ", $RT::LdapServer);
                                  "LDAP server ", $RT::LdapServer);
          return undef;
            return undef;
      }
        }
 
   
      # I seem to have problems if I try and bind with a NULL username
        # I seem to have problems if I try and bind with a NULL username
      # by hand So this now checks to see if we are really going to bind
        # by hand So this now checks to see if we are really going to bind
      # with a username.
        # with a username.
      if (defined($RT::LdapUser) &amp;&amp; $RT::LdapUser ne '') {
        if (defined($RT::LdapUser) &amp;&amp; $RT::LdapUser ne '') {
          $mesg = $ldap-&gt;bind($RT::LdapUser,
            $mesg = $ldap-&gt;bind($RT::LdapUser,
                              password =&gt; $RT::LdapPass );
                                password =&gt; $RT::LdapPass );
      } else {
        } else {
          # This bind is redundant with LDAP protocol version 3
            # This bind is redundant with LDAP protocol version 3
          $mesg = $ldap-&gt;bind;
            $mesg = $ldap-&gt;bind;
      }
        }
      if ($mesg-&gt;code != LDAP_SUCCESS) {
        if ($mesg-&gt;code != LDAP_SUCCESS) {
          $RT::Logger-&gt;critical("IsLdapPassword: Cannot bind to LDAP: ",
            $RT::Logger-&gt;critical("IsLdapPassword: Cannot bind to LDAP: ",
                                "retval=", $mesg-&gt;code, " ",
                                  "retval=", $mesg-&gt;code, " ",
                                ldap_error_name($mesg-&gt;code));
                                  ldap_error_name($mesg-&gt;code));
          return undef;
            return undef;
      }
        }
      return $ldap;
        return $ldap;
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub LdapDisconnect
    # {{{ sub LdapDisconnect
 
   
  =head2 LdapDisconnect
    =head2 LdapDisconnect
 
   
  Disconnect from the LDAP database.
    Disconnect from the LDAP database.
 
   
  =cut
    =cut
 
   
  sub LdapDisconnect {
    sub LdapDisconnect {
      my $ldap = shift;
        my $ldap = shift;
      my $mesg = $ldap-&gt;unbind();
        my $mesg = $ldap-&gt;unbind();
      if ($mesg-&gt;code != LDAP_SUCCESS) {
        if ($mesg-&gt;code != LDAP_SUCCESS) {
          $RT::Logger-&gt;critical("LdapDisconnect: unbind failed: ",
            $RT::Logger-&gt;critical("LdapDisconnect: unbind failed: ",
                                "retval=", $mesg-&gt;code, " ",
                                  "retval=", $mesg-&gt;code, " ",
                                ldap_error_name($mesg-&gt;code));
                                  ldap_error_name($mesg-&gt;code));
      }
        }
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub LdapFindUser
    # {{{ sub LdapFindUser
 
   
  =head2 LdapFindUser
    =head2 LdapFindUser
 
   
  Locate info on a giver user given the username.
    Locate info on a giver user given the username.
 
   
  Configure options used by this function:
    Configure options used by this function:
 
   
    $RT::LdpaAuthBase
      $RT::LdpaAuthBase
    $RT::LdpaAuthFilter
      $RT::LdpaAuthFilter
    $RT::LdpaAuthUidAttr
      $RT::LdpaAuthUidAttr
 
   
  =cut
    =cut
 
   
  sub LdapFindUser {
    sub LdapFindUser {
      my $ldap = shift;
        my $ldap = shift;
      my $username = shift;
        my $username = shift;
 
   
      my $filter;
        my $filter;
      if ($RT::LdapAuthFilter) {
        if ($RT::LdapAuthFilter) {
          $filter = "(&amp;(" .$RT::LdapAuthUidAttr . "=$username)$RT::LdapAuthFilter)";
            $filter = "(&amp;(" .$RT::LdapAuthUidAttr . "=$username)$RT::LdapAuthFilter)";
      } else {
        } else {
          $filter = "(" .$RT::LdapAuthUidAttr . "=$username)";
            $filter = "(" .$RT::LdapAuthUidAttr . "=$username)";
      }
        }
 
   
      $RT::Logger-&gt;debug("IsLdapPassword: First search filter '$filter'");
        $RT::Logger-&gt;debug("IsLdapPassword: First search filter '$filter'");
      my $mesg = $ldap-&gt;search(base  =&gt; $RT::LdapAuthBase,
        my $mesg = $ldap-&gt;search(base  =&gt; $RT::LdapAuthBase,
                                filter =&gt; $filter,
                                filter =&gt; $filter,
                                attrs  =&gt; ['dn']);
                                attrs  =&gt; ['dn']);
      if (!(($mesg-&gt;code == LDAP_SUCCESS) or
        if (!(($mesg-&gt;code == LDAP_SUCCESS) or
            ($mesg-&gt;code == LDAP_PARTIAL_RESULTS)))
              ($mesg-&gt;code == LDAP_PARTIAL_RESULTS)))
      {
        {
          $RT::Logger-&gt;debug("IsLdapPassword: Could not search for $filter: ",
            $RT::Logger-&gt;debug("IsLdapPassword: Could not search for $filter: ",
                              "retval=", $mesg-&gt;code, " ",
                              "retval=", $mesg-&gt;code, " ",
                              ldap_error_name($mesg-&gt;code));
                              ldap_error_name($mesg-&gt;code));
          return undef;
            return undef;
      }
        }
      return $mesg;
        return $mesg;
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub IsLdapPassword
    # {{{ sub IsLdapPassword
 
   
  =head2 IsLdapPassword
    =head2 IsLdapPassword
 
   
  Takes a username and password as argument, and check if the password
    Takes a username and password as argument, and check if the password
  is correct for the given user.  Return undef if password check failed,
    is correct for the given user.  Return undef if password check failed,
  -1 if the user is unknown, and 1 if the password check succeeded.
    -1 if the user is unknown, and 1 if the password check succeeded.
 
   
  =cut
    =cut
 
   
  sub IsLdapPassword {
    sub IsLdapPassword {
      my $username = shift;
        my $username = shift;
      my $value    = shift;
        my $value    = shift;
 
   
      $RT::Logger-&gt;debug("IsLdapPassword: executing");
        $RT::Logger-&gt;debug("IsLdapPassword: executing");
      my $ldap = LdapConnect();
        my $ldap = LdapConnect();
      return undef unless $ldap;
        return undef unless $ldap;
 
   
      my $mesg = LdapFindUser($ldap, $username);
        my $mesg = LdapFindUser($ldap, $username);
      unless ($mesg) {
        unless ($mesg) {
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
          return undef;
            return undef;
      }
        }
      $RT::Logger-&gt;debug("IsLdapPassword: First search produced ",
        $RT::Logger-&gt;debug("IsLdapPassword: First search produced ",
                          $mesg-&gt;count, " results");
                          $mesg-&gt;count, " results");
      if (! $mesg-&gt;count)
        if (! $mesg-&gt;count)
      {
        {
          $RT::Logger-&gt;info("IsLdapPassword: AUTH FAILED $username");
            $RT::Logger-&gt;info("IsLdapPassword: AUTH FAILED $username");
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
          return -1;
            return -1;
      }
        }
      $ldap-&gt;start_tls( verify =&gt; 'require',
        $ldap-&gt;start_tls( verify =&gt; 'require',
                        cafile =&gt; $RT::LdapCAFile ) if ($RT::LdapAuthStartTLS);
                          cafile =&gt; $RT::LdapCAFile ) if ($RT::LdapAuthStartTLS);
 
   
      my $userdn = $mesg-&gt;first_entry-&gt;dn;
        my $userdn = $mesg-&gt;first_entry-&gt;dn;
      $RT::Logger-&gt;debug("IsLdapPassword: Trying to bind using DN=$userdn");
        $RT::Logger-&gt;debug("IsLdapPassword: Trying to bind using DN=$userdn");
      my $mesg2 = $ldap-&gt;bind($userdn,
        my $mesg2 = $ldap-&gt;bind($userdn,
                              password =&gt; $value );
                                password =&gt; $value );
      if ($mesg2-&gt;code != LDAP_SUCCESS) {
        if ($mesg2-&gt;code != LDAP_SUCCESS) {
          $RT::Logger-&gt;critical("IsLdapPassword: Unable to bind as $userdn: ",
            $RT::Logger-&gt;critical("IsLdapPassword: Unable to bind as $userdn: ",
                                "retval=", $mesg2-&gt;code, " ",
                                  "retval=", $mesg2-&gt;code, " ",
                                ldap_error_name($mesg2-&gt;code));
                                  ldap_error_name($mesg2-&gt;code));
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
          return undef;
            return undef;
      }
        }
      else
        else
      {
        {
          $RT::Logger-&gt;info("IsLdapPassword: AUTH OK $username ($userdn) base:",
            $RT::Logger-&gt;info("IsLdapPassword: AUTH OK $username ($userdn) base:",
                            $RT::LdapAuthBase);
                              $RT::LdapAuthBase);
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
          return 1;
            return 1;
      }
        }
  }
    }
 
   
  # }}}
    # }}}
 
   
  1;
    1;
 
   
 
   
  </nowiki>
    </nowiki>


Add configuration to /opt/rt3/etc/[[RT SiteConfig|RT_SiteConfig]].pm:
Add configuration to /opt/rt3/etc/[[RT SiteConfig|RT_SiteConfig]].pm:
Line 696: Line 696:


  <nowiki># BEGIN LICENSE BLOCK
  <nowiki># BEGIN LICENSE BLOCK
  #
    #
  # Copyright (c) 2005 Nathan Mehl &lt;rt-ad-sso@memory.blank.org&gt;
    # Copyright (c) 2005 Nathan Mehl &lt;rt-ad-sso@memory.blank.org&gt;
  # (Except where explictly superceded by other copyright notices)
    # (Except where explictly superceded by other copyright notices)
  # portions Copyright (c) 2004 Petter Reinholdtsen &lt;pere@hungry.com&gt;
    # portions Copyright (c) 2004 Petter Reinholdtsen &lt;pere@hungry.com&gt;
  # portions Copyright (c) 2004 Jesse Vincent &lt;jesse@fsck.com&gt;
    # portions Copyright (c) 2004 Jesse Vincent &lt;jesse@fsck.com&gt;
  #
    #
  # This work is made available to you under the terms of Version 2 of
    # This work is made available to you under the terms of Version 2 of
  # the GNU General Public License. A copy of that license should have
    # the GNU General Public License. A copy of that license should have
  # been provided with this software, but in any event can be snarfed
    # been provided with this software, but in any event can be snarfed
  # from www.gnu.org.
    # from www.gnu.org.
  #
    #
  # This work is distributed in the hope that it will be useful, but
    # This work is distributed in the hope that it will be useful, but
  # WITHOUT ANY WARRANTY; without even the implied warranty of
    # WITHOUT ANY WARRANTY; without even the implied warranty of
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  # General Public License for more details.
    # General Public License for more details.
  #
    #
  # Unless otherwise specified, all modifications, corrections or
    # Unless otherwise specified, all modifications, corrections or
  # extensions to this work which alter its source code become the
    # extensions to this work which alter its source code become the
  # property of Best Practical Solutions, LLC when submitted for
    # property of Best Practical Solutions, LLC when submitted for
  # inclusion in the work.
    # inclusion in the work.
  #
    #
  #
    #
  # END LICENSE BLOCK
    # END LICENSE BLOCK
 
   
  package RT::Interface::Web;
    package RT::Interface::Web;
 
   
  no warnings qw(redefine);
    no warnings qw(redefine);
 
   
  # {{{ WebExternalAutoInfo
    # {{{ WebExternalAutoInfo
 
   
  =head2 WebExternalAutoInfo($user);
    =head2 WebExternalAutoInfo($user);
 
   
  Returns a hash of user attributes, used when WebExternalAuto is set.
    Returns a hash of user attributes, used when WebExternalAuto is set.
 
   
  =cut
    =cut
 
   
  sub WebExternalAutoInfo {
    sub WebExternalAutoInfo {
      my $user = shift;
        my $user = shift;
 
   
      my %user_info;
        my %user_info;
 
   
      $user_info{'Privileged'} = 0;
        $user_info{'Privileged'} = 0;
 
   
      $RT::Logger-&gt;debug( "WebExternalAutoInfo: Looking for ", $user );
        $RT::Logger-&gt;debug( "WebExternalAutoInfo: Looking for ", $user );
      my ($UserFoundInExternalDatabase, %ExternalUserInfo) =
        my ($UserFoundInExternalDatabase, %ExternalUserInfo) =
          LookupExternalUsername( $user );
            LookupExternalUsername( $user );
 
   
      # populate user fields from the ldap directory
        # populate user fields from the ldap directory
      if ($UserFoundInExternalDatabase) {
        if ($UserFoundInExternalDatabase) {
          $user_info{'RealName'} = $ExternalUserInfo{'RealName'} if defined $ExternalUserInfo{'RealName'};
            $user_info{'RealName'} = $ExternalUserInfo{'RealName'} if defined $ExternalUserInfo{'RealName'};
          $user_info{'Name'} = $ExternalUserInfo{'Name'} if defined $ExternalUserInfo{'Name'};
            $user_info{'Name'} = $ExternalUserInfo{'Name'} if defined $ExternalUserInfo{'Name'};
          $user_info{'EmailAddress'} = $ExternalUserInfo{'EmailAddress'} if defined $ExternalUserInfo{'EmailAddress'};
            $user_info{'EmailAddress'} = $ExternalUserInfo{'EmailAddress'} if defined $ExternalUserInfo{'EmailAddress'};
      } elsif ($^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/) {
        } elsif ($^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/) {
          # Populate fields with information from Unix /etc/passwd
            # Populate fields with information from Unix /etc/passwd
          my ($comments, $realname) = (getpwnam($user))[5, 6];
            my ($comments, $realname) = (getpwnam($user))[5, 6];
          $user_info{'Comments'} = $comments if defined $comments;
            $user_info{'Comments'} = $comments if defined $comments;
          $user_info{'RealName'} = $realname if defined $realname;
            $user_info{'RealName'} = $realname if defined $realname;
      }
        }
      elsif ($^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1') {
        elsif ($^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1') {
          # Populate fields with information from NT domain controller
            # Populate fields with information from NT domain controller
      }
        }
 
   
      # and return the wad of stuff
        # and return the wad of stuff
      return {%user_info};
        return {%user_info};
  }
    }
 
   
  # }}}
    # }}}
 
   
  sub LookupExternalUsername {
    sub LookupExternalUsername {
    my %UserInfo = ();
      my %UserInfo = ();
    $UserInfo{'Name'} = shift;
      $UserInfo{'Name'} = shift;
    $UserInfo{'Name'} =~ s/\"//g;
      $UserInfo{'Name'} =~ s/\"//g;
 
   
    my $FoundInExternalDatabase = 0;
      my $FoundInExternalDatabase = 0;
 
   
    $RT::Logger-&gt;debug( "LookupExternalUsername: Looking for ", $UserInfo{'Name'} );
      $RT::Logger-&gt;debug( "LookupExternalUsername: Looking for ", $UserInfo{'Name'} );
    # Name is the RT username you want to use for this user.
      # Name is the RT username you want to use for this user.
    my %LdapUserInfo = LdapUserFindByUsername($UserInfo{'Name'});
      my %LdapUserInfo = LdapUserFindByUsername($UserInfo{'Name'});
    if ($LdapUserInfo{'Name'}) {
      if ($LdapUserInfo{'Name'}) {
        $FoundInExternalDatabase = 1;
          $FoundInExternalDatabase = 1;
        $RT::Logger-&gt;debug("LookupExternalUsername: Mapping '".
          $RT::Logger-&gt;debug("LookupExternalUsername: Mapping '".
                          $UserInfo{'Name'} .
                            $UserInfo{'Name'} .
                          "' to '" .
                            "' to '" .
                          $LdapUserInfo{'EmailAddress'} . "'");
                            $LdapUserInfo{'EmailAddress'} . "'");
        foreach my $key (keys %LdapUserInfo) {
          foreach my $key (keys %LdapUserInfo) {
            $UserInfo{$key} = $LdapUserInfo{$key};
              $UserInfo{$key} = $LdapUserInfo{$key};
        }
          }
    } else {
      } else {
        $RT::Logger-&gt;debug("LookupExternalUsername: Fail to find username for '".
          $RT::Logger-&gt;debug("LookupExternalUsername: Fail to find username for '".
                          $UserInfo{'Name'}."'");
                            $UserInfo{'Name'}."'");
    }
      }
 
   
    return ($FoundInExternalDatabase, %UserInfo);
      return ($FoundInExternalDatabase, %UserInfo);
  }
    }
 
   
  sub LdapUserFindByUsername {
    sub LdapUserFindByUsername {
      my $username = shift;
        my $username = shift;
      my %UserInfo = ();
        my %UserInfo = ();
 
   
      my $ldap = RT::User::LdapConnect();
        my $ldap = RT::User::LdapConnect();
      my $filter = "(&amp;($RT::LdapAuthUidAttr=$username)$RT::LdapMailFilter)";
        my $filter = "(&amp;($RT::LdapAuthUidAttr=$username)$RT::LdapMailFilter)";
      my @attr = keys %RT::LdapMailResultMap;
        my @attr = keys %RT::LdapMailResultMap;
      $RT::Logger-&gt;debug( "LdapUserFindByUsername: Looking for ",
        $RT::Logger-&gt;debug( "LdapUserFindByUsername: Looking for ",
                              join(" ", @attr), " filter=", $filter );
                              join(" ", @attr), " filter=", $filter );
      my $mesg = $ldap-&gt;search(
        my $mesg = $ldap-&gt;search(
                            base      =&gt; $RT::LdapMailBase,
                              base      =&gt; $RT::LdapMailBase,
                            scope      =&gt; $RT::LdapMailScope,
                              scope      =&gt; $RT::LdapMailScope,
                            filter    =&gt; $filter,
                              filter    =&gt; $filter,
                            attributes =&gt; [@attr],
                              attributes =&gt; [@attr],
                            );
                              );
      if ( ($mesg-&gt;code != LDAP_SUCCESS) and
        if ( ($mesg-&gt;code != LDAP_SUCCESS) and
            ($mesg-&gt;code != LDAP_PARTIAL_RESULTS) ) {
            ($mesg-&gt;code != LDAP_PARTIAL_RESULTS) ) {
          $RT::Logger-&gt;critical("LdapUserFindByUsername: Search failed: ",
            $RT::Logger-&gt;critical("LdapUserFindByUsername: Search failed: ",
                                "retval=", $mesg-&gt;code, " ",
                                  "retval=", $mesg-&gt;code, " ",
                                ldap_error_name($mesg-&gt;code));
                                  ldap_error_name($mesg-&gt;code));
          RT::User::LdapDisconnect($ldap);
            RT::User::LdapDisconnect($ldap);
          return undef;
            return undef;
      }
        }
 
   
      if (1 != $mesg-&gt;count) {
        if (1 != $mesg-&gt;count) {
          $RT::Logger-&gt;critical("LdapUserFindByUsername: Search returned 0 results: ",
            $RT::Logger-&gt;critical("LdapUserFindByUsername: Search returned 0 results: ",
                                "retval=", $mesg-&gt;code, " ",
                                  "retval=", $mesg-&gt;code, " ",
                                ldap_error_name($mesg-&gt;code));
                                  ldap_error_name($mesg-&gt;code));
          RT::User::LdapDisconnect($ldap);
            RT::User::LdapDisconnect($ldap);
          return undef;
            return undef;
      }
        }
 
   
      while( my $entry = $mesg-&gt;shift_entry) {
        while( my $entry = $mesg-&gt;shift_entry) {
          foreach my $attr (keys %RT::LdapMailResultMap) {
            foreach my $attr (keys %RT::LdapMailResultMap) {
              foreach my $value ($entry-&gt;get_value($attr)) {
                foreach my $value ($entry-&gt;get_value($attr)) {
                  $UserInfo{$RT::LdapMailResultMap{$attr}} = $value;
                    $UserInfo{$RT::LdapMailResultMap{$attr}} = $value;
              }
                }
          }
            }
      }
        }
      RT::User::LdapDisconnect($ldap);
        RT::User::LdapDisconnect($ldap);
      return %UserInfo;
        return %UserInfo;
  }
    }
 
   
  1;
    1;
 
   
 
   
 
   
  </nowiki>
    </nowiki>


After running this for a while, I found users were entering logins instead of email addresses, and lookup in AD was failing, so I changed [[User Local|User_Local]].pm as follows, to try lookup by username if lookup by email address fails:
After running this for a while, I found users were entering logins instead of email addresses, and lookup in AD was failing, so I changed [[User Local|User_Local]].pm as follows, to try lookup by username if lookup by email address fails:


  <nowiki>[root@hotel RT]# diff -C 5 -b User_Local.pm.old User_Local.pm
  <nowiki>[root@hotel RT]# diff -C 5 -b User_Local.pm.old User_Local.pm
  *** User_Local.pm.old  2007-03-23 13:05:02.000000000 +1200
    *** User_Local.pm.old  2007-03-23 13:05:02.000000000 +1200
  --- User_Local.pm      2007-03-23 13:13:23.000000000 +1200
    --- User_Local.pm      2007-03-23 13:13:23.000000000 +1200
  ***************
    ***************
  *** 20,41 ****
    *** 20,41 ****
  --- 20,45 ----
    --- 20,45 ----
    # inclusion in the work.
      # inclusion in the work.
    #
      #
    #
      #
    # END LICENSE BLOCK
      # END LICENSE BLOCK
 
   
  +
    +
    # LDAP integration in RT 3.  These overrides provide LDAP
      # LDAP integration in RT 3.  These overrides provide LDAP
    # authentication and user info syncronizing.
      # authentication and user info syncronizing.
    #
      #
    # Written by Petter Reinholdtsen &lt;pere@hungry.com&gt; based on Code from
      # Written by Petter Reinholdtsen &lt;pere@hungry.com&gt; based on Code from
    # Marcelo Bartsch &lt;bartschm_cl@hotmail.com&gt;, Stewart James
      # Marcelo Bartsch &lt;bartschm_cl@hotmail.com&gt;, Stewart James
    # &lt;stewart.james@vu.edu.au&gt; and Carl Makin &lt;carl@xena.IPAustralia.gov.au&gt;.
      # &lt;stewart.james@vu.edu.au&gt; and Carl Makin &lt;carl@xena.IPAustralia.gov.au&gt;.
    #
      #
    # Copy this file into rt3/local/lib/RT/User_Local.pm to active it.
      # Copy this file into rt3/local/lib/RT/User_Local.pm to active it.
 
   
  +
    +
    # Modification Originally by Marcelo Bartsch &lt;bartschm_cl@hotmail.com&gt;
      # Modification Originally by Marcelo Bartsch &lt;bartschm_cl@hotmail.com&gt;
    # Update by Stewart James &lt;stewart.james@vu.edu.au for rt3.
      # Update by Stewart James &lt;stewart.james@vu.edu.au for rt3.
    # Update with TLS support and more flexible LDAP code by Petter Reinholdtsen.
      # Update with TLS support and more flexible LDAP code by Petter Reinholdtsen.
  + # Update to handle logins presented as email addresses.
    + # Update to handle logins presented as email addresses.
  + #
    + #
    # Drop this file in /opt/rt3/lib/RT/User_Local.pm
      # Drop this file in /opt/rt3/lib/RT/User_Local.pm
    # Drop something like below in yout RT_SiteConfig.pm
      # Drop something like below in yout RT_SiteConfig.pm
    #
      #
    # Set($LDAPExternalAuth, 1); # Enable LDAP auth
      # Set($LDAPExternalAuth, 1); # Enable LDAP auth
    # Set($LdapServer, "ldap.domain.com");
      # Set($LdapServer, "ldap.domain.com");
  ***************
    ***************
  *** 45,54 ****
    *** 45,54 ****
  --- 49,59 ----
    --- 49,59 ----
    # Set($LdapAuthPass, "");
      # Set($LdapAuthPass, "");
    # Set($LdapAuthBase, "ou=users,dc=domain,dc=com");
      # Set($LdapAuthBase, "ou=users,dc=domain,dc=com");
    # Set($LdapAuthUidAttr, "uid");
      # Set($LdapAuthUidAttr, "uid");
    # Set($LdapAuthFilter, "(objectclass=posixAccount)");
      # Set($LdapAuthFilter, "(objectclass=posixAccount)");
 
   
  +
    +
    no warnings qw(redefine);
      no warnings qw(redefine);
 
   
    # {{{ sub LookupExternalUserInfo
      # {{{ sub LookupExternalUserInfo
 
   
    =item LookupExternalUserInfo
      =item LookupExternalUserInfo
  ***************
    ***************
  *** 85,96 ****
    *** 85,96 ****
  --- 90,110 ----
    --- 90,110 ----
      $UserInfo{'RealName'} = shift;
        $UserInfo{'RealName'} = shift;
      $UserInfo{'RealName'} =~ s/\"//g;
        $UserInfo{'RealName'} =~ s/\"//g;
 
   
      my $FoundInExternalDatabase = 0;
        my $FoundInExternalDatabase = 0;
 
   
  +  $RT::Logger-&gt;info("LookupExternalUserInfo: looking up EmailAddress: '"
    +  $RT::Logger-&gt;info("LookupExternalUserInfo: looking up EmailAddress: '"
  +      . $UserInfo{'EmailAddress'} . "', RealName: '"
    +      . $UserInfo{'EmailAddress'} . "', RealName: '"
  +      . $UserInfo{'RealName'} . "'" );
    +      . $UserInfo{'RealName'} . "'" );
  +
    +
      # Name is the RT username you want to use for this user.
        # Name is the RT username you want to use for this user.
      my %LdapUserInfo = LdapUserFindByMailaddr($UserInfo{'EmailAddress'});
        my %LdapUserInfo = LdapUserFindByMailaddr($UserInfo{'EmailAddress'});
  +  # The EmailAddress may actually be a username, so if lookup by
    +  # The EmailAddress may actually be a username, so if lookup by
  +  # email address fails, try lookup by username
    +  # email address fails, try lookup by username
  +  unless($LdapUserInfo{'Name'}) {
    +  unless($LdapUserInfo{'Name'}) {
  +      %LdapUserInfo = LdapUserFindByName($UserInfo{'EmailAddress'});
    +      %LdapUserInfo = LdapUserFindByName($UserInfo{'EmailAddress'});
  +  }
    +  }
      if ($LdapUserInfo{'Name'}) {
        if ($LdapUserInfo{'Name'}) {
          $FoundInExternalDatabase = 1;
            $FoundInExternalDatabase = 1;
          $RT::Logger-&gt;info("LookupExternalUserInfo: Mapping '".
            $RT::Logger-&gt;info("LookupExternalUserInfo: Mapping '".
                            $UserInfo{'EmailAddress'} .
                              $UserInfo{'EmailAddress'} .
                            "' to '" .
                              "' to '" .
  ***************
    ***************
  *** 113,122 ****
    *** 113,122 ****
  --- 127,140 ----
    --- 127,140 ----
    sub CanonicalizeUserInfo {
      sub CanonicalizeUserInfo {
        my $self    = shift;
          my $self    = shift;
        my $argsref = shift;
          my $argsref = shift;
        my $success = 1;
          my $success = 1;
 
   
  +  $RT::Logger-&gt;info("CanonicalizeUserInfo: "
    +  $RT::Logger-&gt;info("CanonicalizeUserInfo: "
  +      . "EmailAddress: '" . $argsref-&gt;{'EmailAddress'}
    +      . "EmailAddress: '" . $argsref-&gt;{'EmailAddress'}
  +      . "', RealName: '" . $argsref-&gt;{'RealName'} . "'" );
    +      . "', RealName: '" . $argsref-&gt;{'RealName'} . "'" );
  +
    +
        my ($UserFoundInExternalDatabase, %ExternalUserInfo) =
          my ($UserFoundInExternalDatabase, %ExternalUserInfo) =
            LookupExternalUserInfo( $argsref-&gt;{'EmailAddress'},
              LookupExternalUserInfo( $argsref-&gt;{'EmailAddress'},
                                    $argsref-&gt;{'RealName'} );
                                      $argsref-&gt;{'RealName'} );
        if ($UserFoundInExternalDatabase) {
          if ($UserFoundInExternalDatabase) {
            for my $key (keys %ExternalUserInfo) {
              for my $key (keys %ExternalUserInfo) {
  ***************
    ***************
  *** 275,284 ****
    *** 275,284 ****
  --- 293,379 ----
    --- 293,379 ----
            return (undef);
              return (undef);
    }
      }
 
   
    # }}}
      # }}}
 
   
  + # {{{ sub LdapUserFindByName
    + # {{{ sub LdapUserFindByName
  +
    +
  + =head2 LdapUserFindByName
    + =head2 LdapUserFindByName
  +
    +
  + Lookup user owning a given name on OU, returning the
    + Lookup user owning a given name on OU, returning the
  + username or undef if not known or the search failed.
    + username or undef if not known or the search failed.
  +
    +
  + The following configure options are used by this function in addition
    + The following configure options are used by this function in addition
  + to the ones used by LdapConnect().
    + to the ones used by LdapConnect().
  +
    +
  +  $RT::LdapNameBase
    +  $RT::LdapNameBase
  +  $RT::LdapNameFilter
    +  $RT::LdapNameFilter
  +  $RT::LdapNameScope
    +  $RT::LdapNameScope
  +  $RT::LdapNameSearchAttr
    +  $RT::LdapNameSearchAttr
  +  $RT::LdapNameResultMap
    +  $RT::LdapNameResultMap
  +
    +
  + For example:
    + For example:
  +
    +
  +  Set($LdapNameBase,              'dc=mydomain,dc=co,dc=nz');
    +  Set($LdapNameBase,              'dc=mydomain,dc=co,dc=nz');
  +  Set($LdapNameFilter,            '(objectClass=user)');
    +  Set($LdapNameFilter,            '(objectClass=user)');
  +  Set($LdapNameScope,            'sub');
    +  Set($LdapNameScope,            'sub');
  +  Set($LdapNameSearchAttr,        'sAMAccountName');
    +  Set($LdapNameSearchAttr,        'sAMAccountName');
  +  %RT::LdapNameResultMap = (
    +  %RT::LdapNameResultMap = (
  +        'sAMAccountName'        =&gt; 'Name',
    +        'sAMAccountName'        =&gt; 'Name',
  +        'mail'                  =&gt; 'EmailAddress',
    +        'mail'                  =&gt; 'EmailAddress',
  +        'cn'                    =&gt; 'RealName',
    +        'cn'                    =&gt; 'RealName',
  +        );
    +        );
  +
    +
  +
    +
  + =cut
    + =cut
  +
    +
  + # Example search
    + # Example search
  + #  ldapsearch -x -b ou=mail,dc=uio,dc=no -ZZ -h ldap.uio.no -D uid=pre,ou=users,dc=uio,dc=no -W target=mathiasm
    + #  ldapsearch -x -b ou=mail,dc=uio,dc=no -ZZ -h ldap.uio.no -D uid=pre,ou=users,dc=uio,dc=no -W target=mathiasm
  +
    +
  + sub LdapUserFindByName {
    + sub LdapUserFindByName {
  +    my $name = shift;
    +    my $name = shift;
  +    my %UserInfo = ();
    +    my %UserInfo = ();
  +    $ldap = LdapConnect();
    +    $ldap = LdapConnect();
  +    my $filter = "(&amp;($RT::LdapNameSearchAttr=$name)$RT::LdapNameFilter)";
    +    my $filter = "(&amp;($RT::LdapNameSearchAttr=$name)$RT::LdapNameFilter)";
  +    my @attr = keys %RT::LdapNameResultMap;
    +    my @attr = keys %RT::LdapNameResultMap;
  +    $RT::Logger-&gt;info( "LdapUserFindByName: Looking for ",
    +    $RT::Logger-&gt;info( "LdapUserFindByName: Looking for ",
  +                            join(" ", @attr), " filter=", $filter );
    +                            join(" ", @attr), " filter=", $filter );
  +    $mesg = $ldap-&gt;search(
    +    $mesg = $ldap-&gt;search(
  +                          base      =&gt; $RT::LdapNameBase,
    +                          base      =&gt; $RT::LdapNameBase,
  +                          scope      =&gt; $RT::LdapNameScope,
    +                          scope      =&gt; $RT::LdapNameScope,
  +                          filter    =&gt; $filter,
    +                          filter    =&gt; $filter,
  +                          attributes =&gt; [@attr],
    +                          attributes =&gt; [@attr],
  +                          );
    +                          );
  +    if ( ($mesg-&gt;code != LDAP_SUCCESS) and
    +    if ( ($mesg-&gt;code != LDAP_SUCCESS) and
  +          ($mesg-&gt;code != LDAP_PARTIAL_RESULTS) ) {
    +          ($mesg-&gt;code != LDAP_PARTIAL_RESULTS) ) {
  +        $RT::Logger-&gt;critical("LdapUserFindByName: Search failed: ",
    +        $RT::Logger-&gt;critical("LdapUserFindByName: Search failed: ",
  +                              "retval=", $mesg-&gt;code, " ",
    +                              "retval=", $mesg-&gt;code, " ",
  +                              ldap_error_name($mesg-&gt;code));
    +                              ldap_error_name($mesg-&gt;code));
  +        LdapDisconnect($ldap);
    +        LdapDisconnect($ldap);
  +        return undef;
    +        return undef;
  +    }
    +    }
  +
    +
  +    if (1 != $mesg-&gt;count) {
    +    if (1 != $mesg-&gt;count) {
  +        $RT::Logger-&gt;critical("LdapUserFindByName: Search failed: ",
    +        $RT::Logger-&gt;critical("LdapUserFindByName: Search failed: ",
  +                              "\$mesg-&gt;count=", $mesg-&gt;count);
    +                              "\$mesg-&gt;count=", $mesg-&gt;count);
  +        LdapDisconnect($ldap);
    +        LdapDisconnect($ldap);
  +        return undef;
    +        return undef;
  +    }
    +    }
  +
    +
  +    while( my $entry = $mesg-&gt;shift_entry) {
    +    while( my $entry = $mesg-&gt;shift_entry) {
  +        foreach my $attr (keys %RT::LdapNameResultMap) {
    +        foreach my $attr (keys %RT::LdapNameResultMap) {
  +            foreach my $value ($entry-&gt;get_value($attr)) {
    +            foreach my $value ($entry-&gt;get_value($attr)) {
  +                $UserInfo{$RT::LdapNameResultMap{$attr}} = $value;
    +                $UserInfo{$RT::LdapNameResultMap{$attr}} = $value;
  +            }
    +            }
  +        }
    +        }
  +    }
    +    }
  +    LdapDisconnect($ldap);
    +    LdapDisconnect($ldap);
  +    return %UserInfo;
    +    return %UserInfo;
  + }
    + }
  +
    +
  + # }}}
    + # }}}
  +
    +
    # {{{ sub LdapUserFindByMailaddr
      # {{{ sub LdapUserFindByMailaddr
 
   
    =head2 LdapUserFindByMailaddr
      =head2 LdapUserFindByMailaddr
 
   
    Lookup user owning a given email address on UiO, returning the
      Lookup user owning a given email address on UiO, returning the
  ***************
    ***************
  *** 422,431 ****
    *** 422,431 ****
  --- 517,530 ----
    --- 517,530 ----
 
   
    sub LdapFindUser {
      sub LdapFindUser {
        my $ldap = shift;
          my $ldap = shift;
        my $username = shift;
          my $username = shift;
 
   
  +  $RT::Logger-&gt;info("LdapFindUser: "
    +  $RT::Logger-&gt;info("LdapFindUser: "
  +      . "ldap: '" . $ldap
    +      . "ldap: '" . $ldap
  +      . "', username: '" . $username . "'" );
    +      . "', username: '" . $username . "'" );
  +
    +
        my $filter;
          my $filter;
        if ($RT::LdapAuthFilter) {
          if ($RT::LdapAuthFilter) {
            $filter = "(&amp;(" .$RT::LdapAuthUidAttr . "=$username)$RT::LdapAuthFilter)";
              $filter = "(&amp;(" .$RT::LdapAuthUidAttr . "=$username)$RT::LdapAuthFilter)";
        } else {
          } else {
            $filter = "(" .$RT::LdapAuthUidAttr . "=$username)";
              $filter = "(" .$RT::LdapAuthUidAttr . "=$username)";
 
   
 
   
 
   
  </nowiki>
    </nowiki>


Or, if you just want to cut and paste the final form:
Or, if you just want to cut and paste the final form:


  <nowiki># BEGIN LICENSE BLOCK
  <nowiki># BEGIN LICENSE BLOCK
  #
    #
  # Copyright (c) 2004 Petter Reinholdtsen &lt;pere@hungry.com&gt;
    # Copyright (c) 2004 Petter Reinholdtsen &lt;pere@hungry.com&gt;
  #
    #
  # (Except where explictly superceded by other copyright notices)
    # (Except where explictly superceded by other copyright notices)
  #
    #
  # This work is made available to you under the terms of Version 2 of
    # This work is made available to you under the terms of Version 2 of
  # the GNU General Public License. A copy of that license should have
    # the GNU General Public License. A copy of that license should have
  # been provided with this software, but in any event can be snarfed
    # been provided with this software, but in any event can be snarfed
  # from www.gnu.org.
    # from www.gnu.org.
  #
    #
  # This work is distributed in the hope that it will be useful, but
    # This work is distributed in the hope that it will be useful, but
  # WITHOUT ANY WARRANTY; without even the implied warranty of
    # WITHOUT ANY WARRANTY; without even the implied warranty of
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  # General Public License for more details.
    # General Public License for more details.
  #
    #
  # Unless otherwise specified, all modifications, corrections or
    # Unless otherwise specified, all modifications, corrections or
  # extensions to this work which alter its source code become the
    # extensions to this work which alter its source code become the
  # property of Best Practical Solutions, LLC when submitted for
    # property of Best Practical Solutions, LLC when submitted for
  # inclusion in the work.
    # inclusion in the work.
  #
    #
  #
    #
  # END LICENSE BLOCK
    # END LICENSE BLOCK
 
   
 
   
  # LDAP integration in RT 3.  These overrides provide LDAP
    # LDAP integration in RT 3.  These overrides provide LDAP
  # authentication and user info syncronizing.
    # authentication and user info syncronizing.
  #
    #
  # Written by Petter Reinholdtsen &lt;pere@hungry.com&gt; based on Code from
    # Written by Petter Reinholdtsen &lt;pere@hungry.com&gt; based on Code from
  # Marcelo Bartsch &lt;bartschm_cl@hotmail.com&gt;, Stewart James
    # Marcelo Bartsch &lt;bartschm_cl@hotmail.com&gt;, Stewart James
  # &lt;stewart.james@vu.edu.au&gt; and Carl Makin &lt;carl@xena.IPAustralia.gov.au&gt;.
    # &lt;stewart.james@vu.edu.au&gt; and Carl Makin &lt;carl@xena.IPAustralia.gov.au&gt;.
  #
    #
  # Copy this file into rt3/local/lib/RT/User_Local.pm to active it.
    # Copy this file into rt3/local/lib/RT/User_Local.pm to active it.
 
   
 
   
  # Modification Originally by Marcelo Bartsch &lt;bartschm_cl@hotmail.com&gt;
    # Modification Originally by Marcelo Bartsch &lt;bartschm_cl@hotmail.com&gt;
  # Update by Stewart James &lt;stewart.james@vu.edu.au for rt3.
    # Update by Stewart James &lt;stewart.james@vu.edu.au for rt3.
  # Update with TLS support and more flexible LDAP code by Petter Reinholdtsen.
    # Update with TLS support and more flexible LDAP code by Petter Reinholdtsen.
  # Update to handle logins presented as email addresses.
    # Update to handle logins presented as email addresses.
  #
    #
  # Drop this file in /opt/rt3/lib/RT/User_Local.pm
    # Drop this file in /opt/rt3/lib/RT/User_Local.pm
  # Drop something like below in yout RT_SiteConfig.pm
    # Drop something like below in yout RT_SiteConfig.pm
  #
    #
  # Set($LDAPExternalAuth, 1); # Enable LDAP auth
    # Set($LDAPExternalAuth, 1); # Enable LDAP auth
  # Set($LdapServer, "ldap.domain.com");
    # Set($LdapServer, "ldap.domain.com");
  # Set($LdapCAFile, "/site/w3-sertifikater/w3_cacert.pem");
    # Set($LdapCAFile, "/site/w3-sertifikater/w3_cacert.pem");
  # Set($LdapAuthStartTLS, 1); # Need to use TLS or ldaps to check passwords
    # Set($LdapAuthStartTLS, 1); # Need to use TLS or ldaps to check passwords
  # Set($LdapUser, ""); # Can search without username and password
    # Set($LdapUser, ""); # Can search without username and password
  # Set($LdapAuthPass, "");
    # Set($LdapAuthPass, "");
  # Set($LdapAuthBase, "ou=users,dc=domain,dc=com");
    # Set($LdapAuthBase, "ou=users,dc=domain,dc=com");
  # Set($LdapAuthUidAttr, "uid");
    # Set($LdapAuthUidAttr, "uid");
  # Set($LdapAuthFilter, "(objectclass=posixAccount)");
    # Set($LdapAuthFilter, "(objectclass=posixAccount)");
 
   
 
   
  no warnings qw(redefine);
    no warnings qw(redefine);
 
   
  # {{{ sub LookupExternalUserInfo
    # {{{ sub LookupExternalUserInfo
 
   
  =item LookupExternalUserInfo
    =item LookupExternalUserInfo
 
   
    LookupExternalUserInfo is a site-definable method for synchronizing
    LookupExternalUserInfo is a site-definable method for synchronizing
    incoming users with an external data source.
    incoming users with an external data source.
 
   
    This routine takes a tuple of EmailAddress and FriendlyName
    This routine takes a tuple of EmailAddress and FriendlyName
      EmailAddress is the user's email address, ususally taken from
      EmailAddress is the user's email address, ususally taken from
          an email message's From: header.
          an email message's From: header.
      RealName is a freeform string, ususally taken from the "comment"
      RealName is a freeform string, ususally taken from the "comment"
          portion of an email message's From: header.
          portion of an email message's From: header.
 
   
    It returns (FoundInExternalDatabase, ParamHash);
    It returns (FoundInExternalDatabase, ParamHash);
 
   
      FoundInExternalDatabase must be set to 1 before return if the user
      FoundInExternalDatabase must be set to 1 before return if the user
      was found in the external database.
      was found in the external database.
 
   
      ParamHash is a Perl parameter hash which can contain at least the
      ParamHash is a Perl parameter hash which can contain at least the
      following fields. These fields are used to populate RT's users
      following fields. These fields are used to populate RT's users
      database when the user is created
      database when the user is created
 
   
        EmailAddress is the email address that RT should use for this user.
        EmailAddress is the email address that RT should use for this user.
        Name is the 'Name' attribute RT should use for this user.
        Name is the 'Name' attribute RT should use for this user.
            'Name' is used for things like access control and user lookups.
            'Name' is used for things like access control and user lookups.
        RealName is what RT should display as the user's name when displaying
        RealName is what RT should display as the user's name when displaying
            'friendly' names
            'friendly' names
 
   
  =cut
    =cut
 
   
  sub LookupExternalUserInfo {
    sub LookupExternalUserInfo {
    my %UserInfo = ();
      my %UserInfo = ();
    $UserInfo{'EmailAddress'} = shift;
      $UserInfo{'EmailAddress'} = shift;
    $UserInfo{'RealName'} = shift;
      $UserInfo{'RealName'} = shift;
    $UserInfo{'RealName'} =~ s/\"//g;
      $UserInfo{'RealName'} =~ s/\"//g;
 
   
    my $FoundInExternalDatabase = 0;
      my $FoundInExternalDatabase = 0;
 
   
    $RT::Logger-&gt;info("LookupExternalUserInfo: looking up EmailAddress: '"
      $RT::Logger-&gt;info("LookupExternalUserInfo: looking up EmailAddress: '"
          . $UserInfo{'EmailAddress'} . "', RealName: '"
            . $UserInfo{'EmailAddress'} . "', RealName: '"
          . $UserInfo{'RealName'} . "'" );
            . $UserInfo{'RealName'} . "'" );
 
   
    # Name is the RT username you want to use for this user.
      # Name is the RT username you want to use for this user.
    my %LdapUserInfo = LdapUserFindByMailaddr($UserInfo{'EmailAddress'});
      my %LdapUserInfo = LdapUserFindByMailaddr($UserInfo{'EmailAddress'});
    # The EmailAddress may actually be a username, so if lookup by
      # The EmailAddress may actually be a username, so if lookup by
    # email address fails, try lookup by username
      # email address fails, try lookup by username
    unless($LdapUserInfo{'Name'}) {
      unless($LdapUserInfo{'Name'}) {
        %LdapUserInfo = LdapUserFindByName($UserInfo{'EmailAddress'});
          %LdapUserInfo = LdapUserFindByName($UserInfo{'EmailAddress'});
    }
      }
    if ($LdapUserInfo{'Name'}) {
      if ($LdapUserInfo{'Name'}) {
        $FoundInExternalDatabase = 1;
          $FoundInExternalDatabase = 1;
        $RT::Logger-&gt;info("LookupExternalUserInfo: Mapping '".
          $RT::Logger-&gt;info("LookupExternalUserInfo: Mapping '".
                          $UserInfo{'EmailAddress'} .
                            $UserInfo{'EmailAddress'} .
                          "' to '" .
                            "' to '" .
                          $LdapUserInfo{'Name'} . "'");
                            $LdapUserInfo{'Name'} . "'");
        foreach my $key (keys %LdapUserInfo) {
          foreach my $key (keys %LdapUserInfo) {
            $UserInfo{$key} = $LdapUserInfo{$key};
              $UserInfo{$key} = $LdapUserInfo{$key};
        }
          }
    } else {
      } else {
        $RT::Logger-&gt;info("LookupExternalUserInfo: Fail to find username for '".
          $RT::Logger-&gt;info("LookupExternalUserInfo: Fail to find username for '".
                          $UserInfo{'EmailAddress'}."'");
                            $UserInfo{'EmailAddress'}."'");
    }
      }
 
   
    return ($FoundInExternalDatabase, %UserInfo);
      return ($FoundInExternalDatabase, %UserInfo);
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub CanonicalizeUserInfo
    # {{{ sub CanonicalizeUserInfo
 
   
  sub CanonicalizeUserInfo {
    sub CanonicalizeUserInfo {
      my $self    = shift;
        my $self    = shift;
      my $argsref = shift;
        my $argsref = shift;
      my $success = 1;
        my $success = 1;
 
   
    $RT::Logger-&gt;info("CanonicalizeUserInfo: "
      $RT::Logger-&gt;info("CanonicalizeUserInfo: "
          . "EmailAddress: '" . $argsref-&gt;{'EmailAddress'}
            . "EmailAddress: '" . $argsref-&gt;{'EmailAddress'}
          . "', RealName: '" . $argsref-&gt;{'RealName'} . "'" );
            . "', RealName: '" . $argsref-&gt;{'RealName'} . "'" );
 
   
      my ($UserFoundInExternalDatabase, %ExternalUserInfo) =
        my ($UserFoundInExternalDatabase, %ExternalUserInfo) =
          LookupExternalUserInfo( $argsref-&gt;{'EmailAddress'},
            LookupExternalUserInfo( $argsref-&gt;{'EmailAddress'},
                                  $argsref-&gt;{'RealName'} );
                                    $argsref-&gt;{'RealName'} );
      if ($UserFoundInExternalDatabase) {
        if ($UserFoundInExternalDatabase) {
          for my $key (keys %ExternalUserInfo) {
            for my $key (keys %ExternalUserInfo) {
              $argsref-&gt;{$key} = $ExternalUserInfo{$key};
                $argsref-&gt;{$key} = $ExternalUserInfo{$key};
          }
            }
      }
        }
 
   
      return ($success);
        return ($success);
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub SetPasswordExternal
    # {{{ sub SetPasswordExternal
 
   
  =head2 SetPasswordExternal
    =head2 SetPasswordExternal
 
   
  Takes a string, and try to set this string as the users password in an
    Takes a string, and try to set this string as the users password in an
  external system, if the user is listed in the external system.
    external system, if the user is listed in the external system.
 
   
  Returns 1 if the password was set successfully, undef if it failed,
    Returns 1 if the password was set successfully, undef if it failed,
  and -1 if the user is unknown to the external system.
    and -1 if the user is unknown to the external system.
 
   
  This hook is called from SetPassword.
    This hook is called from SetPassword.
 
   
  =cut
    =cut
 
   
  sub SetPasswordExternal {
    sub SetPasswordExternal {
      my $self    = shift;
        my $self    = shift;
      my $password = shift;
        my $password = shift;
 
   
      # Not allowed to set password for users in LDAP
        # Not allowed to set password for users in LDAP
      if ($RT::LDAPExternalAuth) {
        if ($RT::LDAPExternalAuth) {
          my $ldap = LdapConnect();
            my $ldap = LdapConnect();
          my $mesg;
            my $mesg;
          if ( $mesg = LdapFindUser( $ldap, $self-&gt;Name )
            if ( $mesg = LdapFindUser( $ldap, $self-&gt;Name )
                &amp;&amp; defined $mesg &amp;&amp; $mesg-&gt;count ) {
                &amp;&amp; defined $mesg &amp;&amp; $mesg-&gt;count ) {
              LdapDisconnect($ldap);
                LdapDisconnect($ldap);
              return ( undef,
                return ( undef,
                        $self-&gt;loc("LDAP users must change password in LDAP") );
                        $self-&gt;loc("LDAP users must change password in LDAP") );
          }
            }
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
      }
        }
      return (-1, "No such user in LDAP");
        return (-1, "No such user in LDAP");
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub SetPassword
    # {{{ sub SetPassword
 
   
  =head2 SetPassword
    =head2 SetPassword
 
   
  Takes a string. Checks the string's length and sets this user's password
    Takes a string. Checks the string's length and sets this user's password
  to that string.
    to that string.
 
   
  Override for function in User_Overlay.pm, with modification for LDAP
    Override for function in User_Overlay.pm, with modification for LDAP
  authentication.
    authentication.
 
   
  =cut
    =cut
 
   
  sub SetPassword {
    sub SetPassword {
      my $self    = shift;
        my $self    = shift;
      my $password = shift;
        my $password = shift;
 
   
      unless ( $self-&gt;CurrentUserCanModify('Password') ) {
        unless ( $self-&gt;CurrentUserCanModify('Password') ) {
          return ( 0, $self-&gt;loc('Permission Denied') );
            return ( 0, $self-&gt;loc('Permission Denied') );
      }
        }
 
   
      my ($code, $msg) = $self-&gt;SetPasswordExternal($password);
        my ($code, $msg) = $self-&gt;SetPasswordExternal($password);
      return ($code, $msg) unless (-1 == $code);
        return ($code, $msg) unless (-1 == $code);
 
   
      if ( !$password ) {
        if ( !$password ) {
          return ( 0, $self-&gt;loc("No password set") );
            return ( 0, $self-&gt;loc("No password set") );
      }
        }
      elsif ( length($password) &lt; $RT::MinimumPasswordLength ) {
        elsif ( length($password) &lt; $RT::MinimumPasswordLength ) {
          return ( 0, $self-&gt;loc("Password too short") );
            return ( 0, $self-&gt;loc("Password too short") );
      }
        }
      else {
        else {
          $password = $self-&gt;_GeneratePassword($password);
            $password = $self-&gt;_GeneratePassword($password);
          return ( $self-&gt;SUPER::SetPassword( $password));
            return ( $self-&gt;SUPER::SetPassword( $password));
      }
        }
 
   
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub IsPasswordExternal
    # {{{ sub IsPasswordExternal
 
   
  =head2 IsPasswordExternal
    =head2 IsPasswordExternal
 
   
  Returns true if the passed in value is this user's password.  Return
    Returns true if the passed in value is this user's password.  Return
  undef if the password don't match.  Return -1 if the user is unknown
    undef if the password don't match.  Return -1 if the user is unknown
  in the external system.
    in the external system.
 
   
  This hook is called from IsPassword.
    This hook is called from IsPassword.
 
   
  =cut
    =cut
 
   
  sub IsPasswordExternal {
    sub IsPasswordExternal {
      my $self  = shift;
        my $self  = shift;
      my $value = shift;
        my $value = shift;
          # Let LDAP be authorative for users in LDAP, and only fall
            # Let LDAP be authorative for users in LDAP, and only fall
          # through for users without LDAP entry.
            # through for users without LDAP entry.
          if ($RT::LDAPExternalAuth) {
            if ($RT::LDAPExternalAuth) {
              return IsLdapPassword($self-&gt;Name, $value);
                return IsLdapPassword($self-&gt;Name, $value);
          }
            }
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub IsPassword
    # {{{ sub IsPassword
 
   
  =head2 IsPassword
    =head2 IsPassword
 
   
  Check the users password using LDAP.  Override for function in
    Check the users password using LDAP.  Override for function in
  User_Overlay.pm, with modification for LDAP authentication.
    User_Overlay.pm, with modification for LDAP authentication.
 
   
  =cut
    =cut
 
   
  sub IsPassword {
    sub IsPassword {
          my $self  = shift;
            my $self  = shift;
          my $value = shift;
            my $value = shift;
 
   
          #TODO there isn't any apparent way to legitimately ACL this
            #TODO there isn't any apparent way to legitimately ACL this
 
   
          # RT does not allow null passwords
            # RT does not allow null passwords
          if ( ( !defined($value) ) or ( $value eq '' ) ) {
            if ( ( !defined($value) ) or ( $value eq '' ) ) {
                  return (undef);
                    return (undef);
          }
            }
 
   
          if ( $self-&gt;PrincipalObj-&gt;Disabled ) {
            if ( $self-&gt;PrincipalObj-&gt;Disabled ) {
                  $RT::Logger-&gt;info(
                    $RT::Logger-&gt;info(
                          "Disabled user " . $self-&gt;Name . " tried to log in" );
                            "Disabled user " . $self-&gt;Name . " tried to log in" );
                  return (undef);
                    return (undef);
          }
            }
 
   
          if ( ($self-&gt;__Value('Password') eq '') ||
            if ( ($self-&gt;__Value('Password') eq '') ||
                  ($self-&gt;__Value('Password') eq undef) )  {
                    ($self-&gt;__Value('Password') eq undef) )  {
                  return(undef);
                    return(undef);
          }
            }
 
   
          my $code = $self-&gt;IsPasswordExternal($value);
            my $code = $self-&gt;IsPasswordExternal($value);
          return ($code) unless (-1 == $code);
            return ($code) unless (-1 == $code);
 
   
          # is it an MD5 password
            # is it an MD5 password
          if ($self-&gt;__Value('Password') eq $self-&gt;_GeneratePassword($value)) {
            if ($self-&gt;__Value('Password') eq $self-&gt;_GeneratePassword($value)) {
                  return(1);
                    return(1);
          }
            }
 
   
          # if it's recognized by crypt, we say ok too.
            # if it's recognized by crypt, we say ok too.
          if ($self-&gt;__Value('Password') eq crypt($value,
            if ($self-&gt;__Value('Password') eq crypt($value,
                                                  $self-&gt;__Value('Password'))) {
                                                    $self-&gt;__Value('Password'))) {
              return (1);
                return (1);
          }
            }
 
   
          # no password check has succeeded. get out
            # no password check has succeeded. get out
          return (undef);
            return (undef);
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub LdapUserFindByName
    # {{{ sub LdapUserFindByName
 
   
  =head2 LdapUserFindByName
    =head2 LdapUserFindByName
 
   
  Lookup user owning a given name on OU, returning the
    Lookup user owning a given name on OU, returning the
  username or undef if not known or the search failed.
    username or undef if not known or the search failed.
 
   
  The following configure options are used by this function in addition
    The following configure options are used by this function in addition
  to the ones used by LdapConnect().
    to the ones used by LdapConnect().
 
   
    $RT::LdapNameBase
    $RT::LdapNameBase
    $RT::LdapNameFilter
    $RT::LdapNameFilter
    $RT::LdapNameScope
    $RT::LdapNameScope
    $RT::LdapNameSearchAttr
    $RT::LdapNameSearchAttr
    $RT::LdapNameResultMap
    $RT::LdapNameResultMap
 
   
  For example:
    For example:
 
   
    Set($LdapNameBase,              'dc=mydomain,dc=co,dc=nz');
    Set($LdapNameBase,              'dc=mydomain,dc=co,dc=nz');
    Set($LdapNameFilter,            '(objectClass=user)');
    Set($LdapNameFilter,            '(objectClass=user)');
    Set($LdapNameScope,            'sub');
    Set($LdapNameScope,            'sub');
    Set($LdapNameSearchAttr,        'sAMAccountName');
    Set($LdapNameSearchAttr,        'sAMAccountName');
    %RT::LdapNameResultMap = (
    %RT::LdapNameResultMap = (
          'sAMAccountName'        =&gt; 'Name',
            'sAMAccountName'        =&gt; 'Name',
          'mail'                  =&gt; 'EmailAddress',
            'mail'                  =&gt; 'EmailAddress',
          'cn'                    =&gt; 'RealName',
            'cn'                    =&gt; 'RealName',
          );
            );
 
   
 
   
  =cut
    =cut
 
   
  # Example search
    # Example search
  #  ldapsearch -x -b ou=mail,dc=uio,dc=no -ZZ -h ldap.uio.no -D uid=pre,ou=users,dc=uio,dc=no -W target=mathiasm
    #  ldapsearch -x -b ou=mail,dc=uio,dc=no -ZZ -h ldap.uio.no -D uid=pre,ou=users,dc=uio,dc=no -W target=mathiasm
 
   
  sub LdapUserFindByName {
    sub LdapUserFindByName {
      my $name = shift;
        my $name = shift;
      my %UserInfo = ();
        my %UserInfo = ();
      $ldap = LdapConnect();
        $ldap = LdapConnect();
      my $filter = "(&amp;($RT::LdapNameSearchAttr=$name)$RT::LdapNameFilter)";
        my $filter = "(&amp;($RT::LdapNameSearchAttr=$name)$RT::LdapNameFilter)";
      my @attr = keys %RT::LdapNameResultMap;
        my @attr = keys %RT::LdapNameResultMap;
      $RT::Logger-&gt;info( "LdapUserFindByName: Looking for ",
        $RT::Logger-&gt;info( "LdapUserFindByName: Looking for ",
                              join(" ", @attr), " filter=", $filter );
                              join(" ", @attr), " filter=", $filter );
      $mesg = $ldap-&gt;search(
        $mesg = $ldap-&gt;search(
                            base      =&gt; $RT::LdapNameBase,
                              base      =&gt; $RT::LdapNameBase,
                            scope      =&gt; $RT::LdapNameScope,
                              scope      =&gt; $RT::LdapNameScope,
                            filter    =&gt; $filter,
                              filter    =&gt; $filter,
                            attributes =&gt; [@attr],
                              attributes =&gt; [@attr],
                            );
                              );
      if ( ($mesg-&gt;code != LDAP_SUCCESS) and
        if ( ($mesg-&gt;code != LDAP_SUCCESS) and
            ($mesg-&gt;code != LDAP_PARTIAL_RESULTS) ) {
            ($mesg-&gt;code != LDAP_PARTIAL_RESULTS) ) {
          $RT::Logger-&gt;critical("LdapUserFindByName: Search failed: ",
            $RT::Logger-&gt;critical("LdapUserFindByName: Search failed: ",
                                "retval=", $mesg-&gt;code, " ",
                                  "retval=", $mesg-&gt;code, " ",
                                ldap_error_name($mesg-&gt;code));
                                  ldap_error_name($mesg-&gt;code));
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
          return undef;
            return undef;
      }
        }
 
   
      if (1 != $mesg-&gt;count) {
        if (1 != $mesg-&gt;count) {
          $RT::Logger-&gt;critical("LdapUserFindByName: Search failed: ",
            $RT::Logger-&gt;critical("LdapUserFindByName: Search failed: ",
                                "\$mesg-&gt;count=", $mesg-&gt;count);
                                  "\$mesg-&gt;count=", $mesg-&gt;count);
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
          return undef;
            return undef;
      }
        }
 
   
      while( my $entry = $mesg-&gt;shift_entry) {
        while( my $entry = $mesg-&gt;shift_entry) {
          foreach my $attr (keys %RT::LdapNameResultMap) {
            foreach my $attr (keys %RT::LdapNameResultMap) {
              foreach my $value ($entry-&gt;get_value($attr)) {
                foreach my $value ($entry-&gt;get_value($attr)) {
                  $UserInfo{$RT::LdapNameResultMap{$attr}} = $value;
                    $UserInfo{$RT::LdapNameResultMap{$attr}} = $value;
              }
                }
          }
            }
      }
        }
      LdapDisconnect($ldap);
        LdapDisconnect($ldap);
      return %UserInfo;
        return %UserInfo;
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub LdapUserFindByMailaddr
    # {{{ sub LdapUserFindByMailaddr
 
   
  =head2 LdapUserFindByMailaddr
    =head2 LdapUserFindByMailaddr
 
   
  Lookup user owning a given email address on UiO, returning the
    Lookup user owning a given email address on UiO, returning the
  username or undef if not known or the search failed.
    username or undef if not known or the search failed.
 
   
  The following configure options are used by this function in addition
    The following configure options are used by this function in addition
  to the ones used by LdapConnect().
    to the ones used by LdapConnect().
 
   
    $RT::LdapMailBase
    $RT::LdapMailBase
    $RT::LdapMailFilter
    $RT::LdapMailFilter
    $RT::LdapMailScope
    $RT::LdapMailScope
    $RT::LdapMailSearchAttr
    $RT::LdapMailSearchAttr
    $RT::LdapMailMap
    $RT::LdapMailMap
 
   
  =cut
    =cut
 
   
  # Example search
    # Example search
  #  ldapsearch -x -b ou=mail,dc=uio,dc=no -ZZ -h ldap.uio.no -D uid=pre,ou=users,dc=uio,dc=no -W target=mathiasm
    #  ldapsearch -x -b ou=mail,dc=uio,dc=no -ZZ -h ldap.uio.no -D uid=pre,ou=users,dc=uio,dc=no -W target=mathiasm
 
   
  sub LdapUserFindByMailaddr {
    sub LdapUserFindByMailaddr {
      my $mailaddr = shift;
        my $mailaddr = shift;
      my %UserInfo = ();
        my %UserInfo = ();
      $ldap = LdapConnect();
        $ldap = LdapConnect();
      my $filter = "(&amp;($RT::LdapMailSearchAttr=$mailaddr)$RT::LdapMailFilter)";
        my $filter = "(&amp;($RT::LdapMailSearchAttr=$mailaddr)$RT::LdapMailFilter)";
      my @attr = keys %RT::LdapMailResultMap;
        my @attr = keys %RT::LdapMailResultMap;
      $RT::Logger-&gt;info( "LdapUserFindByMailaddr: Looking for ",
        $RT::Logger-&gt;info( "LdapUserFindByMailaddr: Looking for ",
                              join(" ", @attr), " filter=", $filter );
                              join(" ", @attr), " filter=", $filter );
      $mesg = $ldap-&gt;search(
        $mesg = $ldap-&gt;search(
                            base      =&gt; $RT::LdapMailBase,
                              base      =&gt; $RT::LdapMailBase,
                            scope      =&gt; $RT::LdapMailScope,
                              scope      =&gt; $RT::LdapMailScope,
                            filter    =&gt; $filter,
                              filter    =&gt; $filter,
                            attributes =&gt; [@attr],
                              attributes =&gt; [@attr],
                            );
                              );
      if ( ($mesg-&gt;code != LDAP_SUCCESS) and
        if ( ($mesg-&gt;code != LDAP_SUCCESS) and
            ($mesg-&gt;code != LDAP_PARTIAL_RESULTS) ) {
            ($mesg-&gt;code != LDAP_PARTIAL_RESULTS) ) {
          $RT::Logger-&gt;critical("LdapUserFindByMailaddr: Search failed: ",
            $RT::Logger-&gt;critical("LdapUserFindByMailaddr: Search failed: ",
                                "retval=", $mesg-&gt;code, " ",
                                  "retval=", $mesg-&gt;code, " ",
                                ldap_error_name($mesg-&gt;code));
                                  ldap_error_name($mesg-&gt;code));
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
          return undef;
            return undef;
      }
        }
 
   
      if (1 != $mesg-&gt;count) {
        if (1 != $mesg-&gt;count) {
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
          return undef;
            return undef;
      }
        }
 
   
      while( my $entry = $mesg-&gt;shift_entry) {
        while( my $entry = $mesg-&gt;shift_entry) {
          foreach my $attr (keys %RT::LdapMailResultMap) {
            foreach my $attr (keys %RT::LdapMailResultMap) {
              foreach my $value ($entry-&gt;get_value($attr)) {
                foreach my $value ($entry-&gt;get_value($attr)) {
                  $UserInfo{$RT::LdapMailResultMap{$attr}} = $value;
                    $UserInfo{$RT::LdapMailResultMap{$attr}} = $value;
              }
                }
          }
            }
      }
        }
      LdapDisconnect($ldap);
        LdapDisconnect($ldap);
      return %UserInfo;
        return %UserInfo;
  }
    }
 
   
  # {{{ sub LdapConnect
    # {{{ sub LdapConnect
 
   
  =head2 LdapConnect
    =head2 LdapConnect
 
   
  Connect to the LDAP databsae.
    Connect to the LDAP databsae.
 
   
  The following configure options are used by this function:
    The following configure options are used by this function:
 
   
    $RT::LdapServer
      $RT::LdapServer
    $RT::LdapUser
      $RT::LdapUser
    $RT::LdapPass
      $RT::LdapPass
 
   
  =cut
    =cut
 
   
  sub LdapConnect {
    sub LdapConnect {
      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);
 
   
      my $mesg;
        my $mesg;
      my $ldap = Net::LDAP-&gt;new($RT::LdapServer,
        my $ldap = Net::LDAP-&gt;new($RT::LdapServer,
                                version =&gt; 3);
                                  version =&gt; 3);
 
   
      unless ($ldap) {
        unless ($ldap) {
          $RT::Logger-&gt;critical("IsLdapPassword: Cannot connect to",
            $RT::Logger-&gt;critical("IsLdapPassword: Cannot connect to",
                                "LDAP server ", $RT::LdapServer);
                                  "LDAP server ", $RT::LdapServer);
          return undef;
            return undef;
      }
        }
 
   
      # I seem to have problems if I try and bind with a NULL username
        # I seem to have problems if I try and bind with a NULL username
      # by hand So this now checks to see if we are really going to bind
        # by hand So this now checks to see if we are really going to bind
      # with a username.
        # with a username.
      if (defined($RT::LdapUser) &amp;&amp; $RT::LdapUser ne '') {
        if (defined($RT::LdapUser) &amp;&amp; $RT::LdapUser ne '') {
          $mesg = $ldap-&gt;bind($RT::LdapUser,
            $mesg = $ldap-&gt;bind($RT::LdapUser,
                              password =&gt; $RT::LdapPass );
                                password =&gt; $RT::LdapPass );
      } else {
        } else {
          # This bind is redundant with LDAP protocol version 3
            # This bind is redundant with LDAP protocol version 3
          $mesg = $ldap-&gt;bind;
            $mesg = $ldap-&gt;bind;
      }
        }
      if ($mesg-&gt;code != LDAP_SUCCESS) {
        if ($mesg-&gt;code != LDAP_SUCCESS) {
          $RT::Logger-&gt;critical("IsLdapPassword: Cannot bind to LDAP: ",
            $RT::Logger-&gt;critical("IsLdapPassword: Cannot bind to LDAP: ",
                                "retval=", $mesg-&gt;code, " ",
                                  "retval=", $mesg-&gt;code, " ",
                                ldap_error_name($mesg-&gt;code));
                                  ldap_error_name($mesg-&gt;code));
          return undef;
            return undef;
      }
        }
      return $ldap;
        return $ldap;
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub LdapDisconnect
    # {{{ sub LdapDisconnect
 
   
  =head2 LdapDisconnect
    =head2 LdapDisconnect
 
   
  Disconnect from the LDAP database.
    Disconnect from the LDAP database.
 
   
  =cut
    =cut
 
   
  sub LdapDisconnect {
    sub LdapDisconnect {
      my $ldap = shift;
        my $ldap = shift;
      my $mesg = $ldap-&gt;unbind();
        my $mesg = $ldap-&gt;unbind();
      if ($mesg-&gt;code != LDAP_SUCCESS) {
        if ($mesg-&gt;code != LDAP_SUCCESS) {
          $RT::Logger-&gt;critical("LdapDisconnect: unbind failed: ",
            $RT::Logger-&gt;critical("LdapDisconnect: unbind failed: ",
                                "retval=", $mesg-&gt;code, " ",
                                  "retval=", $mesg-&gt;code, " ",
                                ldap_error_name($mesg-&gt;code));
                                  ldap_error_name($mesg-&gt;code));
      }
        }
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub LdapFindUser
    # {{{ sub LdapFindUser
 
   
  =head2 LdapFindUser
    =head2 LdapFindUser
 
   
  Locate info on a giver user given the username.
    Locate info on a giver user given the username.
 
   
  Configure options used by this function:
    Configure options used by this function:
 
   
    $RT::LdpaAuthBase
      $RT::LdpaAuthBase
    $RT::LdpaAuthFilter
      $RT::LdpaAuthFilter
    $RT::LdpaAuthUidAttr
      $RT::LdpaAuthUidAttr
 
   
  =cut
    =cut
 
   
  sub LdapFindUser {
    sub LdapFindUser {
      my $ldap = shift;
        my $ldap = shift;
      my $username = shift;
        my $username = shift;
 
   
    $RT::Logger-&gt;info("LdapFindUser: "
      $RT::Logger-&gt;info("LdapFindUser: "
          . "ldap: '" . $ldap
            . "ldap: '" . $ldap
          . "', username: '" . $username . "'" );
            . "', username: '" . $username . "'" );
 
   
      my $filter;
        my $filter;
      if ($RT::LdapAuthFilter) {
        if ($RT::LdapAuthFilter) {
          $filter = "(&amp;(" .$RT::LdapAuthUidAttr . "=$username)$RT::LdapAuthFilter)";
            $filter = "(&amp;(" .$RT::LdapAuthUidAttr . "=$username)$RT::LdapAuthFilter)";
      } else {
        } else {
          $filter = "(" .$RT::LdapAuthUidAttr . "=$username)";
            $filter = "(" .$RT::LdapAuthUidAttr . "=$username)";
      }
        }
 
   
      $RT::Logger-&gt;debug("IsLdapPassword: First search filter '$filter'");
        $RT::Logger-&gt;debug("IsLdapPassword: First search filter '$filter'");
      my $mesg = $ldap-&gt;search(base  =&gt; $RT::LdapAuthBase,
        my $mesg = $ldap-&gt;search(base  =&gt; $RT::LdapAuthBase,
                                filter =&gt; $filter,
                                filter =&gt; $filter,
                                attrs  =&gt; ['dn']);
                                attrs  =&gt; ['dn']);
      if (!(($mesg-&gt;code == LDAP_SUCCESS) or
        if (!(($mesg-&gt;code == LDAP_SUCCESS) or
            ($mesg-&gt;code == LDAP_PARTIAL_RESULTS)))
              ($mesg-&gt;code == LDAP_PARTIAL_RESULTS)))
      {
        {
          $RT::Logger-&gt;debug("IsLdapPassword: Could not search for $filter: ",
            $RT::Logger-&gt;debug("IsLdapPassword: Could not search for $filter: ",
                              "retval=", $mesg-&gt;code, " ",
                              "retval=", $mesg-&gt;code, " ",
                              ldap_error_name($mesg-&gt;code));
                              ldap_error_name($mesg-&gt;code));
          return undef;
            return undef;
      }
        }
      return $mesg;
        return $mesg;
  }
    }
 
   
  # }}}
    # }}}
 
   
  # {{{ sub IsLdapPassword
    # {{{ sub IsLdapPassword
 
   
  =head2 IsLdapPassword
    =head2 IsLdapPassword
 
   
  Takes a username and password as argument, and check if the password
    Takes a username and password as argument, and check if the password
  is correct for the given user.  Return undef if password check failed,
    is correct for the given user.  Return undef if password check failed,
  -1 if the user is unknown, and 1 if the password check succeeded.
    -1 if the user is unknown, and 1 if the password check succeeded.
 
   
  =cut
    =cut
 
   
  sub IsLdapPassword {
    sub IsLdapPassword {
      my $username = shift;
        my $username = shift;
      my $value    = shift;
        my $value    = shift;
 
   
      $RT::Logger-&gt;debug("IsLdapPassword: executing");
        $RT::Logger-&gt;debug("IsLdapPassword: executing");
      my $ldap = LdapConnect();
        my $ldap = LdapConnect();
      return undef unless $ldap;
        return undef unless $ldap;
 
   
      my $mesg = LdapFindUser($ldap, $username);
        my $mesg = LdapFindUser($ldap, $username);
      unless ($mesg) {
        unless ($mesg) {
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
          return undef;
            return undef;
      }
        }
      $RT::Logger-&gt;debug("IsLdapPassword: First search produced ",
        $RT::Logger-&gt;debug("IsLdapPassword: First search produced ",
                          $mesg-&gt;count, " results");
                          $mesg-&gt;count, " results");
      if (! $mesg-&gt;count)
        if (! $mesg-&gt;count)
      {
        {
          $RT::Logger-&gt;info("IsLdapPassword: AUTH FAILED $username");
            $RT::Logger-&gt;info("IsLdapPassword: AUTH FAILED $username");
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
          return -1;
            return -1;
      }
        }
      $ldap-&gt;start_tls( verify =&gt; 'require',
        $ldap-&gt;start_tls( verify =&gt; 'require',
                        cafile =&gt; $RT::LdapCAFile ) if ($RT::LdapAuthStartTLS);
                          cafile =&gt; $RT::LdapCAFile ) if ($RT::LdapAuthStartTLS);
 
   
      my $userdn = $mesg-&gt;first_entry-&gt;dn;
        my $userdn = $mesg-&gt;first_entry-&gt;dn;
      $RT::Logger-&gt;debug("IsLdapPassword: Trying to bind using DN=$userdn");
        $RT::Logger-&gt;debug("IsLdapPassword: Trying to bind using DN=$userdn");
      my $mesg2 = $ldap-&gt;bind($userdn,
        my $mesg2 = $ldap-&gt;bind($userdn,
                              password =&gt; $value );
                                password =&gt; $value );
      if ($mesg2-&gt;code != LDAP_SUCCESS) {
        if ($mesg2-&gt;code != LDAP_SUCCESS) {
          $RT::Logger-&gt;critical("IsLdapPassword: Unable to bind as $userdn: ",
            $RT::Logger-&gt;critical("IsLdapPassword: Unable to bind as $userdn: ",
                                "retval=", $mesg2-&gt;code, " ",
                                  "retval=", $mesg2-&gt;code, " ",
                                ldap_error_name($mesg2-&gt;code));
                                  ldap_error_name($mesg2-&gt;code));
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
          return undef;
            return undef;
      }
        }
      else
        else
      {
        {
          $RT::Logger-&gt;info("IsLdapPassword: AUTH OK $username ($userdn) base:",
            $RT::Logger-&gt;info("IsLdapPassword: AUTH OK $username ($userdn) base:",
                            $RT::LdapAuthBase);
                              $RT::LdapAuthBase);
          LdapDisconnect($ldap);
            LdapDisconnect($ldap);
          return 1;
            return 1;
      }
        }
  }
    }
 
   
  # }}}
    # }}}
 
   
  1;
    1;
 
   
  </nowiki>
    </nowiki>

Revision as of 07:13, 22 May 2015

NTLM Authentication - a.k.a. Single sign-on

RT 3.4.6 on Apache 2.2 with mod_perl on FC4

Here are some notes from my setup of RT 3.4.6, running on Apache 2.2 with mod_perl on FC4.

I followed the excellent guidance provided by Nathan Mehl at http://blank.org/memory/output/rt-ad-sso.html.

First, I installed Apache 2, mod_perl and RT 3.4.6 and got it all working, then...

Install mod_ntlm2

Downloaded mod_ntlm2-0.1.tgz from http://modntlm.sourceforge.net/

cd /usr/local/src
gunzip <mod_ntlm2-0.1.tgz | tar -xvf -
cd mod_ntlm2-0.1
PATH=$PATH:/usr/local/apache2/bin

There are some declarations in smbval/smblib.inc.c that the compiler doesn't like, so change it as follows:

diff -r mod_ntlm2-0.1/smbval/smblib.inc.c mod_ntlm2-0.1-fixed/smbval/smblib.inc.c
25,26c25,26
< static int SMBlib_errno;
< static int SMBlib_SMB_Error;
---
> int SMBlib_errno;
> int SMBlib_SMB_Error;
35c35
< static SMB_State_Types SMBlib_State;
---
> SMB_State_Types SMBlib_State;


If you don't do this, you may see errors like the following when you compile:

In file included from mod_ntlm.c:107:
smbval/smblib.inc.c: At top level:
smbval/smblib.inc.c:25: error: static declaration of 'SMBlib_errno' follows non-static declaration
smbval/smblib-priv.h:668: error: previous declaration of 'SMBlib_errno' was here
smbval/smblib.inc.c:26: error: static declaration of 'SMBlib_SMB_Error' follows non-static declaration
smbval/smblib-priv.h:669: error: previous declaration of 'SMBlib_SMB_Error' was here
smbval/smblib.inc.c:35: error: static declaration of 'SMBlib_State' follows non-static declaration
smbval/smblib-priv.h:665: error: previous declaration of 'SMBlib_State' was here


Also, it seems APXS (or the GNU libtools) have changed since the Makefile was written, so change Makefile as follows:

diff -r mod_ntlm2-0.1/Makefile mod_ntlm2-0.1-fixed/Makefile
20c20
<       $(APXS) -i -a -n 'ntlm' mod_ntlm.so
---
>       $(APXS) -i -a -n 'ntlm' mod_ntlm.la
diff -r mod_ntlm2-0.1/mod_ntlm.c mod_ntlm2-0.1-fixed/mod_ntlm.c
590c590,596
<     apr_pool_sub_make(&sp,p,NULL);
---
>     /*
>      * apr_pool_sub_make(&sp,p,NULL);
>      *
>      * This function call is not longer available with apache 2.2
>      * Try replacing it with apr_pool_create_ex()
>      */
>     apr_pool_create_ex(&sp,p,NULL,NULL);

If you don't, you may see errors like the following when you try to make the package:

cp mod_ntlm.so /usr/local/apache2/modules/mod_ntlm.so
cp: cannot stat `mod_ntlm.so': No such file or directory
apxs:Error: Command failed with rc=65536


Finally, I had to change mod_ntlm.c to work with Apache 2.2:

diff mod_ntlm2-0.1/mod_ntlm.c mod_ntlm2-0.1-fixed/mod_ntlm.c
590c590,596
<     apr_pool_sub_make(&sp,p,NULL);
---
>     /*
>      * apr_pool_sub_make(&sp,p,NULL);
>      *
>      * This function call is not longer available with apache 2.2
>      * Try replacing it with apr_pool_create_ex()
>      */
>     apr_pool_create_ex(&sp,p,NULL,NULL);


Without this change, you may see errors like the following when you stop Apache (after installing and configuring, which follows):

httpd: Syntax error on line 55 of /usr/local/apache2/conf/httpd.conf: Cannot load /usr/local/apache2/modules/mod_ntlm.so into server: /usr/local/apache2/modules/mod_ntlm.so: undefined symbol: apr_pool_sub_make


Finally, you should be ready to make and install the package...

make
make install

Configure RT

Add to /opt/rt3/etc/RT_SiteConfig.pm:

Set($WebExternalAuth , '1');
Set($WebFallbackToInternalAuth , '1');
Set($WebExternalGecos , undef);
Set($WebExternalAuto , '1');

Change VirtualHost definition in /usr/local/apache2/conf/extra/httpd-vhosts.conf:

<VirtualHost *:80>
        ServerName rt.mydomain.com
        AddDefaultCharset UTF-8
        DocumentRoot /opt/rt3/share/html
    
        <Directory "/opt/rt3/share/html">
            AuthName "Request Tracker"
            AuthType NTLM
            NTLMAuth on
            NTLMAuthoritative on
            NTLMDomain lhl.co.nz
            NTLMServer dc1.mydomain.com
            NTLMBackup dc2.mydomain.com
            require valid-user
        </Directory>
    
    #    RedirectMatch permanent (.*)/$ http://rt.mydomain.com/$1/index.html
    
        # this line applies to Apache2+mod_perl2 only
        # Below line might be incorrect, I had to use:
        #     PerlModule Apache2::compat
        # mod_perl 2.0.1 from FC4 Linux
        #PerlModule Apache2 Apache::compat
        PerlModule Apache2::compat
    
        PerlModule Apache::DBI
        PerlRequire /opt/rt3/bin/webmux.pl
    
        <Location />
               SetHandler perl-script
               PerlHandler RT::Mason
        </Location>
    
            ErrorLog logs/rt.lhl.co.nz-error_log
            CustomLog logs/rt.lhl.co.nz-access_log common
    
    </VirtualHost>
    
    
    

Create /opt/rt3/lib/User_Local.pm as:

# BEGIN LICENSE BLOCK
    #
    # Copyright (c) 2004 Petter Reinholdtsen <pere@hungry.com>
    #
    # (Except where explictly superceded by other copyright notices)
    #
    # This work is made available to you under the terms of Version 2 of
    # the GNU General Public License. A copy of that license should have
    # been provided with this software, but in any event can be snarfed
    # from www.gnu.org.
    #
    # This work is distributed in the hope that it will be useful, but
    # WITHOUT ANY WARRANTY; without even the implied warranty of
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    # General Public License for more details.
    #
    # Unless otherwise specified, all modifications, corrections or
    # extensions to this work which alter its source code become the
    # property of Best Practical Solutions, LLC when submitted for
    # inclusion in the work.
    #
    #
    # END LICENSE BLOCK
    
    
    # LDAP integration in RT 3.  These overrides provide LDAP
    # authentication and user info syncronizing.
    #
    # Written by Petter Reinholdtsen <pere@hungry.com> based on Code from
    # Marcelo Bartsch <bartschm_cl@hotmail.com>, Stewart James
    # <stewart.james@vu.edu.au> and Carl Makin <carl@xena.IPAustralia.gov.au>.
    #
    # Copy this file into rt3/local/lib/RT/User_Local.pm to active it.
    
    
    # Modification Originally by Marcelo Bartsch <bartschm_cl@hotmail.com>
    # Update by Stewart James <stewart.james@vu.edu.au for rt3.
    # Update with TLS support and more flexible LDAP code by Petter Reinholdtsen.
    # Drop this file in /opt/rt3/lib/RT/User_Local.pm
    # Drop something like below in yout RT_SiteConfig.pm
    #
    # Set($LDAPExternalAuth, 1); # Enable LDAP auth
    # Set($LdapServer, "ldap.domain.com");
    # Set($LdapCAFile, "/site/w3-sertifikater/w3_cacert.pem");
    # Set($LdapAuthStartTLS, 1); # Need to use TLS or ldaps to check passwords
    # Set($LdapUser, ""); # Can search without username and password
    # Set($LdapAuthPass, "");
    # Set($LdapAuthBase, "ou=users,dc=domain,dc=com");
    # Set($LdapAuthUidAttr, "uid");
    # Set($LdapAuthFilter, "(objectclass=posixAccount)");
    
    
    no warnings qw(redefine);
    
    # {{{ sub LookupExternalUserInfo
    
    =item LookupExternalUserInfo
    
     LookupExternalUserInfo is a site-definable method for synchronizing
     incoming users with an external data source.
    
     This routine takes a tuple of EmailAddress and FriendlyName
       EmailAddress is the user's email address, ususally taken from
           an email message's From: header.
       RealName is a freeform string, ususally taken from the "comment"
           portion of an email message's From: header.
    
     It returns (FoundInExternalDatabase, ParamHash);
    
       FoundInExternalDatabase must be set to 1 before return if the user
       was found in the external database.
    
       ParamHash is a Perl parameter hash which can contain at least the
       following fields. These fields are used to populate RT's users
       database when the user is created
    
         EmailAddress is the email address that RT should use for this user.
         Name is the 'Name' attribute RT should use for this user.
             'Name' is used for things like access control and user lookups.
         RealName is what RT should display as the user's name when displaying
             'friendly' names
    
    =cut
    
    sub LookupExternalUserInfo {
      my %UserInfo = ();
      $UserInfo{'EmailAddress'} = shift;
      $UserInfo{'RealName'} = shift;
      $UserInfo{'RealName'} =~ s/\"//g;
    
      my $FoundInExternalDatabase = 0;
    
      # Name is the RT username you want to use for this user.
      my %LdapUserInfo = LdapUserFindByMailaddr($UserInfo{'EmailAddress'});
      if ($LdapUserInfo{'Name'}) {
          $FoundInExternalDatabase = 1;
          $RT::Logger->info("LookupExternalUserInfo: Mapping '".
                            $UserInfo{'EmailAddress'} .
                            "' to '" .
                            $LdapUserInfo{'Name'} . "'");
          foreach my $key (keys %LdapUserInfo) {
              $UserInfo{$key} = $LdapUserInfo{$key};
          }
      } else {
          $RT::Logger->info("LookupExternalUserInfo: Fail to find username for '".
                            $UserInfo{'EmailAddress'}."'");
      }
    
      return ($FoundInExternalDatabase, %UserInfo);
    }
    
    # }}}
    
    # {{{ sub CanonicalizeUserInfo
    
    sub CanonicalizeUserInfo {
        my $self    = shift;
        my $argsref = shift;
        my $success = 1;
    
        my ($UserFoundInExternalDatabase, %ExternalUserInfo) =
            LookupExternalUserInfo( $argsref->{'EmailAddress'},
                                    $argsref->{'RealName'} );
        if ($UserFoundInExternalDatabase) {
            for my $key (keys %ExternalUserInfo) {
                $argsref->{$key} = $ExternalUserInfo{$key};
            }
        }
    
        return ($success);
    }
    
    # }}}
    
    # {{{ sub SetPasswordExternal
    
    =head2 SetPasswordExternal
    
    Takes a string, and try to set this string as the users password in an
    external system, if the user is listed in the external system.
    
    Returns 1 if the password was set successfully, undef if it failed,
    and -1 if the user is unknown to the external system.
    
    This hook is called from SetPassword.
    
    =cut
    
    sub SetPasswordExternal {
        my $self     = shift;
        my $password = shift;
    
        # Not allowed to set password for users in LDAP
        if ($RT::LDAPExternalAuth) {
            my $ldap = LdapConnect();
            my $mesg;
            if ( $mesg = LdapFindUser( $ldap, $self->Name )
                 && defined $mesg && $mesg->count ) {
                LdapDisconnect($ldap);
                return ( undef,
                         $self->loc("LDAP users must change password in LDAP") );
            }
            LdapDisconnect($ldap);
        }
        return (-1, "No such user in LDAP");
    }
    
    # }}}
    
    # {{{ sub SetPassword
    
    =head2 SetPassword
    
    Takes a string. Checks the string's length and sets this user's password
    to that string.
    
    Override for function in User_Overlay.pm, with modification for LDAP
    authentication.
    
    =cut
    
    sub SetPassword {
        my $self     = shift;
        my $password = shift;
    
        unless ( $self->CurrentUserCanModify('Password') ) {
            return ( 0, $self->loc('Permission Denied') );
        }
    
        my ($code, $msg) = $self->SetPasswordExternal($password);
        return ($code, $msg) unless (-1 == $code);
    
        if ( !$password ) {
            return ( 0, $self->loc("No password set") );
        }
        elsif ( length($password) < $RT::MinimumPasswordLength ) {
            return ( 0, $self->loc("Password too short") );
        }
        else {
            $password = $self->_GeneratePassword($password);
            return ( $self->SUPER::SetPassword( $password));
        }
    
    }
    
    # }}}
    
    # {{{ sub IsPasswordExternal
    
    =head2 IsPasswordExternal
    
    Returns true if the passed in value is this user's password.  Return
    undef if the password don't match.  Return -1 if the user is unknown
    in the external system.
    
    This hook is called from IsPassword.
    
    =cut
    
    sub IsPasswordExternal {
        my $self  = shift;
        my $value = shift;
            # Let LDAP be authorative for users in LDAP, and only fall
            # through for users without LDAP entry.
            if ($RT::LDAPExternalAuth) {
                return IsLdapPassword($self->Name, $value);
            }
    }
    
    # }}}
    
    # {{{ sub IsPassword
    
    =head2 IsPassword
    
    Check the users password using LDAP.  Override for function in
    User_Overlay.pm, with modification for LDAP authentication.
    
    =cut
    
    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) ) or ( $value eq '' ) ) {
                    return (undef);
            }
    
            if ( $self->PrincipalObj->Disabled ) {
                    $RT::Logger->info(
                            "Disabled user " . $self->Name . " tried to log in" );
                    return (undef);
            }
    
            if ( ($self->__Value('Password') eq '') ||
                    ($self->__Value('Password') eq undef) )  {
                    return(undef);
            }
    
            my $code = $self->IsPasswordExternal($value);
            return ($code) unless (-1 == $code);
    
            # is it an MD5 password
            if ($self->__Value('Password') eq $self->_GeneratePassword($value)) {
                    return(1);
            }
    
            # if it's recognized by crypt, we say ok too.
            if ($self->__Value('Password') eq crypt($value,
                                                    $self->__Value('Password'))) {
                return (1);
            }
    
            # no password check has succeeded. get out
            return (undef);
    }
    
    # }}}
    
    # {{{ sub LdapUserFindByMailaddr
    
    =head2 LdapUserFindByMailaddr
    
    Lookup user owning a given email address on UiO, returning the
    username or undef if not known or the search failed.
    
    The following configure options are used by this function in addition
    to the ones used by LdapConnect().
    
     $RT::LdapMailBase
     $RT::LdapMailFilter
     $RT::LdapMailScope
     $RT::LdapMailSearchAttr
     $RT::LdapMailMap
    
    =cut
    
    # Example search
    #   ldapsearch -x -b ou=mail,dc=uio,dc=no -ZZ -h ldap.uio.no -D uid=pre,ou=users,dc=uio,dc=no -W target=mathiasm
    
    sub LdapUserFindByMailaddr {
        my $mailaddr = shift;
        my %UserInfo = ();
        $ldap = LdapConnect();
        my $filter = "(&($RT::LdapMailSearchAttr=$mailaddr)$RT::LdapMailFilter)";
        my @attr = keys %RT::LdapMailResultMap;
        $RT::Logger->info( "LdapUserFindByMailaddr: Looking for ",
                               join(" ", @attr), " filter=", $filter );
        $mesg = $ldap->search(
                              base       => $RT::LdapMailBase,
                              scope      => $RT::LdapMailScope,
                              filter     => $filter,
                              attributes => [@attr],
                              );
        if ( ($mesg->code != LDAP_SUCCESS) and
             ($mesg->code != LDAP_PARTIAL_RESULTS) ) {
            $RT::Logger->critical("LdapUserFindByMailaddr: Search failed: ",
                                  "retval=", $mesg->code, " ",
                                  ldap_error_name($mesg->code));
            LdapDisconnect($ldap);
            return undef;
        }
    
        if (1 != $mesg->count) {
            LdapDisconnect($ldap);
            return undef;
        }
    
        while( my $entry = $mesg->shift_entry) {
            foreach my $attr (keys %RT::LdapMailResultMap) {
                foreach my $value ($entry->get_value($attr)) {
                    $UserInfo{$RT::LdapMailResultMap{$attr}} = $value;
                }
            }
        }
        LdapDisconnect($ldap);
        return %UserInfo;
    }
    
    # {{{ sub LdapConnect
    
    =head2 LdapConnect
    
    Connect to the LDAP databsae.
    
    The following configure options are used by this function:
    
      $RT::LdapServer
      $RT::LdapUser
      $RT::LdapPass
    
    =cut
    
    sub LdapConnect {
        use Net::LDAP qw(LDAP_SUCCESS LDAP_PARTIAL_RESULTS);
        use Net::LDAP::Util qw (ldap_error_name);
    
        my $mesg;
        my $ldap = Net::LDAP->new($RT::LdapServer,
                                  version => 3);
    
        unless ($ldap) {
            $RT::Logger->critical("IsLdapPassword: Cannot connect to",
                                  "LDAP server ", $RT::LdapServer);
            return undef;
        }
    
        # I seem to have problems if I try and bind with a NULL username
        # by hand So this now checks to see if we are really going to bind
        # with a username.
        if (defined($RT::LdapUser) && $RT::LdapUser ne '') {
            $mesg = $ldap->bind($RT::LdapUser,
                                password => $RT::LdapPass );
        } else {
            # This bind is redundant with LDAP protocol version 3
            $mesg = $ldap->bind;
        }
        if ($mesg->code != LDAP_SUCCESS) {
            $RT::Logger->critical("IsLdapPassword: Cannot bind to LDAP: ",
                                  "retval=", $mesg->code, " ",
                                  ldap_error_name($mesg->code));
            return undef;
        }
        return $ldap;
    }
    
    # }}}
    
    # {{{ sub LdapDisconnect
    
    =head2 LdapDisconnect
    
    Disconnect from the LDAP database.
    
    =cut
    
    sub LdapDisconnect {
        my $ldap = shift;
        my $mesg = $ldap->unbind();
        if ($mesg->code != LDAP_SUCCESS) {
            $RT::Logger->critical("LdapDisconnect: unbind failed: ",
                                  "retval=", $mesg->code, " ",
                                  ldap_error_name($mesg->code));
        }
    }
    
    # }}}
    
    # {{{ sub LdapFindUser
    
    =head2 LdapFindUser
    
    Locate info on a giver user given the username.
    
    Configure options used by this function:
    
      $RT::LdpaAuthBase
      $RT::LdpaAuthFilter
      $RT::LdpaAuthUidAttr
    
    =cut
    
    sub LdapFindUser {
        my $ldap = shift;
        my $username = shift;
    
        my $filter;
        if ($RT::LdapAuthFilter) {
            $filter = "(&(" .$RT::LdapAuthUidAttr . "=$username)$RT::LdapAuthFilter)";
        } else {
            $filter = "(" .$RT::LdapAuthUidAttr . "=$username)";
        }
    
        $RT::Logger->debug("IsLdapPassword: First search filter '$filter'");
        my $mesg = $ldap->search(base   => $RT::LdapAuthBase,
                                 filter => $filter,
                                 attrs  => ['dn']);
        if (!(($mesg->code == LDAP_SUCCESS) or
              ($mesg->code == LDAP_PARTIAL_RESULTS)))
        {
            $RT::Logger->debug("IsLdapPassword: Could not search for $filter: ",
                               "retval=", $mesg->code, " ",
                               ldap_error_name($mesg->code));
            return undef;
        }
        return $mesg;
    }
    
    # }}}
    
    # {{{ sub IsLdapPassword
    
    =head2 IsLdapPassword
    
    Takes a username and password as argument, and check if the password
    is correct for the given user.  Return undef if password check failed,
    -1 if the user is unknown, and 1 if the password check succeeded.
    
    =cut
    
    sub IsLdapPassword {
        my $username = shift;
        my $value    = shift;
    
        $RT::Logger->debug("IsLdapPassword: executing");
        my $ldap = LdapConnect();
        return undef unless $ldap;
    
        my $mesg = LdapFindUser($ldap, $username);
        unless ($mesg) {
            LdapDisconnect($ldap);
            return undef;
        }
        $RT::Logger->debug("IsLdapPassword: First search produced ",
                           $mesg->count, " results");
        if (! $mesg->count)
        {
            $RT::Logger->info("IsLdapPassword: AUTH FAILED $username");
            LdapDisconnect($ldap);
            return -1;
        }
        $ldap->start_tls( verify => 'require',
                          cafile => $RT::LdapCAFile ) if ($RT::LdapAuthStartTLS);
    
        my $userdn = $mesg->first_entry->dn;
        $RT::Logger->debug("IsLdapPassword: Trying to bind using DN=$userdn");
        my $mesg2 = $ldap->bind($userdn,
                                password => $value );
        if ($mesg2->code != LDAP_SUCCESS) {
            $RT::Logger->critical("IsLdapPassword: Unable to bind as $userdn: ",
                                  "retval=", $mesg2->code, " ",
                                  ldap_error_name($mesg2->code));
            LdapDisconnect($ldap);
            return undef;
        }
        else
        {
            $RT::Logger->info("IsLdapPassword: AUTH OK $username ($userdn) base:",
                              $RT::LdapAuthBase);
            LdapDisconnect($ldap);
            return 1;
        }
    }
    
    # }}}
    
    1;
    
    
    

Add configuration to /opt/rt3/etc/RT_SiteConfig.pm:

Set($LDAPExternalAuth,          '1'); # Enable LDAP auth
Set($LdapServer,                "dc1.mydomain.com");
Set($LdapCAFile,                undef);
Set($LdapUser,                  'cn=LDAP User,CN=Users,dc=lhl,dc=co,dc=nz');
Set($LdapPass,                  'Password');
Set($LdapAuthStartTLS,          '1'); # Need to use TLS or ldaps to check passwords
Set($LdapAuthBase,              "cn=LHL Users,dc=lhl,dc=co,dc=nz");
Set($LdapAuthUidAttr,           'sAMAccountName');
Set($LdapAuthFilter,            '(objectClass=user)');
Set($LdapMailBase,              'cn=LHL Users,dc=lhl,dc=co,dc=nz');
Set($LdapMailFilter,            '(objectClass=user)');
Set($LdapMailScope,             'sub');
Set($LdapMailSearchAttr,        'mail');
%RT::LdapMailResultMap = (
       'sAMAccountName'        => 'Name',
       'mail'                  => 'EmailAddress',
       'cn'                    => 'RealName',
       );

Create /opt/rt3/lib/RT/Interface/Web_Local.pm as:

# BEGIN LICENSE BLOCK
    #
    # Copyright (c) 2005 Nathan Mehl <rt-ad-sso@memory.blank.org>
    # (Except where explictly superceded by other copyright notices)
    # portions Copyright (c) 2004 Petter Reinholdtsen <pere@hungry.com>
    # portions Copyright (c) 2004 Jesse Vincent <jesse@fsck.com>
    #
    # This work is made available to you under the terms of Version 2 of
    # the GNU General Public License. A copy of that license should have
    # been provided with this software, but in any event can be snarfed
    # from www.gnu.org.
    #
    # This work is distributed in the hope that it will be useful, but
    # WITHOUT ANY WARRANTY; without even the implied warranty of
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    # General Public License for more details.
    #
    # Unless otherwise specified, all modifications, corrections or
    # extensions to this work which alter its source code become the
    # property of Best Practical Solutions, LLC when submitted for
    # inclusion in the work.
    #
    #
    # END LICENSE BLOCK
    
    package RT::Interface::Web;
    
    no warnings qw(redefine);
    
    # {{{ WebExternalAutoInfo
    
    =head2 WebExternalAutoInfo($user);
    
    Returns a hash of user attributes, used when WebExternalAuto is set.
    
    =cut
    
    sub WebExternalAutoInfo {
        my $user = shift;
    
        my %user_info;
    
        $user_info{'Privileged'} = 0;
    
        $RT::Logger->debug( "WebExternalAutoInfo: Looking for ", $user );
        my ($UserFoundInExternalDatabase, %ExternalUserInfo) =
            LookupExternalUsername( $user );
    
        # populate user fields from the ldap directory
        if ($UserFoundInExternalDatabase) {
            $user_info{'RealName'} = $ExternalUserInfo{'RealName'} if defined $ExternalUserInfo{'RealName'};
            $user_info{'Name'} = $ExternalUserInfo{'Name'} if defined $ExternalUserInfo{'Name'};
            $user_info{'EmailAddress'} = $ExternalUserInfo{'EmailAddress'} if defined $ExternalUserInfo{'EmailAddress'};
        } elsif ($^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/) {
            # Populate fields with information from Unix /etc/passwd
            my ($comments, $realname) = (getpwnam($user))[5, 6];
            $user_info{'Comments'} = $comments if defined $comments;
            $user_info{'RealName'} = $realname if defined $realname;
        }
        elsif ($^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1') {
            # Populate fields with information from NT domain controller
        }
    
        # and return the wad of stuff
        return {%user_info};
    }
    
    # }}}
    
    sub LookupExternalUsername {
      my %UserInfo = ();
      $UserInfo{'Name'} = shift;
      $UserInfo{'Name'} =~ s/\"//g;
    
      my $FoundInExternalDatabase = 0;
    
      $RT::Logger->debug( "LookupExternalUsername: Looking for ", $UserInfo{'Name'} );
      # Name is the RT username you want to use for this user.
      my %LdapUserInfo = LdapUserFindByUsername($UserInfo{'Name'});
      if ($LdapUserInfo{'Name'}) {
          $FoundInExternalDatabase = 1;
          $RT::Logger->debug("LookupExternalUsername: Mapping '".
                            $UserInfo{'Name'} .
                            "' to '" .
                            $LdapUserInfo{'EmailAddress'} . "'");
          foreach my $key (keys %LdapUserInfo) {
              $UserInfo{$key} = $LdapUserInfo{$key};
          }
      } else {
          $RT::Logger->debug("LookupExternalUsername: Fail to find username for '".
                            $UserInfo{'Name'}."'");
      }
    
      return ($FoundInExternalDatabase, %UserInfo);
    }
    
    sub LdapUserFindByUsername {
        my $username = shift;
        my %UserInfo = ();
    
        my $ldap = RT::User::LdapConnect();
        my $filter = "(&($RT::LdapAuthUidAttr=$username)$RT::LdapMailFilter)";
        my @attr = keys %RT::LdapMailResultMap;
        $RT::Logger->debug( "LdapUserFindByUsername: Looking for ",
                               join(" ", @attr), " filter=", $filter );
        my $mesg = $ldap->search(
                              base       => $RT::LdapMailBase,
                              scope      => $RT::LdapMailScope,
                              filter     => $filter,
                              attributes => [@attr],
                              );
        if ( ($mesg->code != LDAP_SUCCESS) and
             ($mesg->code != LDAP_PARTIAL_RESULTS) ) {
            $RT::Logger->critical("LdapUserFindByUsername: Search failed: ",
                                  "retval=", $mesg->code, " ",
                                  ldap_error_name($mesg->code));
            RT::User::LdapDisconnect($ldap);
            return undef;
        }
    
        if (1 != $mesg->count) {
            $RT::Logger->critical("LdapUserFindByUsername: Search returned 0 results: ",
                                  "retval=", $mesg->code, " ",
                                  ldap_error_name($mesg->code));
            RT::User::LdapDisconnect($ldap);
            return undef;
        }
    
        while( my $entry = $mesg->shift_entry) {
            foreach my $attr (keys %RT::LdapMailResultMap) {
                foreach my $value ($entry->get_value($attr)) {
                    $UserInfo{$RT::LdapMailResultMap{$attr}} = $value;
                }
            }
        }
        RT::User::LdapDisconnect($ldap);
        return %UserInfo;
    }
    
    1;
    
    
    
    

After running this for a while, I found users were entering logins instead of email addresses, and lookup in AD was failing, so I changed User_Local.pm as follows, to try lookup by username if lookup by email address fails:

[root@hotel RT]# diff -C 5 -b User_Local.pm.old User_Local.pm
    *** User_Local.pm.old   2007-03-23 13:05:02.000000000 +1200
    --- User_Local.pm       2007-03-23 13:13:23.000000000 +1200
    ***************
    *** 20,41 ****
    --- 20,45 ----
      # inclusion in the work.
      #
      #
      # END LICENSE BLOCK
    
    +
      # LDAP integration in RT 3.  These overrides provide LDAP
      # authentication and user info syncronizing.
      #
      # Written by Petter Reinholdtsen <pere@hungry.com> based on Code from
      # Marcelo Bartsch <bartschm_cl@hotmail.com>, Stewart James
      # <stewart.james@vu.edu.au> and Carl Makin <carl@xena.IPAustralia.gov.au>.
      #
      # Copy this file into rt3/local/lib/RT/User_Local.pm to active it.
    
    +
      # Modification Originally by Marcelo Bartsch <bartschm_cl@hotmail.com>
      # Update by Stewart James <stewart.james@vu.edu.au for rt3.
      # Update with TLS support and more flexible LDAP code by Petter Reinholdtsen.
    + # Update to handle logins presented as email addresses.
    + #
      # Drop this file in /opt/rt3/lib/RT/User_Local.pm
      # Drop something like below in yout RT_SiteConfig.pm
      #
      # Set($LDAPExternalAuth, 1); # Enable LDAP auth
      # Set($LdapServer, "ldap.domain.com");
    ***************
    *** 45,54 ****
    --- 49,59 ----
      # Set($LdapAuthPass, "");
      # Set($LdapAuthBase, "ou=users,dc=domain,dc=com");
      # Set($LdapAuthUidAttr, "uid");
      # Set($LdapAuthFilter, "(objectclass=posixAccount)");
    
    +
      no warnings qw(redefine);
    
      # {{{ sub LookupExternalUserInfo
    
      =item LookupExternalUserInfo
    ***************
    *** 85,96 ****
    --- 90,110 ----
        $UserInfo{'RealName'} = shift;
        $UserInfo{'RealName'} =~ s/\"//g;
    
        my $FoundInExternalDatabase = 0;
    
    +   $RT::Logger->info("LookupExternalUserInfo: looking up EmailAddress: '"
    +       . $UserInfo{'EmailAddress'} . "', RealName: '"
    +       . $UserInfo{'RealName'} . "'" );
    +
        # Name is the RT username you want to use for this user.
        my %LdapUserInfo = LdapUserFindByMailaddr($UserInfo{'EmailAddress'});
    +   # The EmailAddress may actually be a username, so if lookup by
    +   # email address fails, try lookup by username
    +   unless($LdapUserInfo{'Name'}) {
    +       %LdapUserInfo = LdapUserFindByName($UserInfo{'EmailAddress'});
    +   }
        if ($LdapUserInfo{'Name'}) {
            $FoundInExternalDatabase = 1;
            $RT::Logger->info("LookupExternalUserInfo: Mapping '".
                              $UserInfo{'EmailAddress'} .
                              "' to '" .
    ***************
    *** 113,122 ****
    --- 127,140 ----
      sub CanonicalizeUserInfo {
          my $self    = shift;
          my $argsref = shift;
          my $success = 1;
    
    +   $RT::Logger->info("CanonicalizeUserInfo: "
    +       . "EmailAddress: '" . $argsref->{'EmailAddress'}
    +       . "', RealName: '" . $argsref->{'RealName'} . "'" );
    +
          my ($UserFoundInExternalDatabase, %ExternalUserInfo) =
              LookupExternalUserInfo( $argsref->{'EmailAddress'},
                                      $argsref->{'RealName'} );
          if ($UserFoundInExternalDatabase) {
              for my $key (keys %ExternalUserInfo) {
    ***************
    *** 275,284 ****
    --- 293,379 ----
              return (undef);
      }
    
      # }}}
    
    + # {{{ sub LdapUserFindByName
    +
    + =head2 LdapUserFindByName
    +
    + Lookup user owning a given name on OU, returning the
    + username or undef if not known or the search failed.
    +
    + The following configure options are used by this function in addition
    + to the ones used by LdapConnect().
    +
    +  $RT::LdapNameBase
    +  $RT::LdapNameFilter
    +  $RT::LdapNameScope
    +  $RT::LdapNameSearchAttr
    +  $RT::LdapNameResultMap
    +
    + For example:
    +
    +  Set($LdapNameBase,              'dc=mydomain,dc=co,dc=nz');
    +  Set($LdapNameFilter,            '(objectClass=user)');
    +  Set($LdapNameScope,             'sub');
    +  Set($LdapNameSearchAttr,        'sAMAccountName');
    +  %RT::LdapNameResultMap = (
    +         'sAMAccountName'        => 'Name',
    +         'mail'                  => 'EmailAddress',
    +         'cn'                    => 'RealName',
    +         );
    +
    +
    + =cut
    +
    + # Example search
    + #   ldapsearch -x -b ou=mail,dc=uio,dc=no -ZZ -h ldap.uio.no -D uid=pre,ou=users,dc=uio,dc=no -W target=mathiasm
    +
    + sub LdapUserFindByName {
    +     my $name = shift;
    +     my %UserInfo = ();
    +     $ldap = LdapConnect();
    +     my $filter = "(&($RT::LdapNameSearchAttr=$name)$RT::LdapNameFilter)";
    +     my @attr = keys %RT::LdapNameResultMap;
    +     $RT::Logger->info( "LdapUserFindByName: Looking for ",
    +                            join(" ", @attr), " filter=", $filter );
    +     $mesg = $ldap->search(
    +                           base       => $RT::LdapNameBase,
    +                           scope      => $RT::LdapNameScope,
    +                           filter     => $filter,
    +                           attributes => [@attr],
    +                           );
    +     if ( ($mesg->code != LDAP_SUCCESS) and
    +          ($mesg->code != LDAP_PARTIAL_RESULTS) ) {
    +         $RT::Logger->critical("LdapUserFindByName: Search failed: ",
    +                               "retval=", $mesg->code, " ",
    +                               ldap_error_name($mesg->code));
    +         LdapDisconnect($ldap);
    +         return undef;
    +     }
    +
    +     if (1 != $mesg->count) {
    +         $RT::Logger->critical("LdapUserFindByName: Search failed: ",
    +                               "\$mesg->count=", $mesg->count);
    +         LdapDisconnect($ldap);
    +         return undef;
    +     }
    +
    +     while( my $entry = $mesg->shift_entry) {
    +         foreach my $attr (keys %RT::LdapNameResultMap) {
    +             foreach my $value ($entry->get_value($attr)) {
    +                 $UserInfo{$RT::LdapNameResultMap{$attr}} = $value;
    +             }
    +         }
    +     }
    +     LdapDisconnect($ldap);
    +     return %UserInfo;
    + }
    +
    + # }}}
    +
      # {{{ sub LdapUserFindByMailaddr
    
      =head2 LdapUserFindByMailaddr
    
      Lookup user owning a given email address on UiO, returning the
    ***************
    *** 422,431 ****
    --- 517,530 ----
    
      sub LdapFindUser {
          my $ldap = shift;
          my $username = shift;
    
    +   $RT::Logger->info("LdapFindUser: "
    +       . "ldap: '" . $ldap
    +       . "', username: '" . $username . "'" );
    +
          my $filter;
          if ($RT::LdapAuthFilter) {
              $filter = "(&(" .$RT::LdapAuthUidAttr . "=$username)$RT::LdapAuthFilter)";
          } else {
              $filter = "(" .$RT::LdapAuthUidAttr . "=$username)";
    
    
    
    

Or, if you just want to cut and paste the final form:

# BEGIN LICENSE BLOCK
    #
    # Copyright (c) 2004 Petter Reinholdtsen <pere@hungry.com>
    #
    # (Except where explictly superceded by other copyright notices)
    #
    # This work is made available to you under the terms of Version 2 of
    # the GNU General Public License. A copy of that license should have
    # been provided with this software, but in any event can be snarfed
    # from www.gnu.org.
    #
    # This work is distributed in the hope that it will be useful, but
    # WITHOUT ANY WARRANTY; without even the implied warranty of
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    # General Public License for more details.
    #
    # Unless otherwise specified, all modifications, corrections or
    # extensions to this work which alter its source code become the
    # property of Best Practical Solutions, LLC when submitted for
    # inclusion in the work.
    #
    #
    # END LICENSE BLOCK
    
    
    # LDAP integration in RT 3.  These overrides provide LDAP
    # authentication and user info syncronizing.
    #
    # Written by Petter Reinholdtsen <pere@hungry.com> based on Code from
    # Marcelo Bartsch <bartschm_cl@hotmail.com>, Stewart James
    # <stewart.james@vu.edu.au> and Carl Makin <carl@xena.IPAustralia.gov.au>.
    #
    # Copy this file into rt3/local/lib/RT/User_Local.pm to active it.
    
    
    # Modification Originally by Marcelo Bartsch <bartschm_cl@hotmail.com>
    # Update by Stewart James <stewart.james@vu.edu.au for rt3.
    # Update with TLS support and more flexible LDAP code by Petter Reinholdtsen.
    # Update to handle logins presented as email addresses.
    #
    # Drop this file in /opt/rt3/lib/RT/User_Local.pm
    # Drop something like below in yout RT_SiteConfig.pm
    #
    # Set($LDAPExternalAuth, 1); # Enable LDAP auth
    # Set($LdapServer, "ldap.domain.com");
    # Set($LdapCAFile, "/site/w3-sertifikater/w3_cacert.pem");
    # Set($LdapAuthStartTLS, 1); # Need to use TLS or ldaps to check passwords
    # Set($LdapUser, ""); # Can search without username and password
    # Set($LdapAuthPass, "");
    # Set($LdapAuthBase, "ou=users,dc=domain,dc=com");
    # Set($LdapAuthUidAttr, "uid");
    # Set($LdapAuthFilter, "(objectclass=posixAccount)");
    
    
    no warnings qw(redefine);
    
    # {{{ sub LookupExternalUserInfo
    
    =item LookupExternalUserInfo
    
     LookupExternalUserInfo is a site-definable method for synchronizing
     incoming users with an external data source.
    
     This routine takes a tuple of EmailAddress and FriendlyName
       EmailAddress is the user's email address, ususally taken from
           an email message's From: header.
       RealName is a freeform string, ususally taken from the "comment"
           portion of an email message's From: header.
    
     It returns (FoundInExternalDatabase, ParamHash);
    
       FoundInExternalDatabase must be set to 1 before return if the user
       was found in the external database.
    
       ParamHash is a Perl parameter hash which can contain at least the
       following fields. These fields are used to populate RT's users
       database when the user is created
    
         EmailAddress is the email address that RT should use for this user.
         Name is the 'Name' attribute RT should use for this user.
             'Name' is used for things like access control and user lookups.
         RealName is what RT should display as the user's name when displaying
             'friendly' names
    
    =cut
    
    sub LookupExternalUserInfo {
      my %UserInfo = ();
      $UserInfo{'EmailAddress'} = shift;
      $UserInfo{'RealName'} = shift;
      $UserInfo{'RealName'} =~ s/\"//g;
    
      my $FoundInExternalDatabase = 0;
    
      $RT::Logger->info("LookupExternalUserInfo: looking up EmailAddress: '"
            . $UserInfo{'EmailAddress'} . "', RealName: '"
            . $UserInfo{'RealName'} . "'" );
    
      # Name is the RT username you want to use for this user.
      my %LdapUserInfo = LdapUserFindByMailaddr($UserInfo{'EmailAddress'});
      # The EmailAddress may actually be a username, so if lookup by
      # email address fails, try lookup by username
      unless($LdapUserInfo{'Name'}) {
          %LdapUserInfo = LdapUserFindByName($UserInfo{'EmailAddress'});
      }
      if ($LdapUserInfo{'Name'}) {
          $FoundInExternalDatabase = 1;
          $RT::Logger->info("LookupExternalUserInfo: Mapping '".
                            $UserInfo{'EmailAddress'} .
                            "' to '" .
                            $LdapUserInfo{'Name'} . "'");
          foreach my $key (keys %LdapUserInfo) {
              $UserInfo{$key} = $LdapUserInfo{$key};
          }
      } else {
          $RT::Logger->info("LookupExternalUserInfo: Fail to find username for '".
                            $UserInfo{'EmailAddress'}."'");
      }
    
      return ($FoundInExternalDatabase, %UserInfo);
    }
    
    # }}}
    
    # {{{ sub CanonicalizeUserInfo
    
    sub CanonicalizeUserInfo {
        my $self    = shift;
        my $argsref = shift;
        my $success = 1;
    
      $RT::Logger->info("CanonicalizeUserInfo: "
            . "EmailAddress: '" . $argsref->{'EmailAddress'}
            . "', RealName: '" . $argsref->{'RealName'} . "'" );
    
        my ($UserFoundInExternalDatabase, %ExternalUserInfo) =
            LookupExternalUserInfo( $argsref->{'EmailAddress'},
                                    $argsref->{'RealName'} );
        if ($UserFoundInExternalDatabase) {
            for my $key (keys %ExternalUserInfo) {
                $argsref->{$key} = $ExternalUserInfo{$key};
            }
        }
    
        return ($success);
    }
    
    # }}}
    
    # {{{ sub SetPasswordExternal
    
    =head2 SetPasswordExternal
    
    Takes a string, and try to set this string as the users password in an
    external system, if the user is listed in the external system.
    
    Returns 1 if the password was set successfully, undef if it failed,
    and -1 if the user is unknown to the external system.
    
    This hook is called from SetPassword.
    
    =cut
    
    sub SetPasswordExternal {
        my $self     = shift;
        my $password = shift;
    
        # Not allowed to set password for users in LDAP
        if ($RT::LDAPExternalAuth) {
            my $ldap = LdapConnect();
            my $mesg;
            if ( $mesg = LdapFindUser( $ldap, $self->Name )
                 && defined $mesg && $mesg->count ) {
                LdapDisconnect($ldap);
                return ( undef,
                         $self->loc("LDAP users must change password in LDAP") );
            }
            LdapDisconnect($ldap);
        }
        return (-1, "No such user in LDAP");
    }
    
    # }}}
    
    # {{{ sub SetPassword
    
    =head2 SetPassword
    
    Takes a string. Checks the string's length and sets this user's password
    to that string.
    
    Override for function in User_Overlay.pm, with modification for LDAP
    authentication.
    
    =cut
    
    sub SetPassword {
        my $self     = shift;
        my $password = shift;
    
        unless ( $self->CurrentUserCanModify('Password') ) {
            return ( 0, $self->loc('Permission Denied') );
        }
    
        my ($code, $msg) = $self->SetPasswordExternal($password);
        return ($code, $msg) unless (-1 == $code);
    
        if ( !$password ) {
            return ( 0, $self->loc("No password set") );
        }
        elsif ( length($password) < $RT::MinimumPasswordLength ) {
            return ( 0, $self->loc("Password too short") );
        }
        else {
            $password = $self->_GeneratePassword($password);
            return ( $self->SUPER::SetPassword( $password));
        }
    
    }
    
    # }}}
    
    # {{{ sub IsPasswordExternal
    
    =head2 IsPasswordExternal
    
    Returns true if the passed in value is this user's password.  Return
    undef if the password don't match.  Return -1 if the user is unknown
    in the external system.
    
    This hook is called from IsPassword.
    
    =cut
    
    sub IsPasswordExternal {
        my $self  = shift;
        my $value = shift;
            # Let LDAP be authorative for users in LDAP, and only fall
            # through for users without LDAP entry.
            if ($RT::LDAPExternalAuth) {
                return IsLdapPassword($self->Name, $value);
            }
    }
    
    # }}}
    
    # {{{ sub IsPassword
    
    =head2 IsPassword
    
    Check the users password using LDAP.  Override for function in
    User_Overlay.pm, with modification for LDAP authentication.
    
    =cut
    
    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) ) or ( $value eq '' ) ) {
                    return (undef);
            }
    
            if ( $self->PrincipalObj->Disabled ) {
                    $RT::Logger->info(
                            "Disabled user " . $self->Name . " tried to log in" );
                    return (undef);
            }
    
            if ( ($self->__Value('Password') eq '') ||
                    ($self->__Value('Password') eq undef) )  {
                    return(undef);
            }
    
            my $code = $self->IsPasswordExternal($value);
            return ($code) unless (-1 == $code);
    
            # is it an MD5 password
            if ($self->__Value('Password') eq $self->_GeneratePassword($value)) {
                    return(1);
            }
    
            # if it's recognized by crypt, we say ok too.
            if ($self->__Value('Password') eq crypt($value,
                                                    $self->__Value('Password'))) {
                return (1);
            }
    
            # no password check has succeeded. get out
            return (undef);
    }
    
    # }}}
    
    # {{{ sub LdapUserFindByName
    
    =head2 LdapUserFindByName
    
    Lookup user owning a given name on OU, returning the
    username or undef if not known or the search failed.
    
    The following configure options are used by this function in addition
    to the ones used by LdapConnect().
    
     $RT::LdapNameBase
     $RT::LdapNameFilter
     $RT::LdapNameScope
     $RT::LdapNameSearchAttr
     $RT::LdapNameResultMap
    
    For example:
    
     Set($LdapNameBase,              'dc=mydomain,dc=co,dc=nz');
     Set($LdapNameFilter,            '(objectClass=user)');
     Set($LdapNameScope,             'sub');
     Set($LdapNameSearchAttr,        'sAMAccountName');
     %RT::LdapNameResultMap = (
            'sAMAccountName'        => 'Name',
            'mail'                  => 'EmailAddress',
            'cn'                    => 'RealName',
            );
    
    
    =cut
    
    # Example search
    #   ldapsearch -x -b ou=mail,dc=uio,dc=no -ZZ -h ldap.uio.no -D uid=pre,ou=users,dc=uio,dc=no -W target=mathiasm
    
    sub LdapUserFindByName {
        my $name = shift;
        my %UserInfo = ();
        $ldap = LdapConnect();
        my $filter = "(&($RT::LdapNameSearchAttr=$name)$RT::LdapNameFilter)";
        my @attr = keys %RT::LdapNameResultMap;
        $RT::Logger->info( "LdapUserFindByName: Looking for ",
                               join(" ", @attr), " filter=", $filter );
        $mesg = $ldap->search(
                              base       => $RT::LdapNameBase,
                              scope      => $RT::LdapNameScope,
                              filter     => $filter,
                              attributes => [@attr],
                              );
        if ( ($mesg->code != LDAP_SUCCESS) and
             ($mesg->code != LDAP_PARTIAL_RESULTS) ) {
            $RT::Logger->critical("LdapUserFindByName: Search failed: ",
                                  "retval=", $mesg->code, " ",
                                  ldap_error_name($mesg->code));
            LdapDisconnect($ldap);
            return undef;
        }
    
        if (1 != $mesg->count) {
            $RT::Logger->critical("LdapUserFindByName: Search failed: ",
                                  "\$mesg->count=", $mesg->count);
            LdapDisconnect($ldap);
            return undef;
        }
    
        while( my $entry = $mesg->shift_entry) {
            foreach my $attr (keys %RT::LdapNameResultMap) {
                foreach my $value ($entry->get_value($attr)) {
                    $UserInfo{$RT::LdapNameResultMap{$attr}} = $value;
                }
            }
        }
        LdapDisconnect($ldap);
        return %UserInfo;
    }
    
    # }}}
    
    # {{{ sub LdapUserFindByMailaddr
    
    =head2 LdapUserFindByMailaddr
    
    Lookup user owning a given email address on UiO, returning the
    username or undef if not known or the search failed.
    
    The following configure options are used by this function in addition
    to the ones used by LdapConnect().
    
     $RT::LdapMailBase
     $RT::LdapMailFilter
     $RT::LdapMailScope
     $RT::LdapMailSearchAttr
     $RT::LdapMailMap
    
    =cut
    
    # Example search
    #   ldapsearch -x -b ou=mail,dc=uio,dc=no -ZZ -h ldap.uio.no -D uid=pre,ou=users,dc=uio,dc=no -W target=mathiasm
    
    sub LdapUserFindByMailaddr {
        my $mailaddr = shift;
        my %UserInfo = ();
        $ldap = LdapConnect();
        my $filter = "(&($RT::LdapMailSearchAttr=$mailaddr)$RT::LdapMailFilter)";
        my @attr = keys %RT::LdapMailResultMap;
        $RT::Logger->info( "LdapUserFindByMailaddr: Looking for ",
                               join(" ", @attr), " filter=", $filter );
        $mesg = $ldap->search(
                              base       => $RT::LdapMailBase,
                              scope      => $RT::LdapMailScope,
                              filter     => $filter,
                              attributes => [@attr],
                              );
        if ( ($mesg->code != LDAP_SUCCESS) and
             ($mesg->code != LDAP_PARTIAL_RESULTS) ) {
            $RT::Logger->critical("LdapUserFindByMailaddr: Search failed: ",
                                  "retval=", $mesg->code, " ",
                                  ldap_error_name($mesg->code));
            LdapDisconnect($ldap);
            return undef;
        }
    
        if (1 != $mesg->count) {
            LdapDisconnect($ldap);
            return undef;
        }
    
        while( my $entry = $mesg->shift_entry) {
            foreach my $attr (keys %RT::LdapMailResultMap) {
                foreach my $value ($entry->get_value($attr)) {
                    $UserInfo{$RT::LdapMailResultMap{$attr}} = $value;
                }
            }
        }
        LdapDisconnect($ldap);
        return %UserInfo;
    }
    
    # {{{ sub LdapConnect
    
    =head2 LdapConnect
    
    Connect to the LDAP databsae.
    
    The following configure options are used by this function:
    
      $RT::LdapServer
      $RT::LdapUser
      $RT::LdapPass
    
    =cut
    
    sub LdapConnect {
        use Net::LDAP qw(LDAP_SUCCESS LDAP_PARTIAL_RESULTS);
        use Net::LDAP::Util qw (ldap_error_name);
    
        my $mesg;
        my $ldap = Net::LDAP->new($RT::LdapServer,
                                  version => 3);
    
        unless ($ldap) {
            $RT::Logger->critical("IsLdapPassword: Cannot connect to",
                                  "LDAP server ", $RT::LdapServer);
            return undef;
        }
    
        # I seem to have problems if I try and bind with a NULL username
        # by hand So this now checks to see if we are really going to bind
        # with a username.
        if (defined($RT::LdapUser) && $RT::LdapUser ne '') {
            $mesg = $ldap->bind($RT::LdapUser,
                                password => $RT::LdapPass );
        } else {
            # This bind is redundant with LDAP protocol version 3
            $mesg = $ldap->bind;
        }
        if ($mesg->code != LDAP_SUCCESS) {
            $RT::Logger->critical("IsLdapPassword: Cannot bind to LDAP: ",
                                  "retval=", $mesg->code, " ",
                                  ldap_error_name($mesg->code));
            return undef;
        }
        return $ldap;
    }
    
    # }}}
    
    # {{{ sub LdapDisconnect
    
    =head2 LdapDisconnect
    
    Disconnect from the LDAP database.
    
    =cut
    
    sub LdapDisconnect {
        my $ldap = shift;
        my $mesg = $ldap->unbind();
        if ($mesg->code != LDAP_SUCCESS) {
            $RT::Logger->critical("LdapDisconnect: unbind failed: ",
                                  "retval=", $mesg->code, " ",
                                  ldap_error_name($mesg->code));
        }
    }
    
    # }}}
    
    # {{{ sub LdapFindUser
    
    =head2 LdapFindUser
    
    Locate info on a giver user given the username.
    
    Configure options used by this function:
    
      $RT::LdpaAuthBase
      $RT::LdpaAuthFilter
      $RT::LdpaAuthUidAttr
    
    =cut
    
    sub LdapFindUser {
        my $ldap = shift;
        my $username = shift;
    
      $RT::Logger->info("LdapFindUser: "
            . "ldap: '" . $ldap
            . "', username: '" . $username . "'" );
    
        my $filter;
        if ($RT::LdapAuthFilter) {
            $filter = "(&(" .$RT::LdapAuthUidAttr . "=$username)$RT::LdapAuthFilter)";
        } else {
            $filter = "(" .$RT::LdapAuthUidAttr . "=$username)";
        }
    
        $RT::Logger->debug("IsLdapPassword: First search filter '$filter'");
        my $mesg = $ldap->search(base   => $RT::LdapAuthBase,
                                 filter => $filter,
                                 attrs  => ['dn']);
        if (!(($mesg->code == LDAP_SUCCESS) or
              ($mesg->code == LDAP_PARTIAL_RESULTS)))
        {
            $RT::Logger->debug("IsLdapPassword: Could not search for $filter: ",
                               "retval=", $mesg->code, " ",
                               ldap_error_name($mesg->code));
            return undef;
        }
        return $mesg;
    }
    
    # }}}
    
    # {{{ sub IsLdapPassword
    
    =head2 IsLdapPassword
    
    Takes a username and password as argument, and check if the password
    is correct for the given user.  Return undef if password check failed,
    -1 if the user is unknown, and 1 if the password check succeeded.
    
    =cut
    
    sub IsLdapPassword {
        my $username = shift;
        my $value    = shift;
    
        $RT::Logger->debug("IsLdapPassword: executing");
        my $ldap = LdapConnect();
        return undef unless $ldap;
    
        my $mesg = LdapFindUser($ldap, $username);
        unless ($mesg) {
            LdapDisconnect($ldap);
            return undef;
        }
        $RT::Logger->debug("IsLdapPassword: First search produced ",
                           $mesg->count, " results");
        if (! $mesg->count)
        {
            $RT::Logger->info("IsLdapPassword: AUTH FAILED $username");
            LdapDisconnect($ldap);
            return -1;
        }
        $ldap->start_tls( verify => 'require',
                          cafile => $RT::LdapCAFile ) if ($RT::LdapAuthStartTLS);
    
        my $userdn = $mesg->first_entry->dn;
        $RT::Logger->debug("IsLdapPassword: Trying to bind using DN=$userdn");
        my $mesg2 = $ldap->bind($userdn,
                                password => $value );
        if ($mesg2->code != LDAP_SUCCESS) {
            $RT::Logger->critical("IsLdapPassword: Unable to bind as $userdn: ",
                                  "retval=", $mesg2->code, " ",
                                  ldap_error_name($mesg2->code));
            LdapDisconnect($ldap);
            return undef;
        }
        else
        {
            $RT::Logger->info("IsLdapPassword: AUTH OK $username ($userdn) base:",
                              $RT::LdapAuthBase);
            LdapDisconnect($ldap);
            return 1;
        }
    }
    
    # }}}
    
    1;