NtlmAuthentication
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;