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