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