Initial commit 4.0.5-3
[usit-rt.git] / local / plugins / RT-Authen-ExternalAuth / 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 =head1 CONFIGURATION
19
20 =head2 Generic
21
22 =head3 attr_match_list
23
24 The list of RT attributes that uniquely identify a user. It's
25 recommended to use 'Name' and 'EmailAddress' to save
26 encountering problems later. Example:
27
28     'attr_match_list' => [
29         'Name',
30         'EmailAddress', 
31         'RealName',
32         'WorkPhone', 
33     ],
34
35 =head3 attr_map
36
37 Mapping of RT attributes on to attributes in the external source.
38 Example:
39
40     'attr_map' => {
41         'Name'         => 'sAMAccountName',
42         'EmailAddress' => 'mail',
43         'Organization' => 'physicalDeliveryOfficeName',
44         'RealName'     => 'cn',
45         ...
46     },
47
48 Since version 0.10 it's possible to map one RT field to multiple
49 external attributes, for example:
50
51     attr_map => {
52         EmailAddress => ['mail', 'alias'],
53         ...
54     },
55
56 Note that only one value storred in RT. However, search goes by
57 all external attributes if such RT field list in L</attr_match_list>.
58 On create or update entered value is used as long as it's valid.
59 If user didn't enter value then value stored in the first external
60 attribute is used. Config example:
61
62     attr_match_list => ['Name', 'EmailAddress'],
63     attr_map => {
64         Name         => 'account',
65         EmailAddress => ['mail', 'alias'],
66         ...
67     },
68
69 =cut    
70
71 use RT::Authen::ExternalAuth::LDAP;
72 use RT::Authen::ExternalAuth::DBI;
73
74 use strict;
75
76 sub DoAuth {
77     my ($session,$given_user,$given_pass) = @_;
78
79     unless(defined($RT::ExternalAuthPriority)) {
80         return (0, "ExternalAuthPriority not defined, please check your configuration file.");
81     }
82
83     my $no_info_check = 0;
84     unless(defined($RT::ExternalInfoPriority)) {
85         $RT::Logger->debug("ExternalInfoPriority not defined. User information (including user enabled/disabled cannot be externally-sourced");
86         $no_info_check = 1;
87     }
88
89     # This may be used by single sign-on (SSO) authentication mechanisms for bypassing a password check.
90     my $pass_bypass = 0;
91     my $success = 0;
92
93     # Should have checked if user is already logged in before calling this function,
94     # but just in case, we'll check too.
95     return (0, "User already logged in!") if ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id);
96     # We don't have a logged in user. Let's try all our available methods in order.
97     # last if success, next if not.
98     
99     # Get the prioritised list of external authentication services
100     my @auth_services = @$RT::ExternalAuthPriority;
101     
102     # For each of those services..
103     foreach my $service (@auth_services) {
104
105         $pass_bypass = 0;
106
107         # Get the full configuration for that service as a hashref
108         my $config = $RT::ExternalSettings->{$service};
109         $RT::Logger->debug( "Attempting to use external auth service:",
110                             $service);
111
112         # $username will be the final username we decide to check
113         # This will not necessarily be $given_user
114         my $username = undef;
115         
116         #############################################################
117         ####################### SSO Check ###########################
118         #############################################################
119         if ($config->{'type'} eq 'cookie') {    
120             # Currently, Cookie authentication is our only SSO method
121             $username = RT::Authen::ExternalAuth::DBI::GetCookieAuth($config);
122         }
123         #############################################################
124         
125         # If $username is defined, we have a good SSO $username and can
126         # safely bypass the password checking later on; primarily because
127         # it's VERY unlikely we even have a password to check if an SSO succeeded.
128         $pass_bypass = 0;
129         if(defined($username)) {
130             $RT::Logger->debug("Pass not going to be checked, attempting SSO");
131             $pass_bypass = 1;
132         } else {
133
134             # SSO failed and no $user was passed for a login attempt
135             # We only don't return here because the next iteration could be an SSO attempt
136             unless(defined($given_user)) {
137                 $RT::Logger->debug("SSO Failed and no user to test with. Nexting");
138                 next;
139             }
140
141             # We don't have an SSO login, so we will be using the credentials given
142             # on RT's login page to do our authentication.
143             $username = $given_user;
144     
145             # Don't continue unless the service works.
146             # next unless RT::Authen::ExternalAuth::TestConnection($config);
147
148             # Don't continue unless the $username exists in the external service
149
150             $RT::Logger->debug("Calling UserExists with \$username ($username) and \$service ($service)");
151             next unless RT::Authen::ExternalAuth::UserExists($username, $service);
152         }
153
154         ####################################################################
155         ########## Load / Auto-Create ######################################
156         ####################################################################
157         # We are now sure that we're talking about a valid RT user.
158         # If the user already exists, load up their info. If they don't
159         # then we need to create the user in RT.
160
161         # Does user already exist internally to RT?
162         $session->{'CurrentUser'} = RT::CurrentUser->new();
163         $session->{'CurrentUser'}->Load($username);
164
165         # Unless we have loaded a valid user with a UserID create one.
166         unless ($session->{'CurrentUser'}->Id) {
167                         my $UserObj = RT::User->new($RT::SystemUser);
168                 my ($val, $msg) = 
169               $UserObj->Create(%{ref($RT::AutoCreate) ? $RT::AutoCreate : {}},
170                                Name   => $username,
171                                Gecos  => $username,
172                               );
173             unless ($val) {
174                 $RT::Logger->error( "Couldn't create user $username: $msg" );
175                 next;
176             }
177             $RT::Logger->info(  "Autocreated external user",
178                                 $UserObj->Name,
179                                 "(",
180                                 $UserObj->Id,
181                                 ")");
182             
183             $RT::Logger->debug("Loading new user (",
184                                                 $username,
185                                                 ") into current session");
186             $session->{'CurrentUser'}->Load($username);
187         } 
188         
189         ####################################################################
190         ########## Authentication ##########################################
191         ####################################################################
192         # If we successfully used an SSO service, then authentication
193         # succeeded. If we didn't then, success is determined by a password
194         # test.
195         $success = 0;
196         if($pass_bypass) {
197             $RT::Logger->debug("Password check bypassed due to SSO method being in use");
198             $success = 1;
199         } else {
200             $RT::Logger->debug("Password validation required for service - Executing...");
201             $success = RT::Authen::ExternalAuth::GetAuth($service,$username,$given_pass);
202         }
203        
204         $RT::Logger->debug("Password Validation Check Result: ",$success);
205
206         # If the password check succeeded then this is our authoritative service
207         # and we proceed to user information update and login.
208         last if $success;
209     }
210     
211     # If we got here and don't have a user loaded we must have failed to
212     # get a full, valid user from an authoritative external source.
213     unless ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id) {
214         delete $session->{'CurrentUser'};
215         return (0, "No User");
216     }
217
218     unless($success) {
219         delete $session->{'CurrentUser'};
220         return (0, "Password Invalid");
221     }
222     
223     # Otherwise we succeeded.
224     $RT::Logger->debug("Authentication successful. Now updating user information and attempting login.");
225         
226     ####################################################################################################
227     ############################### The following is auth-method agnostic ##############################
228     ####################################################################################################
229     
230     # If we STILL have a completely valid RT user to play with...
231     # and therefore password has been validated...
232     if ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id) {
233         
234         # Even if we have JUST created the user in RT, we are going to
235         # reload their information from an external source. This allows us
236         # to be sure that the user the cookie gave us really does exist in
237         # the database, but more importantly, UpdateFromExternal will check 
238         # whether the user is disabled or not which we have not been able to 
239         # do during auto-create
240
241         # These are not currently used, but may be used in the future.
242         my $info_updated = 0;
243         my $info_updated_msg = "User info not updated";
244
245         unless($no_info_check) {
246             # Note that UpdateUserInfo does not care how we authenticated the user
247             # It will look up user info from whatever is specified in $RT::ExternalInfoPriority
248             ($info_updated,$info_updated_msg) = RT::Authen::ExternalAuth::UpdateUserInfo($session->{'CurrentUser'}->Name);
249         }
250                 
251         # Now that we definitely have up-to-date user information,
252         # if the user is disabled, kick them out. Now!
253         if ($session->{'CurrentUser'}->UserObj->Disabled) {
254             delete $session->{'CurrentUser'};
255             return (0, "User account disabled, login denied");
256         }
257     }
258     
259     # If we **STILL** have a full user and the session hasn't already been deleted
260     # This If/Else is logically unnecessary, but it doesn't hurt to leave it here
261     # just in case. Especially to be a double-check to future modifications.
262     if ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id) {
263             
264             $RT::Logger->info(  "Successful login for",
265                                 $session->{'CurrentUser'}->Name,
266                                 "from",
267                                 $ENV{'REMOTE_ADDR'});
268             # Do not delete the session. User stays logged in and
269             # autohandler will not check the password again
270     } else {
271             # Make SURE the session is deleted.
272             delete $session->{'CurrentUser'};
273             return (0, "Failed to authenticate externally");
274             # This will cause autohandler to request IsPassword 
275             # which will in turn call IsExternalPassword
276     }
277     
278     return (1, "Successful login");
279 }
280
281 sub UpdateUserInfo {
282     my $username        = shift;
283
284     # Prepare for the worst...
285     my $found           = 0;
286     my $updated         = 0;
287     my $msg             = "User NOT updated";
288
289     my $user_disabled   = RT::Authen::ExternalAuth::UserDisabled($username);
290
291     my $UserObj = RT::User->new($RT::SystemUser);
292     $UserObj->Load($username);        
293
294     # If user is disabled, set the RT::Principle to disabled and return out of the function.
295     # I think it's a waste of time and energy to update a user's information if they are disabled
296     # and it could be a security risk if they've updated their external information with some 
297     # carefully concocted code to try to break RT - worst case scenario, but they have been 
298     # denied access after all, don't take any chances.
299      
300     # If someone gives me a good enough reason to do it, 
301     # then I'll update all the info for disabled users
302
303     if ($user_disabled) {
304         # Make sure principle is disabled in RT
305         my ($val, $message) = $UserObj->SetDisabled(1);
306         # Log what has happened
307         $RT::Logger->info("User marked as DISABLED (",
308                             $username,
309                             ") per External Service", 
310                             "($val, $message)\n");
311         $msg = "User Disabled";
312         
313         return ($updated, $msg);
314     }    
315         
316     # Make sure principle is not disabled in RT
317     my ($val, $message) = $UserObj->SetDisabled(0);
318     # Log what has happened
319     $RT::Logger->info("User marked as ENABLED (",
320                         $username,
321                         ") per External Service",
322                         "($val, $message)\n");
323
324     # Update their info from external service using the username as the lookup key
325     # CanonicalizeUserInfo will work out for itself which service to use
326     # Passing it a service instead could break other RT code
327     my %args;
328     $UserObj->CanonicalizeUserInfo( \%args );
329
330     # For each piece of information returned by CanonicalizeUserInfo,
331     # run the Set method for that piece of info to change it for the user
332     foreach my $key (sort(keys(%args))) {
333         next unless $args{$key};
334         my $method = "Set$key";
335         # We do this on the UserObj from above, not self so that there 
336         # are no permission restrictions on setting information
337         my ($method_success,$method_msg) = $UserObj->$method($args{$key});
338         
339         # If your user information is not getting updated, 
340         # uncomment the following logging statements
341         if ($method_success) {
342             # At DEBUG level, log that method succeeded
343             # $RT::Logger->debug((caller(0))[3],"$method Succeeded. $method_msg");
344         } else {
345             # At DEBUG level, log that method failed
346             # $RT::Logger->debug((caller(0))[3],"$method Failed. $method_msg");
347         }
348     }
349
350     # Confirm update success
351     $updated = 1;
352     $RT::Logger->debug( "UPDATED user (",
353                         $username,
354                         ") from External Service\n");
355     $msg = 'User updated';
356
357     return ($updated, $msg);
358 }
359
360 sub GetAuth {
361
362     # Request a username/password check from the specified service
363     # This is only valid for non-SSO services.
364     
365     my ($service,$username,$password) = @_;
366     
367     my $success = 0;
368     
369     # Get the full configuration for that service as a hashref
370     my $config = $RT::ExternalSettings->{$service};
371     
372     # And then act accordingly depending on what type of service it is.
373     # Right now, there is only code for DBI and LDAP non-SSO services
374     if ($config->{'type'} eq 'db') {    
375         $success = RT::Authen::ExternalAuth::DBI::GetAuth($service,$username,$password);
376         $RT::Logger->debug("DBI password validation result:",$success);
377     } elsif ($config->{'type'} eq 'ldap') {
378         $success = RT::Authen::ExternalAuth::LDAP::GetAuth($service,$username,$password);
379         $RT::Logger->debug("LDAP password validation result:",$success);
380     } else {
381         $RT::Logger->error("Invalid service type for GetAuth:",$service);
382     }
383     
384     return $success; 
385 }
386
387 sub UserExists {
388
389     # Request a username/password check from the specified service
390     # This is only valid for non-SSO services.
391
392     my ($username,$service) = @_;
393
394     my $success = 0;
395
396     # Get the full configuration for that service as a hashref
397     my $config = $RT::ExternalSettings->{$service};
398
399     # And then act accordingly depending on what type of service it is.
400     # Right now, there is only code for DBI and LDAP non-SSO services
401     if ($config->{'type'} eq 'db') {
402         $success = RT::Authen::ExternalAuth::DBI::UserExists($username,$service);
403     } elsif ($config->{'type'} eq 'ldap') {
404         $success = RT::Authen::ExternalAuth::LDAP::UserExists($username,$service);
405     } else {
406         $RT::Logger->debug("Invalid service type for UserExists:",$service);
407     }
408
409     return $success;
410 }
411
412 sub UserDisabled {
413     
414     my $username = shift;
415     my $user_disabled = 0;
416     
417     my @info_services = $RT::ExternalInfoPriority ? @{$RT::ExternalInfoPriority} : undef;
418
419     # For each named service in the list
420     # Check to see if the user is found in the external service
421     # If not found, jump to next service
422     # If found, check to see if user is considered disabled by the service
423     # Then update the user's info in RT and return
424     foreach my $service (@info_services) {
425         
426         # Get the external config for this service as a hashref        
427         my $config = $RT::ExternalSettings->{$service};
428         
429         # If the config doesn't exist, don't bother doing anything, skip to next in list.
430         unless(defined($config)) {
431             $RT::Logger->debug("You haven't defined a configuration for the service named \"",
432                                 $service,
433                                 "\" so I'm not going to try to get user information from it. Skipping...");
434             next;
435         }
436         
437         # If it's a DBI config:
438         if ($config->{'type'} eq 'db') {
439             
440             unless(RT::Authen::ExternalAuth::DBI::UserExists($username,$service)) {
441                 $RT::Logger->debug("User (",
442                                     $username,
443                                     ") doesn't exist in service (",
444                                     $service,
445                                     ") - Cannot update information - Skipping...");
446                 next;
447             }
448             $user_disabled = RT::Authen::ExternalAuth::DBI::UserDisabled($username,$service);
449             
450         } elsif ($config->{'type'} eq 'ldap') {
451             
452             unless(RT::Authen::ExternalAuth::LDAP::UserExists($username,$service)) {
453                 $RT::Logger->debug("User (",
454                                     $username,
455                                     ") doesn't exist in service (",
456                                     $service,
457                                     ") - Cannot update information - Skipping...");
458                 next;
459             }
460             $user_disabled = RT::Authen::ExternalAuth::LDAP::UserDisabled($username,$service);
461                     
462         } elsif ($config->{'type'} eq 'cookie') {
463             RT::Logger->error("You cannot use SSO Cookies as an information service.");
464             next;
465         } else {
466             # The type of external service doesn't currently have any methods associated with it. Or it's a typo.
467             RT::Logger->error("Invalid type specification for config %config->{'name'}");
468             # Drop out to next service in list
469             next;
470         }
471     
472     }
473     return $user_disabled;
474 }
475
476 sub CanonicalizeUserInfo {
477     
478     # Careful, this $args hashref was given to RT::User::CanonicalizeUserInfo and
479     # then transparently passed on to this function. The whole purpose is to update
480     # the original hash as whatever passed it to RT::User is expecting to continue its
481     # code with an update args hash.
482     
483     my $UserObj = shift;
484     my $args    = shift;
485
486     WorkaroundAutoCreate( $UserObj, $args );
487
488     my $current_value = sub {
489         my $field = shift;
490         return $args->{ $field } if keys %$args;
491
492         return undef unless $UserObj->can( $field );
493         return $UserObj->$field();
494     };
495
496     my ($found, $config, %params) = (0);
497
498     $RT::Logger->debug( (caller(0))[3], 
499                         "called by", 
500                         caller, 
501                         "with:", 
502                         join(", ", map {sprintf("%s: %s", $_, $args->{$_})}
503                             sort(keys(%$args))));
504
505     # Get the list of defined external services
506     my @info_services = $RT::ExternalInfoPriority ? @{$RT::ExternalInfoPriority} : ();
507     # For each external service...
508     foreach my $service (@info_services) {
509         
510         $RT::Logger->debug( "Attempting to get user info using this external service:",
511                             $service);
512         
513         # Get the config for the service so that we know what attrs we can canonicalize
514         $config = $RT::ExternalSettings->{$service};
515
516         if($config->{'type'} eq 'cookie'){
517             $RT::Logger->debug("You cannot use SSO cookies as an information service!");
518             next;
519         }  
520         
521         # Get the list of unique attrs we need
522         my @service_attrs = do {
523             my %seen;
524             grep !$seen{$_}++, map ref($_)? @$_ : ($_), values %{ $config->{'attr_map'} }
525         };
526
527         # For each attr we've been told to canonicalize in the match list
528         foreach my $rt_attr (@{$config->{'attr_match_list'}}) {
529             # Jump to the next attr in $args if this one isn't in the attr_match_list
530             $RT::Logger->debug( "Attempting to use this canonicalization key:",$rt_attr);
531             my $value = $current_value->( $rt_attr );
532             unless( defined $value && length $value ) {
533                 $RT::Logger->debug("This attribute (",
534                                     $rt_attr,
535                                     ") is null or incorrectly defined in the attr_match_list for this service (",
536                                     $service,
537                                     ")");
538                 next;
539             }
540                                
541             # Else, use it as a canonicalization key and lookup the user info    
542             my $key = $config->{'attr_map'}->{$rt_attr};
543             unless ( $key ) {
544                 $RT::Logger->warning(
545                     "No mapping for $rt_attr in attr_map for this service ($service)"
546                 );
547                 next;
548             }
549
550             # Use an if/elsif structure to do a lookup with any custom code needed 
551             # for any given type of external service, or die if no code exists for
552             # the service requested.
553             
554             if($config->{'type'} eq 'ldap'){    
555                 ($found, %params) = RT::Authen::ExternalAuth::LDAP::CanonicalizeUserInfo($service,$key,$value, \@service_attrs);
556             } elsif ($config->{'type'} eq 'db') {
557                 ($found, %params) = RT::Authen::ExternalAuth::DBI::CanonicalizeUserInfo($service,$key,$value, \@service_attrs);
558             } else {
559                 $RT::Logger->debug( (caller(0))[3],
560                                     "does not consider",
561                                     $service,
562                                     "a valid information service");
563             }
564        
565             # Don't Check any more attributes
566             last if $found;
567         }
568         # Don't Check any more services
569         last if $found;
570     }
571
572     unless ( $found ) {
573         ### HACK: The config var below is to overcome the (IMO) bug in
574         ### RT::User::Create() which expects this function to always
575         ### return true or rejects the user for creation. This should be
576         ### a different config var (CreateUncanonicalizedUsers) and 
577         ### should be honored in RT::User::Create()
578         return($RT::AutoCreateNonExternalUsers);
579     }
580
581     # If found let's build back RT's fields
582     my %res;
583     while ( my ($k, $v) = each %{ $config->{'attr_map'} } ) {
584         unless ( ref $v ) {
585             $res{ $k } = $params{ $v };
586             next;
587         }
588
589         my $current = $current_value->( $k );
590         unless ( defined $current ) {
591             $res{ $k } = (grep defined && length, map $params{ $_ }, @$v)[0];
592         } else {
593             unless ( grep defined && length && $_ eq $current, map $params{ $_ }, @$v ) {
594                 $res{ $k } = (grep defined && length, map $params{ $_ }, @$v)[0];
595             }
596         }
597     }
598
599     # It's important that we always have a canonical email address
600     if ($res{'EmailAddress'}) {
601         $res{'EmailAddress'} = $UserObj->CanonicalizeEmailAddress($res{'EmailAddress'});
602     } 
603
604     # update the args hash that we were given the hashref for
605     %$args = (%$args, %res);
606
607     $RT::Logger->info(  (caller(0))[3], 
608                         "returning", 
609                         join(", ", map {sprintf("%s: %s", $_, $args->{$_})} 
610                             sort(keys(%$args))));
611
612     return $found;
613 }
614
615 {
616     no warnings 'redefine';
617     *RT::User::CanonicalizeUserInfo = sub {
618         my $self = shift;
619         my $args = shift;
620         return ( CanonicalizeUserInfo( $self, $args ) );
621     };
622 }
623
624 {
625     no warnings 'redefine';
626     my $orig = RT::User->can('LoadByCols');
627     *RT::User::LoadByCols = sub {
628         my $self = shift;
629         my %args = @_;
630
631         my $rv = $orig->( $self, %args );
632         return $rv if $self->id;
633
634 # we couldn't load a user. ok, but user may exist anyway. It may happen in the following
635 # cases:
636 # 1) Service has multiple fields in attr_match_list, it's important when we have Name
637 # and EmailAddress in there. 
638
639         my (%other) = FindRecordsByOtherFields( $self, %args );
640         while ( my ($search_by, $values) = each %other ) {
641             foreach my $value ( @$values ) {
642                 my $rv = $orig->( $self, $search_by => $value );
643                 return $rv if $self->id;
644             }
645         }
646
647 # 2) RT fields in attr_match_list are mapped to multiple attributes in an external
648 # source, for example: attr_map => { EmailAddress => [qw(mail alias1 alias2 alias3)], }
649         my ($search_by, @alternatives) = FindRecordsWithAlternatives( $self, %args );
650         foreach my $value ( @alternatives ) {
651             my $rv = $orig->( $self, %args, $search_by => $value );
652             return $rv if $self->id;
653         }
654
655         return $rv;
656     };
657 }
658
659 sub FindRecordsWithAlternatives {
660     my $user = shift;
661     my %args = @_;
662
663     # find services that may have alternative values for a field we search by
664     my @info_services = $RT::ExternalInfoPriority ? @{$RT::ExternalInfoPriority} : ();
665     foreach my $service ( splice @info_services ) {
666         my $config = $RT::ExternalSettings->{ $service };
667         next if $config->{'type'} eq 'cookie';
668         next unless
669             grep ref $config->{'attr_map'}{ $_ },
670             @{ $config->{'attr_match_list'} };
671
672         push @info_services, $service;
673     }
674     return unless @info_services;
675
676     # find user in external service and fetch alternative values
677     # for a field
678     foreach my $service (@info_services) {
679         my $config = $RT::ExternalSettings->{$service};
680
681         my $search_by = undef;
682         foreach my $rt_attr ( @{ $config->{'attr_match_list'} } ) {
683             next unless exists $args{ $rt_attr }
684                 && defined $args{ $rt_attr }
685                 && length $args{ $rt_attr };
686             next unless ref $config->{'attr_map'}{ $rt_attr };
687
688             $search_by = $rt_attr;
689             last;
690         }
691         next unless $search_by;
692
693         my @search_args = (
694             $service,
695             $config->{'attr_map'}{ $search_by },
696             $args{ $search_by },
697             $config->{'attr_map'}{ $search_by },
698         );
699
700         my ($found, %params);
701         if($config->{'type'} eq 'ldap') {
702             ($found, %params) = RT::Authen::ExternalAuth::LDAP::CanonicalizeUserInfo( @search_args );
703         } elsif ($config->{'type'} eq 'db') {
704             ($found, %params) = RT::Authen::ExternalAuth::DBI::CanonicalizeUserInfo( @search_args );
705         } else {
706             $RT::Logger->debug( (caller(0))[3],
707                                 "does not consider",
708                                 $service,
709                                 "a valid information service");
710         }
711         next unless $found;
712
713         my @alternatives = grep defined && length && $_ ne $args{ $search_by }, values %params;
714
715         # Don't Check any more services
716         return @alternatives;
717     }
718     return;
719 }
720
721 sub FindRecordsByOtherFields {
722     my $user = shift;
723     my %args = @_;
724
725     my @info_services = $RT::ExternalInfoPriority ? @{$RT::ExternalInfoPriority} : ();
726     foreach my $service ( splice @info_services ) {
727         my $config = $RT::ExternalSettings->{ $service };
728         next if $config->{'type'} eq 'cookie';
729         next unless @{ $config->{'attr_match_list'} } > 1;
730
731         push @info_services, $service;
732     }
733     return unless @info_services;
734
735     # find user in external service and fetch alternative values
736     # for a field
737     foreach my $service (@info_services) {
738         my $config = $RT::ExternalSettings->{$service};
739
740         foreach my $search_by ( @{ $config->{'attr_match_list'} } ) {
741             next unless exists $args{ $search_by }
742                 && defined $args{ $search_by }
743                 && length $args{ $search_by };
744
745             my @fetch;
746             foreach my $field ( @{ $config->{'attr_match_list'} } ) {
747                 next if $field eq $search_by;
748
749                 my $external = $config->{'attr_map'}{ $field };
750                 push @fetch, ref $external? (@$external) : ($external);
751             }
752             my @search_args = (
753                 $service,
754                 $config->{'attr_map'}{ $search_by },
755                 $args{ $search_by },
756                 \@fetch,
757             );
758
759             my ($found, %params);
760             if($config->{'type'} eq 'ldap') {
761                 ($found, %params) = RT::Authen::ExternalAuth::LDAP::CanonicalizeUserInfo( @search_args );
762             } elsif ($config->{'type'} eq 'db') {
763                 ($found, %params) = RT::Authen::ExternalAuth::DBI::CanonicalizeUserInfo( @search_args );
764             } else {
765                 $RT::Logger->debug( (caller(0))[3],
766                                     "does not consider",
767                                     $service,
768                                     "a valid information service");
769             }
770             next unless $found;
771
772             my %res =
773                 map { $_ => $config->{'attr_map'}{ $_ } }
774                 grep defined $config->{'attr_map'}{ $_ },
775                 grep $_ ne $search_by,
776                 @{ $config->{'attr_match_list'} }
777             ;
778             foreach my $value ( values %res ) {
779                 $value = ref $value? [ map $params{$_}, @$value ] : [ $params{ $value } ];
780             }
781             return %res;
782         }
783     }
784     return;
785 }
786
787 =head2 WorkaroundAutoCreate
788
789 RT has C<$AutoCreate> option in the config. However, up to RT 4.0.0 this
790 option is no used when account created by incomming email. This module
791 workarounds this problem.
792
793 =cut
794
795 sub WorkaroundAutoCreate {
796     my $user = shift;
797     my $args = shift;
798
799     # CreateUser in RT::Interface::Email doesn't account $RT::AutoCreate
800     # config option. Let's workaround it.
801
802     return unless $RT::AutoCreate && keys %$RT::AutoCreate;
803     return unless keys %$args; # no args - update
804     return unless (caller(4))[3] eq 'RT::Interface::Email::CreateUser';
805
806     my %tmp = %$RT::AutoCreate;
807     delete @tmp{qw(Name EmailAddress RealName Comments)};
808     %$args = (%$args, %$RT::AutoCreate);
809 }
810
811 1;