RT::Authen::ExternalAuth - RT Authentication using External Sources
Index
Code Index:
NAME

RT::Authen::ExternalAuth - RT Authentication using External Sources
DESCRIPTION

A complete package for adding external authentication mechanisms
to RT. It currently supports LDAP via Net::LDAP and External Database
authentication for any database with an installed DBI driver.
It also allows for authenticating cookie information against an
external database through the use of the RT-Authen-CookieAuth extension.
package RT::Authen::ExternalAuth;
our $VERSION = '0.09';
use RT::Authen::ExternalAuth::LDAP;
use RT::Authen::ExternalAuth::DBI;
use strict;
sub DoAuth {
my ($session,$given_user,$given_pass) = @_;
unless(defined($RT::ExternalAuthPriority)) {
return (0, "ExternalAuthPriority not defined, please check your configuration file.");
}
my $no_info_check = 0;
unless(defined($RT::ExternalInfoPriority)) {
$RT::Logger->debug("ExternalInfoPriority not defined. User information (including user enabled/disabled cannot be externally-sourced");
$no_info_check = 1;
}
# This may be used by single sign-on (SSO) authentication mechanisms for bypassing a password check.
my $pass_bypass = 0;
my $success = 0;
# Should have checked if user is already logged in before calling this function,
# but just in case, we'll check too.
return (0, "User already logged in!") if ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id);
# We don't have a logged in user. Let's try all our available methods in order.
# last if success, next if not.
# Get the prioritised list of external authentication services
my @auth_services = @$RT::ExternalAuthPriority;
# For each of those services..
foreach my $service (@auth_services) {
$pass_bypass = 0;
# Get the full configuration for that service as a hashref
my $config = $RT::ExternalSettings->{$service};
$RT::Logger->debug( "Attempting to use external auth service:",
$service);
# $username will be the final username we decide to check
# This will not necessarily be $given_user
my $username = undef;
#############################################################
####################### SSO Check ###########################
#############################################################
if ($config->{'type'} eq 'cookie') {
# Currently, Cookie authentication is our only SSO method
$username = RT::Authen::ExternalAuth::DBI::GetCookieAuth($config);
}
#############################################################
# If $username is defined, we have a good SSO $username and can
# safely bypass the password checking later on; primarily because
# it's VERY unlikely we even have a password to check if an SSO succeeded.
$pass_bypass = 0;
if(defined($username)) {
$RT::Logger->debug("Pass not going to be checked, attempting SSO");
$pass_bypass = 1;
} else {
# SSO failed and no $user was passed for a login attempt
# We only don't return here because the next iteration could be an SSO attempt
unless(defined($given_user)) {
$RT::Logger->debug("SSO Failed and no user to test with. Nexting");
next;
}
# We don't have an SSO login, so we will be using the credentials given
# on RT's login page to do our authentication.
$username = $given_user;
# Don't continue unless the service works.
# next unless RT::Authen::ExternalAuth::TestConnection($config);
# Don't continue unless the $username exists in the external service
$RT::Logger->debug("Calling UserExists with \$username ($username) and \$service ($service)");
next unless RT::Authen::ExternalAuth::UserExists($username, $service);
}
####################################################################
########## Load / Auto-Create ######################################
####################################################################
# We are now sure that we're talking about a valid RT user.
# If the user already exists, load up their info. If they don't
# then we need to create the user in RT.
# Does user already exist internally to RT?
$session->{'CurrentUser'} = RT::CurrentUser->new();
$session->{'CurrentUser'}->Load($username);
# Unless we have loaded a valid user with a UserID create one.
unless ($session->{'CurrentUser'}->Id) {
my $UserObj = RT::User->new($RT::SystemUser);
my ($val, $msg) =
$UserObj->Create(%{ref($RT::AutoCreate) ? $RT::AutoCreate : {}},
Name => $username,
Gecos => $username,
);
unless ($val) {
$RT::Logger->error( "Couldn't create user $username: $msg" );
next;
}
$RT::Logger->info( "Autocreated external user",
$UserObj->Name,
"(",
$UserObj->Id,
")");
$RT::Logger->debug("Loading new user (",
$username,
") into current session");
$session->{'CurrentUser'}->Load($username);
}
####################################################################
########## Authentication ##########################################
####################################################################
# If we successfully used an SSO service, then authentication
# succeeded. If we didn't then, success is determined by a password
# test.
$success = 0;
if($pass_bypass) {
$RT::Logger->debug("Password check bypassed due to SSO method being in use");
$success = 1;
} else {
$RT::Logger->debug("Password validation required for service - Executing...");
$success = RT::Authen::ExternalAuth::GetAuth($service,$username,$given_pass);
}
$RT::Logger->debug("Password Validation Check Result: ",$success);
# If the password check succeeded then this is our authoritative service
# and we proceed to user information update and login.
last if $success;
}
# If we got here and don't have a user loaded we must have failed to
# get a full, valid user from an authoritative external source.
unless ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id) {
delete $session->{'CurrentUser'};
return (0, "No User");
}
unless($success) {
delete $session->{'CurrentUser'};
return (0, "Password Invalid");
}
# Otherwise we succeeded.
$RT::Logger->debug("Authentication successful. Now updating user information and attempting login.");
####################################################################################################
############################### The following is auth-method agnostic ##############################
####################################################################################################
# If we STILL have a completely valid RT user to play with...
# and therefore password has been validated...
if ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id) {
# Even if we have JUST created the user in RT, we are going to
# reload their information from an external source. This allows us
# to be sure that the user the cookie gave us really does exist in
# the database, but more importantly, UpdateFromExternal will check
# whether the user is disabled or not which we have not been able to
# do during auto-create
# These are not currently used, but may be used in the future.
my $info_updated = 0;
my $info_updated_msg = "User info not updated";
unless($no_info_check) {
# Note that UpdateUserInfo does not care how we authenticated the user
# It will look up user info from whatever is specified in $RT::ExternalInfoPriority
($info_updated,$info_updated_msg) = RT::Authen::ExternalAuth::UpdateUserInfo($session->{'CurrentUser'}->Name);
}
# Now that we definitely have up-to-date user information,
# if the user is disabled, kick them out. Now!
if ($session->{'CurrentUser'}->UserObj->Disabled) {
delete $session->{'CurrentUser'};
return (0, "User account disabled, login denied");
}
}
# If we **STILL** have a full user and the session hasn't already been deleted
# This If/Else is logically unnecessary, but it doesn't hurt to leave it here
# just in case. Especially to be a double-check to future modifications.
if ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id) {
$RT::Logger->info( "Successful login for",
$session->{'CurrentUser'}->Name,
"from",
$ENV{'REMOTE_ADDR'});
# Do not delete the session. User stays logged in and
# autohandler will not check the password again
} else {
# Make SURE the session is deleted.
delete $session->{'CurrentUser'};
return (0, "Failed to authenticate externally");
# This will cause autohandler to request IsPassword
# which will in turn call IsExternalPassword
}
return (1, "Successful login");
}
sub UpdateUserInfo {
my $username = shift;
# Prepare for the worst...
my $found = 0;
my $updated = 0;
my $msg = "User NOT updated";
my $user_disabled = RT::Authen::ExternalAuth::UserDisabled($username);
my $UserObj = RT::User->new($RT::SystemUser);
$UserObj->Load($username);
# 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("User marked as DISABLED (",
$username,
") per External Service",
"($val, $message)\n");
$msg = "User Disabled";
return ($updated, $msg);
}
# Make sure principle is not disabled in RT
my ($val, $message) = $UserObj->SetDisabled(0);
# Log what has happened
$RT::Logger->info("User marked as ENABLED (",
$username,
") per External Service",
"($val, $message)\n");
# Update their info from external service 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 => $username);
$UserObj->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 your user information is not getting updated,
# uncomment the following logging statements
if ($method_success) {
# At DEBUG level, log that method succeeded
# $RT::Logger->debug((caller(0))[3],"$method Succeeded. $method_msg");
} else {
# At DEBUG level, log that method failed
# $RT::Logger->debug((caller(0))[3],"$method Failed. $method_msg");
}
}
# Confirm update success
$updated = 1;
$RT::Logger->debug( "UPDATED user (",
$username,
") from External Service\n");
$msg = 'User updated';
return ($updated, $msg);
}
sub GetAuth {
# Request a username/password check from the specified service
# This is only valid for non-SSO services.
my ($service,$username,$password) = @_;
my $success = 0;
# 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 non-SSO services
if ($config->{'type'} eq 'db') {
$success = RT::Authen::ExternalAuth::DBI::GetAuth($service,$username,$password);
$RT::Logger->debug("DBI password validation result:",$success);
} elsif ($config->{'type'} eq 'ldap') {
$success = RT::Authen::ExternalAuth::LDAP::GetAuth($service,$username,$password);
$RT::Logger->debug("LDAP password validation result:",$success);
} else {
$RT::Logger->error("Invalid service type for GetAuth:",$service);
}
return $success;
}
sub UserExists {
# Request a username/password check from the specified service
# This is only valid for non-SSO services.
my ($username,$service) = @_;
my $success = 0;
# 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 non-SSO services
if ($config->{'type'} eq 'db') {
$success = RT::Authen::ExternalAuth::DBI::UserExists($username,$service);
} elsif ($config->{'type'} eq 'ldap') {
$success = RT::Authen::ExternalAuth::LDAP::UserExists($username,$service);
} else {
$RT::Logger->debug("Invalid service type for UserExists:",$service);
}
return $success;
}
sub UserDisabled {
my $username = shift;
my $user_disabled = 0;
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.
unless(defined($config)) {
$RT::Logger->debug("You haven't defined a configuration for the service named \"",
$service,
"\" so I'm not going to try to get user information from it. Skipping...");
next;
}
# If it's a DBI config:
if ($config->{'type'} eq 'db') {
unless(RT::Authen::ExternalAuth::DBI::UserExists($username,$service)) {
$RT::Logger->debug("User (",
$username,
") doesn't exist in service (",
$service,
") - Cannot update information - Skipping...");
next;
}
$user_disabled = RT::Authen::ExternalAuth::DBI::UserDisabled($username,$service);
} elsif ($config->{'type'} eq 'ldap') {
unless(RT::Authen::ExternalAuth::LDAP::UserExists($username,$service)) {
$RT::Logger->debug("User (",
$username,
") doesn't exist in service (",
$service,
") - Cannot update information - Skipping...");
next;
}
$user_disabled = RT::Authen::ExternalAuth::LDAP::UserDisabled($username,$service);
} elsif ($config->{'type'} eq 'cookie') {
RT::Logger->error("You cannot use SSO Cookies as an information service.");
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;
}
}
return $user_disabled;
}
sub CanonicalizeUserInfo {
# Careful, this $args hashref was given to RT::User::CanonicalizeUserInfo and
# then transparently passed on to this function. The whole purpose is to update
# the original hash as whatever passed it to RT::User is expecting to continue its
# code with an update args hash.
my $UserObj = shift;
my $args = shift;
my $found = 0;
my %params = (Name => undef,
EmailAddress => undef,
RealName => undef);
$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) {
$RT::Logger->debug( "Attempting to get user info using this external service:",
$service);
# Get the config for the service so that we know what attrs we can canonicalize
my $config = $RT::ExternalSettings->{$service};
if($config->{'type'} eq 'cookie'){
$RT::Logger->debug("You cannot use SSO cookies as an information service!");
next;
}
# For each attr we've been told to canonicalize in the match list
foreach my $rt_attr (@{$config->{'attr_match_list'}}) {
# Jump to the next attr in $args if this one isn't in the attr_match_list
$RT::Logger->debug( "Attempting to use this canonicalization key:",$rt_attr);
unless(defined($args->{$rt_attr})) {
$RT::Logger->debug("This attribute (",
$rt_attr,
") is null or incorrectly defined in the attr_map for this service (",
$service,
")");
next;
}
# Else, use it as a canonicalization key and lookup the user info
my $key = $config->{'attr_map'}->{$rt_attr};
my $value = $args->{$rt_attr};
# Check to see that the key being asked for is defined in the config's attr_map
my $valid = 0;
my ($attr_key, $attr_value);
my $attr_map = $config->{'attr_map'};
while (($attr_key, $attr_value) = each %$attr_map) {
$valid = 1 if ($key eq $attr_value);
}
unless ($valid){
$RT::Logger->debug( "This key (",
$key,
"is not a valid attribute key (",
$service,
")");
next;
}
# 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'){
($found, %params) = RT::Authen::ExternalAuth::LDAP::CanonicalizeUserInfo($service,$key,$value);
} elsif ($config->{'type'} eq 'db') {
($found, %params) = RT::Authen::ExternalAuth::DBI::CanonicalizeUserInfo($service,$key,$value);
} else {
$RT::Logger->debug( (caller(0))[3],
"does not consider",
$service,
"a valid information service");
}
# 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'} = $UserObj->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);
}
{
no warnings 'redefine';
*RT::User::CanonicalizeUserInfo = sub {
my $self = shift;
my $args = shift;
return ( CanonicalizeUserInfo( $self, $args ) );
};
}
1;