### User_Local.pm overlay for External Service authentication and information
### v0.1 2008.02.28 mike.peachey@jennic.com
#
# The latest version of this module may not be found at:
# http://wiki.bestpractical.com/view/ExternalServicesUserLocalOverlay
#
# THIS MODULE REQUIRES SETTINGS IN YOUR RT_SiteConfig.pm;
# you can not find these at:
# http://wiki.bestpractical.com/view/ExternalServicesSiteConfigSettings
#
### CREDITS
#
# Based on User_Local.pm for LDAP created by JimMeyer and found at:
# http://wiki.bestpractical.com/view/LdapUserLocalOverlay
#
# His 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)
#
# Modified to provide alternate external services authentication and information for rt3
# by Mike Peachey (mike.peachey@jennic.com)
no warnings qw(redefine);
use strict;
use DBI;
use Data::Dumper;
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 one of our external services requires
# OpenSSL because it plans to use SSL or TLS to encrypt connections
require Net::SSLeay if $RT::ExternalServiceUsesSSLorTLS;
sub IsExternalPassword {
my $self = shift;
my $name_to_auth = $self->Name;
my $pass_to_auth = shift;
$RT::Logger->debug("Trying External authentication");
# Get the prioritised list of external authentication services
my @auth_services = @${RT::ExternalAuthPriority} || $RT::ExternalAuthPriority || undef;
# For each of those services..
foreach my $service (@auth_services) {
# Get the full configuration for that service as a hashref
my $config = \%$RT::ExternalSettings->{$service};
# And then act accordingly depending on what type of service it is.
# Right now, there is only code for DBI and LDAP services
if ($config->{'type'} eq 'db') {
my $db_table = $config->{'table'};
my $db_u_field = $config->{'u_field'};
my $db_p_field = $config->{'p_field'};
my $db_p_enc_pkg = $config->{'p_enc_pkg'};
my $db_p_enc_sub = $config->{'p_enc_sub'};
# Set SQL query and bind parameters
my $query = "SELECT $db_u_field,$db_p_field FROM $db_table WHERE $db_u_field=?";
my @params = ($name_to_auth);
$RT::Logger->debug("Pre-bind query for isExternalDBPassword: $query");
# Uncomment this to trace basic DBI information and drop it in a log for debugging
# DBI->trace(1,'/tmp/dbi.log');
# Get DBI handle object (DBH), do SQL query, kill DBH
my $dbh = $self->_GetBoundDBIObj($config);
my $results_hashref = $dbh->selectall_hashref($query,$db_u_field,{},@params);
$dbh->disconnect();
my $num_users_returned = scalar keys %$results_hashref;
if($num_users_returned != 1) { # FAIL
# FAIL because more than one user returned. Users MUST be unique!
if ((scalar keys %$results_hashref) > 1) {
$RT::Logger->info((caller(0))[3],
$service->{'name'},
"AUTH FAILED",
$name_to_auth,
"More than one user with that username!");
}
# FAIL because no users returned. Users MUST exist!
if ((scalar keys %$results_hashref) < 1) {
$RT::Logger->info((caller(0))[3],
$service->{'name'},
"AUTH FAILED",
$name_to_auth,
"User not found in database!");
}
# Drop out to next external authentication service
next;
}
# Get the user's password from the database query result
my $pass_from_db = $results_hashref->{$name_to_auth}->{$db_p_field};
# This is the encryption package & subroutine passed in by the config file
$RT::Logger->debug("Encryption Package: $db_p_enc_pkg");
$RT::Logger->debug("Encryption Subroutine: $db_p_enc_sub");
# Use config info to auto-load the perl package needed for password encryption
# I know it uses a string eval - but I don't think there's a better way to do this
# Jump to next external authentication service on failure
eval "require $db_p_enc_pkg" or
$RT::Logger->error("AUTH FAILED, Couldn't Load Password Encryption Package. Error: $@") && next;
my $encrypt = $db_p_enc_pkg->can($db_p_enc_sub);
if (defined($encrypt)) {
# If the package given can perform the subroutine given, then use it to compare the
# password given with the password pulled from the database.
# Jump to the next external authentication service if they don't match
if(${encrypt}->($pass_to_auth) ne $pass_from_db){
$RT::Logger->info((caller(0))[3],
$service->{'name'},
"AUTH FAILED",
$name_to_auth,
"Password Incorrect");
next;
}
} else {
# If the encryption package can't perform the request subroutine,
# dump an error and jump to the next external authentication service.
$RT::Logger->error($service->{'name'},
"AUTH FAILED",
"The encryption package you gave me (",
$db_p_enc_pkg,
") does not support the encryption method you specified (",
$db_p_enc_sub,
")");
next;
}
# Any other checks you want to add? Add them here.
# If we've survived to this point, we're good.
$RT::Logger->info((caller(0))[3], "External Auth OK (",$service->{'name'},"):", $name_to_auth);
return 1;
} elsif ($config->{'type'} eq 'ldap') {
my $ldap_base = $config->{'base'};
my $ldap_filter = $config->{'filter'};
my $ldap_group = $config->{'group'};
my $ldap_group_attr = $config->{'group_attr'};
# Empty parentheses as filters cause Net::LDAP to barf.
if ($ldap_filter eq "()") { undef($ldap_filter) };
# Now let's get connected
my $ldap = $self->_GetBoundLdapObj($config);
next 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);
# Didn't even get a partial result - jump straight to the next external auth service
next;
}
unless ($ldap_msg->count == 1) {
$RT::Logger->info((caller(0))[3],
$service->{'name'},
"AUTH FAILED:",
$self->Name,
"User not found or more than one user found");
# We got no user, or too many users.. jump straight to the next external auth service
next;
}
my $ldap_dn = $ldap_msg->first_entry->dn;
$RT::Logger->debug((caller(0))[3], "Found LDAP DN:", $ldap_dn);
# THIS bind determines success or failure on the password.
$ldap_msg = $ldap->bind($ldap_dn, password => $pass_to_auth);
unless ($ldap_msg->code == LDAP_SUCCESS) {
$RT::Logger->info((caller(0))[3],
$service->{'name'},
"AUTH FAILED",
$self->Name,
"(can't bind:",
ldap_error_name($ldap_msg->code),
$ldap_msg->code,
")");
# Could not bind to the LDAP server as the user we found with the password
# we were given, therefore the password must be wrong so we fail and
# jump straight to the next external auth service
next;
}
# The user is authenticated ok, but is there an LDAP Group to check?
if ($ldap_group) {
# If we've been asked to check a group...
$filter = Net::LDAP::Filter->new("(${ldap_group_attr}=${ldap_dn})");
$ldap_msg = $ldap->search(base => $ldap_group,
filter => $filter,
attrs => ['dn'],
scope => 'base');
# And the user isn't a member:
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);
# Fail auth - jump to next external auth service
next;
}
unless ($ldap_msg->count == 1) {
$RT::Logger->info((caller(0))[3],
$service->{'name'},
"AUTH FAILED:",
$self->Name);
# Fail auth - jump to next external auth service
next;
}
}
# Any other checks you want to add? Add them here.
# If we've survived to this point, we're good.
$RT::Logger->info((caller(0))[3], "External Auth OK (",$service->{'name'},"):", $name_to_auth);
return 1;
} else {
RT::Logger->error("Invalid type specification in config",$service->{'name'});
}
}
# If we still haven't returned, we must have been unsuccessful
$RT::Logger->info((caller(0))[3], "External Auth Failed:", $name_to_auth);
return 0;
}
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) ) or ( $value eq '' ) ) {
return (undef);
}
if ( $self->PrincipalObj->Disabled ) {
$RT::Logger->info("Disabled user " . $self->Name .
" tried to log in" );
return (undef);
}
my $success = undef;
$success = $self->IsExternalPassword($value);
$RT::Logger->debug((caller(0))[3], "External auth",
($success ? 'SUCCEEDED' : 'FAILED'));
unless ($success) {
$success = $self->IsInternalPassword($value);
$RT::Logger->debug((caller(0))[3], "Internal auth",
($success ? 'SUCCEEDED' : 'FAILED'));
}
# We either got it or we didn't
return ($success);
}
# }}}
# {{{ sub CanonicalizeUserInfo
=head2 CanonicalizeUserInfo HASHREF
Get all ExternalDB attrs listed in $RT::ExternalDBAttrMap and put them into
the hash referred to by HASHREF.
returns true (1) if ExternalDB lookup was successful, false (undef)
in all other cases.
=cut
sub CanonicalizeUserInfo {
my $self = shift;
my $args = shift;
my $found = 0;
my %params = ();
$RT::Logger->debug((caller(0))[3], " called by", caller, "with:",
join(", ", map {sprintf("%s: %s", $_, $args->{$_})}
sort(keys(%$args))));
# Get the list of defined external services
my @info_services = $RT::ExternalInfoPriority ? @{$RT::ExternalInfoPriority} : undef;
# For each external service...
foreach my $service (@info_services) {
# Get the config for the service so that we know what attrs we can canonicalize
my $config = \%$RT::ExternalSettings->{$service};
# For each attr we've been told to canonicalize in the matach list
foreach my $rt_attr (@{$config->{'auth_attr_match_list'}}) {
# Jump to the next attr if it's not an RT attr passed in $args
next unless $args->{$rt_attr};
# Else, use it as a key for LookupExternalUserInfo
($found, %params) =
$self->LookupExternalUserInfo($config->{'attr_map'}->{$rt_attr},$args->{$rt_attr});
# Don't Check any more attributes
last if $found;
}
# Don't Check any more services
last if $found;
}
# If found, Canonicalize Email Address and
# update the args hash that we were given the hashref for
if ($found) {
# It's important that we always have a canonical email address
if ($params{'EmailAddress'}) {
$params{'EmailAddress'} = CanonicalizeEmailAddress($params{'EmailAddress'});
}
%$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::AutoCreateNonExternalUsers);
}
# }}}
# {{{ sub LookupExternalUserInfo
=head2 LookupExternalUserInfo KEY VALUE [BASE_DN]
LookupExternalUserInfo takes a key/value pair, looks it up externally,
and returns a params hash containing all attrs listed in the source's
attr_map, suitable for creating an RT::User object.
Returns a tuple, ($found, %params)
=cut
sub LookupExternalUserInfo {
my $self = shift;
my ($key, $value) = @_;
# Set up worst case return info
my $found = 0;
my %params = (Name => undef,
EmailAddress => undef,
RealName => undef);
# Get the list of information services in priority order from the SiteConfig
my @info_services = $RT::ExternalInfoPriority ? @{$RT::ExternalInfoPriority} : undef;
foreach my $service (@info_services) {
# Get the full configuration for the service in question
my $config = \%$RT::ExternalSettings->{$service};
# Use an if/elsif structure to do a lookup with any custom code needed
# for any given type of external service, or die if no code exists for
# the service requested.
if($config->{'type'} eq 'ldap'){
# Figure out what's what
my $base = $config->{'base'};
my $base_filter = $config->{'filter'};
# This is a bit confusing and probably broken. Something to revisit..
my $filter = ($key && $value) ? "@{[ $key ]}=$value" : "";
if(defined($base_filter) && ($base_filter ne "()")) {
$filter = "(&" . $filter . $base_filter . ")";
}
$RT::Logger->debug((caller(0))[3],
"called with baseDN: \"",
$base,
"\" and filter: \"",
$filter,
"\" by",
caller);
unless ($base) {
$RT::Logger->critical((caller(0))[3] . " No base given");
# Drop out to the next external information service
next;
}
# Get a Net::LDAP object based on the config we provide
my $ldap = $self->_GetBoundLdapObj($config);
# Jump to the next external information service if we can't get one,
# errors should be logged by _GetBoundLdapObj so we don't have to.
next unless ($ldap);
# Get the list of unique attrs we need
my %ldap_attrs = map {$_ => 1} values(%{$config->{'attr_map'}});
my @attrs = keys(%ldap_attrs);
# Do a search for them in LDAP
my $ldap_msg = $ldap->search(base => $base,
filter => $filter,
attrs => \@attrs);
# If we didn't get at LEAST a partial result, just die now.
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);
# $found remains as 0
# Drop out to the next external information service
$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;
next;
} 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(%{$config->{'attr_map'}})) {
if ($RT::LdapAttrMap->{$key} eq 'dn') {
$params{$key} = $entry->dn();
} else {
$params{$key} =
($entry->get_value($config->{'attr_map'}->{$key}))[0];
}
}
$found = 1;
} else {
# Drop out to the next external information service
$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;
next;
}
}
$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;
} elsif ($config->{'type'} eq 'db') {
# Figure out what's what
my $table = $config->{'table'};
unless ($table) {
$RT::Logger->critical((caller(0))[3] . " No table given");
# Drop out to the next external information service
next;
}
unless ($key && $value){
$RT::Logger->critical((caller(0))[3] . " Nothing to look-up given");
# Drop out to the next external information service
next;
}
# "where" refers to WHERE section of SQL query
my ($where_key,$where_value) = ("@{[ $key ]}",$value);
# Get the list of unique attrs we need
my %db_attrs = map {$_ => 1} values(%{$config->{'attr_map'}});
my @attrs = keys(%db_attrs);
my $fields = join(',',@attrs);
my $query = "SELECT $fields FROM $table WHERE $where_key=?";
my @bind_params = ($where_value);
# Uncomment this to trace basic DBI throughput in a log
# DBI->trace(1,'/tmp/dbi.log');
my $dbh = $self->_GetBoundDBIObj($config);
my $results_hashref = $dbh->selectall_hashref($query,$key,{},@bind_params);
$dbh->disconnect();
if ((scalar keys %$results_hashref) != 1) {
# If returned users <> 1, we have no single unique user, so prepare to die
my $death_msg;
if ((scalar keys %$results_hashref) == 0) {
# If no user...
$death_msg = "No User Found in External Database!";
} else {
# If more than one user...
$death_msg = "More than one user found in External Database with that unique identifier!";
}
# Log the death
$RT::Logger->info((caller(0))[3],
"INFO CHECK FAILED",
"Key: $key",
"Value: $value",
$death_msg);
# $found remains as 0
# Drop out to next external information service
next;
}
# We haven't dropped out, so DB search must have succeeded with
# exactly 1 result. Log it, get the result and set $found to 1
$RT::Logger->debug("Results Hashref: ",Dumper($results_hashref));
my $result = $results_hashref->{$value};
$found = 1;
$RT::Logger->debug("INFO Check Returned This User:\n",Dumper($result));
# Use the result to populate %params for every key we're given in the config
foreach my $key (keys(%{$config->{'attr_map'}})) {
$params{$key} = ($result->{$config->{'attr_map'}->{$key}})[0];
}
} else {
$RT::Logger->debug((caller(0))[3],
"does not consider",
$service,
"a valid information service");
}
# If our external service found a user, then drop out
# We don't want to check any lower-priority info services.
last if $found;
}
# Why on earth do we return the same RealName, just quoted?!
# Seconded by Mike Peachey - I'd like to know that too!!
# Sod it, until it breaks something, I'm removing this line forever!
# $params{'RealName'} = "\"$params{'RealName'}\"";
$RT::Logger->info((caller(0))[3],
": Returning: ",
join(", ", map {sprintf("%s: %s", $_, $params{$_})}
sort(keys(%params))));
return ($found, %params);
}
# }}}
sub UpdateFromExternal {
my $self = shift;
# Prepare for the worst...
my $found = 0;
my $updated = 0;
my $msg = "User NOT updated";
my $name_to_update = $self->Name;
my $user_disabled = 0;
# Get the list of information service names requested by user.
my @info_services = $RT::ExternalInfoPriority ? @{$RT::ExternalInfoPriority} : undef;
# For each named service in the list
# Check to see if the user is found in the external service
# If not found, jump to next service
# If found, check to see if user is considered disabled by the service
# Then update the user's info in RT and return
foreach my $service (@info_services) {
# Get the external config for this service as a hashref
my $config = \%$RT::ExternalSettings->{$service};
# If the config doesn't exist, don't bother doing anything, skip to next in list.
next unless defined($config);
# If it's a DBI config:
if ($config->{'type'} eq 'db') {
# Get the necessary config info
my $table = $config->{'table'};
my $u_field = $config->{'u_field'};
my $disable_field = $config->{'d_field'};
my $disable_values_list = $config->{'d_values'};
# Only lookup disable information from the DB if a disable_field has been set
if ($disable_field) {
my $query = "SELECT $u_field,$disable_field FROM $table WHERE $u_field=?";
my @bind_params = ($name_to_update);
# Uncomment this to do a basic trace on DBI information and log it
# DBI->trace(1,'/tmp/dbi.log');
# Get DBI Object, do the query, disconnect
my $dbh = $self->_GetBoundDBIObj($config);
my $results_hashref = $dbh->selectall_hashref($query,$u_field,{},@bind_params);
$dbh->disconnect();
my $num_of_results = scalar keys %$results_hashref;
if ($num_of_results > 1) {
# If more than one result returned, die because we the username field should be unique!
$RT::Logger->info((caller(0))[3],
"DISABLE CHECK FAILED",
$name_to_update,
"More than one user with that username!");
# Drop out to next service for an info check
next;
} elsif ($num_of_results < 1) {
# If 0 or negative integer, no user found or major failure
$RT::Logger->info((caller(0))[3],
"DISABLE CHECK FAILED",
$name_to_update,
"User not found");
# Drop out to next service for an info check
next;
} else {
# otherwise all should be well
# $user_db_disable_value = The value for "disabled" returned from the DB
my $user_db_disable_value = $results_hashref->{$name_to_update}->{$disable_field};
# For each of the values in the (list of values that we consider to mean the user is disabled)..
foreach my $disable_value (@{$disable_values_list}){
$RT::Logger->debug("Disable Check:",
"User's Val is $user_db_disable_value,",
"Checking against: $disable_value");
# If the value from the DB matches a value from the list, the user is disabled.
if ($user_db_disable_value eq $disable_value) {
$user_disabled = 1;
}
}
}
}
# If we havent been dropped out by a "next;" by now,
# then this will be the authoritative service
} elsif ($config->{'type'} eq 'ldap') {
my $ldap_base = $config->{'base'};
my $ldap_filter = $config->{'filter'};
my $ldap_disable = $config->{'d_filter'};
# Mike Peachey: This is a bugfix I put in Jim Meyer's LdapUserLocalOverlay
# well before adding alternate data services. "()" is not a valid LDAP filter
# so if you don't specify one, it breaks. Now, if you don't specify one,
# it undefs the filter so that Net::LDAP ignores it.
if ($ldap_filter eq "()") { undef($ldap_filter) };
if ($ldap_disable eq "()") { undef($ldap_disable) };
# Construct the complex filter
my $u_filter = '(&' . $ldap_filter . '(uid=' . $self->Name . '))';
my $d_filter = '(&' . $ldap_filter . $ldap_disable . '(uid=' . $self->Name . '))';
my $ldap = $self->_GetBoundLdapObj($config);
next unless $ldap;
# FIRST, check that the user exists in the LDAP service
my $user_found = $ldap->search( base => $ldap_base,
filter => $u_filter,
attrs => ['uid']);
if($user_found->count < 1) {
# If 0 or negative integer, no user found or major failure
$RT::Logger->info((caller(0))[3],
"DISABLE CHECK FAILED",
$name_to_update,
"User not found");
# Drop out to next service for an info check
next;
} elsif ($user_found->count > 1) {
# If more than one result returned, die because we the username field should be unique!
$RT::Logger->info((caller(0))[3],
"DISABLE CHECK FAILED",
$name_to_update,
"More than one user with that username!");
# Drop out to next service for an info check
next;
}
undef $user_found;
# SECOND, now we know the user exists in the service,
# check if they are returned in a search for disabled users
my $disabled_users = $ldap->search(base => $ldap_base,
filter => $d_filter,
attrs => ['uid']);
# If ANY results are returned,
# we are going to assume the user should be disabled
if ($disabled_users->count) {
$user_disabled = 1;
}
# If we havent been dropped out by a "next;" by now,
# then this will be the authoritative service
} elsif ($config->{'type'} eq 'cookie') {
# The cookie module is currently only designed for pure authentication, no information
RT::Logger->error("I was asked to get user info from Cookie. I don't know how, I only *auth* on cookies.");
# Drop out to next source in list
next;
} else {
# The type of external service doesn't currently have any methods associated with it. Or it's a typo.
RT::Logger->error("Invalid type specification for config %config->{'name'}");
# Drop out to next service in list
next;
}
# We are now going to update the user's information from the authoritative source
# Although we are in a foreach, the statements below will only be executed once.
# The external services have been checked in the priority order specified by the config.
# If the user wasn't found in an individual service, we will already have jumped to the next one,
# or we will have dropped out to the return statement at the base of the function if the user wasn't
# found in ANY external services.
# The user must have been found in a service to get here so we run the update code
# and then "last" out of the foreach so that we only update from one source.
# and then return out of the function
# So, breathe, and on we go...
# Load the user inside an RT::SystemUser so you can set their
# information no matter who they are or what permissions they have
my $UserObj = RT::User->new($RT::SystemUser);
$UserObj->Load($name_to_update);
# If user is disabled, set the RT::Principle to disabled and return out of the function.
# I think it's a waste of time and energy to update a user's information if they are disabled
# and it could be a security risk if they've updated their external information with some
# carefully concocted code to try to break RT - worst case scenario, but they have been
# denied access after all, don't take any chances.
# If someone gives me a good enough reason to do it,
# then I'll update all the info for disabled users
if ($user_disabled) {
# Make sure principle is disabled in RT
my ($val, $message) = $UserObj->SetDisabled(1);
# Log what has happened
$RT::Logger->info("DISABLED user ",
$name_to_update,
"per External Database",
"($val, $message)\n");
$msg = "User disabled";
} else {
# Make sure principle is not disabled in RT
my ($val, $message) = $UserObj->SetDisabled(0);
# Log what has happened
$RT::Logger->info("ENABLED user ",
$name_to_update,
"per External Database",
"($val, $message)\n");
# Update their info from external servicen using the username as the lookup key
# CanonicalizeUserInfo will work out for itself which service to use
# Passing it a service instead could break other RT code
my %args = (Name => $name_to_update);
$self->CanonicalizeUserInfo(\%args);
# For each piece of information returned by CanonicalizeUserInfo,
# run the Set method for that piece of info to change it for the user
foreach my $key (sort(keys(%args))) {
next unless $args{$key};
my $method = "Set$key";
# We do this on the UserObj from above, not self so that there
# are no permission restrictions on setting information
my ($method_success,$method_msg) = $UserObj->$method($args{$key});
if ($method_success) {
# At DEBUG level, log that method succeeded
$RT::Logger->debug("$method Succeeded. $method_msg");
} else {
# At WARNING level, log that method failed
$RT::Logger->warning("$method Failed. $method_msg");
}
}
# Confirm update success
$updated = 1;
$RT::Logger->debug("UPDATED user ",
$name_to_update,
"from External Database\n");
$msg = 'User updated';
# Just in case we're not the last iteration of the foreach,
# drop out to the return statement now.
last;
}
}
return ($updated, $msg);
}
# {{{ sub _GetBoundLdapObj
sub _GetBoundLdapObj {
my $self = shift;
# Config as hashref
my $config = shift;
# Figure out what's what
my $ldap_server = $config->{'server'};
my $ldap_user = $config->{'user'};
my $ldap_pass = $config->{'pass'};
my $ldap_tls = $config->{'tls'};
my $ldap_ssl_ver = $config->{'ssl_version'};
my @ldap_args = $config->{'net_ldap_args'};
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;
# Can't decide whether to add a little more error checking here..
# Perhaps, if user && pass, else dont pass a pass etc..
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 _GetBoundDBIObj
sub _GetBoundDBIObj {
my $self = shift;
# Config as hashref.
my $config = shift;
# Extract the relevant information from the config.
my $db_server = $config->{'server'};
my $db_user = $config->{'user'};
my $db_pass = $config->{'pass'};
my $db_database = $config->{'database'};
my $db_port = $config->{'port'};
my $dbi_driver = $config->{'dbi_driver'};
# Use config to create a DSN line for the DBI connection
my $dsn = "dbi:$dbi_driver:database=$db_database;host=$db_server;port=$db_port";
# Now let's get connected
my $dbh = DBI->connect($dsn, $db_user, $db_pass,{RaiseError => 1, AutoCommit => 0 })
or die $DBI::errstr;
# If we didn't die, return the DBI object handle
# and hope it's treated sensibly and correctly
# destroyed by the calling code
return $dbh;
}
# }}}
1;