Advertisement
lucianoes

ExternalAuth.pm mod for numeric LDAP uid

Oct 3rd, 2011
226
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Perl 23.84 KB | None | 0 0
  1. package RT::Authen::ExternalAuth;
  2.  
  3. our $VERSION = '0.09';
  4.  
  5. =head1 NAME
  6.  
  7.   RT::Authen::ExternalAuth - RT Authentication using External Sources
  8.  
  9. =head1 DESCRIPTION
  10.  
  11.   A complete package for adding external authentication mechanisms
  12.   to RT. It currently supports LDAP via Net::LDAP and External Database
  13.   authentication for any database with an installed DBI driver.
  14.  
  15.   It also allows for authenticating cookie information against an
  16.   external database through the use of the RT-Authen-CookieAuth extension.
  17.  
  18.   MODIFIED IN 2011/10/01 By Luciano Silva - lucianoes at ufrgs dot br
  19.   This version is only numeric UID LDAP. Use at your own risk.     
  20.  
  21. =begin testing
  22.  
  23. ok(require RT::Authen::ExternalAuth);
  24.  
  25. =end testing
  26.  
  27. =cut    
  28.  
  29. use RT::Authen::ExternalAuth::LDAP;
  30. use RT::Authen::ExternalAuth::DBI;
  31.  
  32. use strict;
  33.  
  34. sub DoAuth {
  35.     my ($session,$given_user,$given_pass) = @_;
  36.  
  37.     unless(defined($RT::ExternalAuthPriority)) {
  38.         return (0, "ExternalAuthPriority not defined, please check your configuration file.");
  39.     }
  40.  
  41.     my $no_info_check = 0;
  42.     unless(defined($RT::ExternalInfoPriority)) {
  43.         $RT::Logger->debug("ExternalInfoPriority not defined. User information (including user enabled/disabled cannot be externally-sourced");
  44.         $no_info_check = 1;
  45.     }
  46.  
  47.     # This may be used by single sign-on (SSO) authentication mechanisms for bypassing a password check.
  48.     my $pass_bypass = 0;
  49.     my $success = 0;
  50.  
  51.     # Should have checked if user is already logged in before calling this function,
  52.     # but just in case, we'll check too.
  53.     return (0, "User already logged in!") if ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id);
  54.     # We don't have a logged in user. Let's try all our available methods in order.
  55.     # last if success, next if not.
  56.    
  57.     # Get the prioritised list of external authentication services
  58.     my @auth_services = @$RT::ExternalAuthPriority;
  59.    
  60.     # For each of those services..
  61.     foreach my $service (@auth_services) {
  62.  
  63.     $pass_bypass = 0;
  64.  
  65.         # Get the full configuration for that service as a hashref
  66.         my $config = $RT::ExternalSettings->{$service};
  67.         $RT::Logger->debug( "Attempting to use external auth service:",
  68.                             $service);
  69.  
  70.         # $username will be the final username we decide to check
  71.         # This will not necessarily be $given_user
  72.         my $username = undef;
  73.        
  74.         #############################################################
  75.         ####################### SSO Check ###########################
  76.         #############################################################
  77.         if ($config->{'type'} eq 'cookie') {    
  78.             # Currently, Cookie authentication is our only SSO method
  79.             $username = RT::Authen::ExternalAuth::DBI::GetCookieAuth($config);
  80.         }
  81.         #############################################################
  82.        
  83.         # If $username is defined, we have a good SSO $username and can
  84.         # safely bypass the password checking later on; primarily because
  85.         # it's VERY unlikely we even have a password to check if an SSO succeeded.
  86.         $pass_bypass = 0;
  87.     if(defined($username)) {
  88.         $RT::Logger->debug("Pass not going to be checked, attempting SSO");
  89.             $pass_bypass = 1;
  90.         } else {
  91.  
  92.         # SSO failed and no $user was passed for a login attempt
  93.         # We only don't return here because the next iteration could be an SSO attempt
  94.         unless(defined($given_user)) {
  95.             $RT::Logger->debug("SSO Failed and no user to test with. Nexting");
  96.         next;
  97.         }
  98.  
  99.             # We don't have an SSO login, so we will be using the credentials given
  100.             # on RT's login page to do our authentication.
  101.             $username = $given_user;
  102.    
  103.             # Don't continue unless the service works.
  104.         # next unless RT::Authen::ExternalAuth::TestConnection($config);
  105.  
  106.             # Don't continue unless the $username exists in the external service
  107.  
  108.         $RT::Logger->debug("Calling UserExists with \$username ($username) and \$service ($service)");
  109.             next unless RT::Authen::ExternalAuth::UserExists($username, $service);
  110.         }
  111.  
  112.         ####################################################################
  113.         ########## Load / Auto-Create ######################################
  114.         ####################################################################
  115.         # We are now sure that we're talking about a valid RT user.
  116.         # If the user already exists, load up their info. If they don't
  117.         # then we need to create the user in RT.
  118.     #MODIF
  119.     my $MagicUid = "a";
  120.     #END_MODIF
  121.         # Does user already exist internally to RT?
  122.     $RT::Logger->debug("Auto-Create process, this is where the magic really happens!! Put the A letter befone the UID");
  123.         $session->{'CurrentUser'} = RT::CurrentUser->new();
  124.         $session->{'CurrentUser'}->Load($MagicUid . $username);
  125.         #$session->{'CurrentUser'}->Load($username);
  126.    
  127.         # Unless we have loaded a valid user with a UserID create one.
  128.         unless ($session->{'CurrentUser'}->Id) {
  129.             my $UserObj = RT::User->new($RT::SystemUser);
  130.             my ($val, $msg) =
  131.               $UserObj->Create(%{ref($RT::AutoCreate) ? $RT::AutoCreate : {}},
  132.                                Name   => ("a" . $username),
  133.                    Gecos  => $username,
  134.                               );
  135.             unless ($val) {
  136.                 $RT::Logger->error( "Couldn't create user $username: $msg" );
  137.                 next;
  138.             }
  139.             $RT::Logger->info(  "Autocreated external user",
  140.                                 $UserObj->Name,
  141.                                 "(",
  142.                                 $UserObj->Id,
  143.                                 ")");
  144.            
  145.             $RT::Logger->debug("Loading new user (",
  146.                                 $username,
  147.                                 ") into current session");
  148.         $session->{'CurrentUser'}->Load("a" . $username);
  149.         $RT::Logger->debug("Did the system load the the userseme with a ? Otherwise the next step will fail.");
  150.  
  151.         }
  152.        
  153.         ####################################################################
  154.         ########## Authentication ##########################################
  155.         ####################################################################
  156.         # If we successfully used an SSO service, then authentication
  157.         # succeeded. If we didn't then, success is determined by a password
  158.         # test.
  159.         $success = 0;
  160.     if($pass_bypass) {
  161.             $RT::Logger->debug("Password check bypassed due to SSO method being in use");
  162.             $success = 1;
  163.         } else {
  164.             $RT::Logger->debug("Password validation required for service - Executing...");
  165.             $success = RT::Authen::ExternalAuth::GetAuth($service,$username,$given_pass);
  166.         }
  167.        
  168.         $RT::Logger->debug("Password Validation Check Result: ",$success);
  169.  
  170.         # If the password check succeeded then this is our authoritative service
  171.         # and we proceed to user information update and login.
  172.         last if $success;
  173.     }
  174.    
  175.     # If we got here and don't have a user loaded we must have failed to
  176.     # get a full, valid user from an authoritative external source.
  177.     unless ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id) {
  178.         delete $session->{'CurrentUser'};
  179.         return (0, "No User");
  180.     }
  181.  
  182.     unless($success) {
  183.         delete $session->{'CurrentUser'};
  184.     return (0, "Password Invalid");
  185.     }
  186.    
  187.     # Otherwise we succeeded.
  188.     $RT::Logger->debug("Authentication successful. Now updating user information and attempting login.");
  189.        
  190.     ####################################################################################################
  191.     ############################### The following is auth-method agnostic ##############################
  192.     ####################################################################################################
  193.    
  194.     # If we STILL have a completely valid RT user to play with...
  195.     # and therefore password has been validated...
  196.     if ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id) {
  197.        
  198.         # Even if we have JUST created the user in RT, we are going to
  199.         # reload their information from an external source. This allows us
  200.         # to be sure that the user the cookie gave us really does exist in
  201.         # the database, but more importantly, UpdateFromExternal will check
  202.         # whether the user is disabled or not which we have not been able to
  203.         # do during auto-create
  204.  
  205.     # These are not currently used, but may be used in the future.
  206.     my $info_updated = 0;
  207.     my $info_updated_msg = "User info not updated";
  208.  
  209.         unless($no_info_check) {
  210.             # Note that UpdateUserInfo does not care how we authenticated the user
  211.             # It will look up user info from whatever is specified in $RT::ExternalInfoPriority
  212.             ($info_updated,$info_updated_msg) = RT::Authen::ExternalAuth::UpdateUserInfo($session->{'CurrentUser'}->Name);
  213.         }
  214.                
  215.         # Now that we definitely have up-to-date user information,
  216.         # if the user is disabled, kick them out. Now!
  217.         if ($session->{'CurrentUser'}->UserObj->Disabled) {
  218.             delete $session->{'CurrentUser'};
  219.             return (0, "User account disabled, login denied");
  220.         }
  221.     }
  222.    
  223.     # If we **STILL** have a full user and the session hasn't already been deleted
  224.     # This If/Else is logically unnecessary, but it doesn't hurt to leave it here
  225.     # just in case. Especially to be a double-check to future modifications.
  226.     if ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id) {
  227.            
  228.             $RT::Logger->info(  "Successful login for",
  229.                                 $session->{'CurrentUser'}->Name,
  230.                                 "from",
  231.                                 $ENV{'REMOTE_ADDR'});
  232.             # Do not delete the session. User stays logged in and
  233.             # autohandler will not check the password again
  234.     } else {
  235.             # Make SURE the session is deleted.
  236.             delete $session->{'CurrentUser'};
  237.             return (0, "Failed to authenticate externally");
  238.             # This will cause autohandler to request IsPassword
  239.             # which will in turn call IsExternalPassword
  240.     }
  241.    
  242.     return (1, "Successful login");
  243. }
  244.  
  245. sub UpdateUserInfo {
  246.     my $username        = shift;
  247.  
  248.     # Prepare for the worst...
  249.     my $found           = 0;
  250.     my $updated         = 0;
  251.     my $msg             = "User NOT updated";
  252.  
  253.     my $user_disabled   = RT::Authen::ExternalAuth::UserDisabled($username);
  254.  
  255.     my $UserObj = RT::User->new($RT::SystemUser);
  256.     $UserObj->Load($username);        
  257.  
  258.     # If user is disabled, set the RT::Principle to disabled and return out of the function.
  259.     # I think it's a waste of time and energy to update a user's information if they are disabled
  260.     # and it could be a security risk if they've updated their external information with some
  261.     # carefully concocted code to try to break RT - worst case scenario, but they have been
  262.     # denied access after all, don't take any chances.
  263.      
  264.     # If someone gives me a good enough reason to do it,
  265.     # then I'll update all the info for disabled users
  266.  
  267.     if ($user_disabled) {
  268.         # Make sure principle is disabled in RT
  269.         my ($val, $message) = $UserObj->SetDisabled(1);
  270.         # Log what has happened
  271.         $RT::Logger->info("User marked as DISABLED (",
  272.                             $username,
  273.                             ") per External Service",
  274.                             "($val, $message)\n");
  275.         $msg = "User Disabled";
  276.        
  277.         return ($updated, $msg);
  278.     }    
  279.        
  280.     # Make sure principle is not disabled in RT
  281.     my ($val, $message) = $UserObj->SetDisabled(0);
  282.     # Log what has happened
  283.     $RT::Logger->info("User marked as ENABLED (",
  284.                         $username,
  285.                         ") per External Service",
  286.                         "($val, $message)\n");
  287.  
  288.     # Update their info from external service using the username as the lookup key
  289.     # CanonicalizeUserInfo will work out for itself which service to use
  290.     # Passing it a service instead could break other RT code
  291.     #MODIFICACAO - Incluido o substring no $username para remover o caracter a
  292.     my %args = (Name => substr($username,1));
  293.     $UserObj->CanonicalizeUserInfo(\%args);
  294.  
  295.     # For each piece of information returned by CanonicalizeUserInfo,
  296.     # run the Set method for that piece of info to change it for the user
  297.     foreach my $key (sort(keys(%args))) {
  298.         next unless $args{$key};
  299.         my $method = "Set$key";
  300.         # We do this on the UserObj from above, not self so that there
  301.         # are no permission restrictions on setting information
  302.         my ($method_success,$method_msg) = $UserObj->$method($args{$key});
  303.        
  304.         # If your user information is not getting updated,
  305.         # uncomment the following logging statements
  306.         if ($method_success) {
  307.             # At DEBUG level, log that method succeeded
  308.              $RT::Logger->debug((caller(0))[3],"$method Succeeded. $method_msg");
  309.         } else {
  310.             # At DEBUG level, log that method failed
  311.              $RT::Logger->debug((caller(0))[3],"$method Failed. $method_msg");
  312.         }
  313.     }
  314.  
  315.     # Confirm update success
  316.     $updated = 1;
  317.     $RT::Logger->debug( "UPDATED user (",
  318.                         $username,
  319.                         ") from External Service\n");
  320.     $msg = 'User updated';
  321.  
  322.     return ($updated, $msg);
  323. }
  324.  
  325. sub GetAuth {
  326.  
  327.     # Request a username/password check from the specified service
  328.     # This is only valid for non-SSO services.
  329.    
  330.     my ($service,$username,$password) = @_;
  331.    
  332.     my $success = 0;
  333.    
  334.     # Get the full configuration for that service as a hashref
  335.     my $config = $RT::ExternalSettings->{$service};
  336.    
  337.     # And then act accordingly depending on what type of service it is.
  338.     # Right now, there is only code for DBI and LDAP non-SSO services
  339.     if ($config->{'type'} eq 'db') {    
  340.         $success = RT::Authen::ExternalAuth::DBI::GetAuth($service,$username,$password);
  341.     $RT::Logger->debug("DBI password validation result:",$success);
  342.     } elsif ($config->{'type'} eq 'ldap') {
  343.         $success = RT::Authen::ExternalAuth::LDAP::GetAuth($service,$username,$password);
  344.     $RT::Logger->debug("LDAP password validation result:",$success);
  345.     } else {
  346.         $RT::Logger->error("Invalid service type for GetAuth:",$service);
  347.     }
  348.    
  349.     return $success;
  350. }
  351.  
  352. sub UserExists {
  353.  
  354.     # Request a username/password check from the specified service
  355.     # This is only valid for non-SSO services.
  356. #    my $removemagic = substr($username, 1);   
  357. #    my ($removemagic,$service) = @_;
  358.     my ($username,$service) = @_;
  359.  
  360.     my $success = 0;
  361.  
  362.     # Get the full configuration for that service as a hashref
  363.     my $config = $RT::ExternalSettings->{$service};
  364.  
  365.     # And then act accordingly depending on what type of service it is.
  366.     # Right now, there is only code for DBI and LDAP non-SSO services
  367.     if ($config->{'type'} eq 'db') {
  368.         $success = RT::Authen::ExternalAuth::DBI::UserExists($username,$service);
  369.     } elsif ($config->{'type'} eq 'ldap') {
  370.     my $magicnewname = substr($username, '0', 8);
  371.     my $username = $magicnewname;
  372.     $success = RT::Authen::ExternalAuth::LDAP::UserExists($username,$service);
  373.     } else {
  374.         $RT::Logger->debug("Invalid service type for UserExists:",$service);
  375.     }
  376.  
  377.     return $success;
  378. }
  379.  
  380. sub UserDisabled {
  381.     $RT::Logger->debug("Now is starting UserDisabled function");   
  382.     my $username = shift;
  383.     my $user_disabled = 0;
  384.    
  385.     my @info_services = $RT::ExternalInfoPriority ? @{$RT::ExternalInfoPriority} : undef;
  386.  
  387.     # For each named service in the list
  388.     # Check to see if the user is found in the external service
  389.     # If not found, jump to next service
  390.     # If found, check to see if user is considered disabled by the service
  391.     # Then update the user's info in RT and return
  392.     foreach my $service (@info_services) {
  393.        
  394.         # Get the external config for this service as a hashref        
  395.         my $config = $RT::ExternalSettings->{$service};
  396.        
  397.         # If the config doesn't exist, don't bother doing anything, skip to next in list.
  398.         unless(defined($config)) {
  399.             $RT::Logger->debug("You haven't defined a configuration for the service named \"",
  400.                                 $service,
  401.                                 "\" so I'm not going to try to get user information from it. Skipping...");
  402.             next;
  403.         }
  404.        
  405.         # If it's a DBI config:
  406.         if ($config->{'type'} eq 'db') {
  407.            
  408.             unless(RT::Authen::ExternalAuth::DBI::UserExists($username,$service)) {
  409.                 $RT::Logger->debug("User (",
  410.                                     $username,
  411.                                     ") doesn't exist in service (",
  412.                                     $service,
  413.                                     ") - Cannot update information - Skipping...");
  414.                 next;
  415.             }
  416.             $user_disabled = RT::Authen::ExternalAuth::DBI::UserDisabled($username,$service);
  417.            
  418.         } elsif ($config->{'type'} eq 'ldap') {
  419.     $RT::Logger->debug("Removing special character A from the username.");            
  420.             unless(RT::Authen::ExternalAuth::LDAP::UserExists(substr($username,1),$service)) {
  421.                 $RT::Logger->debug("User (",
  422.                                     $username,
  423.                                     ") doesn't exist in service (",
  424.                                     $service,
  425.                                     ") - Cannot update information - Skipping...");
  426.                 next;
  427.             }
  428.     $RT::Logger->debug("New check on LDAP the username, but removing the special character A from the username."); 
  429.             $user_disabled = RT::Authen::ExternalAuth::LDAP::UserDisabled(substr($username,1),$service);
  430.                    
  431.         } elsif ($config->{'type'} eq 'cookie') {
  432.             RT::Logger->error("You cannot use SSO Cookies as an information service.");
  433.             next;
  434.         } else {
  435.             # The type of external service doesn't currently have any methods associated with it. Or it's a typo.
  436.             RT::Logger->error("Invalid type specification for config %config->{'name'}");
  437.             # Drop out to next service in list
  438.             next;
  439.         }
  440.    
  441.     }
  442.     return $user_disabled;
  443. }
  444.  
  445. sub CanonicalizeUserInfo {
  446.      $RT::Logger->debug("Now is starting CanonicalizeUserInfo function");
  447.     # Careful, this $args hashref was given to RT::User::CanonicalizeUserInfo and
  448.     # then transparently passed on to this function. The whole purpose is to update
  449.     # the original hash as whatever passed it to RT::User is expecting to continue its
  450.     # code with an update args hash.
  451.    
  452.     my $UserObj = shift;
  453.     my $args    = shift;
  454.    
  455.     my $found   = 0;
  456.     my %params  = (Name         => undef,
  457.                   EmailAddress => undef,
  458.                   RealName     => undef);
  459.    
  460.     $RT::Logger->debug( (caller(0))[3],
  461.                         "called by",
  462.                         caller,
  463.                         "with:",
  464.                         join(", ", map {sprintf("%s: %s", $_, $args->{$_})}
  465.                             sort(keys(%$args))));
  466.  
  467.     # Get the list of defined external services
  468.     my @info_services = $RT::ExternalInfoPriority ? @{$RT::ExternalInfoPriority} : undef;
  469.     # For each external service...
  470.     foreach my $service (@info_services) {
  471.        
  472.         $RT::Logger->debug( "Attempting to get user info using this external service:",
  473.                             $service);
  474.        
  475.         # Get the config for the service so that we know what attrs we can canonicalize
  476.         my $config = $RT::ExternalSettings->{$service};
  477.        
  478.         if($config->{'type'} eq 'cookie'){
  479.             $RT::Logger->debug("You cannot use SSO cookies as an information service!");
  480.             next;
  481.         }  
  482.        
  483.         # For each attr we've been told to canonicalize in the match list
  484.         foreach my $rt_attr (@{$config->{'attr_match_list'}}) {
  485.             # Jump to the next attr in $args if this one isn't in the attr_match_list
  486.             $RT::Logger->debug( "Attempting to use this canonicalization key:",$rt_attr);
  487.             unless(defined($args->{$rt_attr})) {
  488.                 $RT::Logger->debug("This attribute (",
  489.                                     $rt_attr,
  490.                                     ") is null or incorrectly defined in the attr_map for this service (",
  491.                                     $service,
  492.                                     ")");
  493.                 next;
  494.             }
  495.                                
  496.             # Else, use it as a canonicalization key and lookup the user info    
  497.             my $key = $config->{'attr_map'}->{$rt_attr};
  498.             my $value = $args->{$rt_attr};
  499.            
  500.             # Check to see that the key being asked for is defined in the config's attr_map
  501.             my $valid = 0;
  502.             my ($attr_key, $attr_value);
  503.             my $attr_map = $config->{'attr_map'};
  504.             while (($attr_key, $attr_value) = each %$attr_map) {
  505.                 $valid = 1 if ($key eq $attr_value);
  506.             }
  507.             unless ($valid){
  508.                 $RT::Logger->debug( "This key (",
  509.                                     $key,
  510.                                     "is not a valid attribute key (",
  511.                                     $service,
  512.                                     ")");
  513.                 next;
  514.             }
  515.            
  516.             # Use an if/elsif structure to do a lookup with any custom code needed
  517.             # for any given type of external service, or die if no code exists for
  518.             # the service requested.
  519.            
  520.             if($config->{'type'} eq 'ldap'){    
  521.                 ($found, %params) = RT::Authen::ExternalAuth::LDAP::CanonicalizeUserInfo($service,$key,$value);
  522.             } elsif ($config->{'type'} eq 'db') {
  523.                 ($found, %params) = RT::Authen::ExternalAuth::DBI::CanonicalizeUserInfo($service,$key,$value);
  524.             } else {
  525.                 $RT::Logger->debug( (caller(0))[3],
  526.                                     "does not consider",
  527.                                     $service,
  528.                                     "a valid information service");
  529.             }
  530.        
  531.             # Don't Check any more attributes
  532.             last if $found;
  533.         }
  534.         # Don't Check any more services
  535.         last if $found;
  536.     }
  537.    
  538.     # If found, Canonicalize Email Address and
  539.     # update the args hash that we were given the hashref for
  540.     if ($found) {
  541.         # It's important that we always have a canonical email address
  542.         if ($params{'EmailAddress'}) {
  543.             $params{'EmailAddress'} = $UserObj->CanonicalizeEmailAddress($params{'EmailAddress'});
  544.         }
  545.         %$args = (%$args, %params);
  546.     }
  547.  
  548.     $RT::Logger->info(  (caller(0))[3],
  549.                         "returning",
  550.                         join(", ", map {sprintf("%s: %s", $_, $args->{$_})}
  551.                             sort(keys(%$args))));
  552.  
  553.     ### HACK: The config var below is to overcome the (IMO) bug in
  554.     ### RT::User::Create() which expects this function to always
  555.     ### return true or rejects the user for creation. This should be
  556.     ### a different config var (CreateUncanonicalizedUsers) and
  557.     ### should be honored in RT::User::Create()
  558.     return($found || $RT::AutoCreateNonExternalUsers);
  559.    
  560. }
  561.  
  562. {
  563.     no warnings 'redefine';
  564.     *RT::User::CanonicalizeUserInfo = sub {
  565.         my $self = shift;
  566.         my $args = shift;
  567.         return ( CanonicalizeUserInfo( $self, $args ) );
  568.     };
  569. }
  570.  
  571. 1;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement