Externalauth changed to accommodate local fix.
[usit-rt.git] / local / plugins / RT-Authen-ExternalAuth / lib / RT / Authen / ExternalAuth.pm
CommitLineData
84fb5b46
MKG
1package RT::Authen::ExternalAuth;
2
3our $VERSION = '0.09';
4
5=head1 NAME
6
ecefa3a7 7 RT::Authen::ExternalAuth - RT Authentication using External Sources
84fb5b46
MKG
8
9=head1 DESCRIPTION
10
ecefa3a7
MKG
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.
84fb5b46 14
ecefa3a7
MKG
15 It also allows for authenticating cookie information against an
16 external database through the use of the RT-Authen-CookieAuth extension.
84fb5b46 17
ecefa3a7 18=begin testing
84fb5b46 19
ecefa3a7 20ok(require RT::Authen::ExternalAuth);
84fb5b46 21
ecefa3a7 22=end testing
84fb5b46
MKG
23
24=cut
25
26use RT::Authen::ExternalAuth::LDAP;
27use RT::Authen::ExternalAuth::DBI;
28
29use strict;
30
31sub 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
236sub 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
ecefa3a7
MKG
282 my %args = (Name => $username);
283 $UserObj->CanonicalizeUserInfo(\%args);
84fb5b46
MKG
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
315sub 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
342sub 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
367sub 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
431sub 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;
ecefa3a7
MKG
440
441 my $found = 0;
442 my %params = (Name => undef,
443 EmailAddress => undef,
444 RealName => undef);
445
84fb5b46
MKG
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
ecefa3a7 454 my @info_services = $RT::ExternalInfoPriority ? @{$RT::ExternalInfoPriority} : undef;
84fb5b46
MKG
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
ecefa3a7
MKG
462 my $config = $RT::ExternalSettings->{$service};
463
84fb5b46
MKG
464 if($config->{'type'} eq 'cookie'){
465 $RT::Logger->debug("You cannot use SSO cookies as an information service!");
466 next;
467 }
468
84fb5b46 469 # For each attr we've been told to canonicalize in the match list
ecefa3a7 470 my $attr_map = (defined $config->{'lookup_attr_map'}) ? $config->{'lookup_attr_map'} : $config->{'attr_map'};
84fb5b46
MKG
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);
ecefa3a7 474 unless(defined($args->{$rt_attr})) {
84fb5b46
MKG
475 $RT::Logger->debug("This attribute (",
476 $rt_attr,
ecefa3a7 477 ") is null or incorrectly defined in the attr_map for this service (",
84fb5b46
MKG
478 $service,
479 ")");
480 next;
481 }
482
483 # Else, use it as a canonicalization key and lookup the user info
ecefa3a7
MKG
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 ")");
84fb5b46
MKG
499 next;
500 }
ecefa3a7 501
84fb5b46
MKG
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'){
ecefa3a7 507 ($found, %params) = RT::Authen::ExternalAuth::LDAP::CanonicalizeUserInfo($service,$key,$value);
84fb5b46 508 } elsif ($config->{'type'} eq 'db') {
ecefa3a7 509 ($found, %params) = RT::Authen::ExternalAuth::DBI::CanonicalizeUserInfo($service,$key,$value);
84fb5b46
MKG
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 }
ecefa3a7
MKG
523
524 # If found, Canonicalize Email Address and
84fb5b46 525 # update the args hash that we were given the hashref for
ecefa3a7
MKG
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 }
84fb5b46
MKG
533
534 $RT::Logger->info( (caller(0))[3],
535 "returning",
536 join(", ", map {sprintf("%s: %s", $_, $args->{$_})}
537 sort(keys(%$args))));
538
ecefa3a7
MKG
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
84fb5b46
MKG
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
84fb5b46 5571;