]>
Commit | Line | Data |
---|---|---|
84fb5b46 MKG |
1 | package RT::Authen::ExternalAuth; |
2 | ||
3 | our $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 | 20 | ok(require RT::Authen::ExternalAuth); |
84fb5b46 | 21 | |
ecefa3a7 | 22 | =end testing |
84fb5b46 MKG |
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) { | |
d7b8a71d | 169 | $session->{'CurrentUser'} = RT::CurrentUser->new; |
84fb5b46 MKG |
170 | return (0, "No User"); |
171 | } | |
172 | ||
173 | unless($success) { | |
d7b8a71d | 174 | $session->{'CurrentUser'} = RT::CurrentUser->new; |
84fb5b46 MKG |
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) { | |
d7b8a71d | 209 | $session->{'CurrentUser'} = RT::CurrentUser->new; |
84fb5b46 MKG |
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 { | |
d7b8a71d MKG |
226 | # Make SURE the session is purged to an empty user. |
227 | $session->{'CurrentUser'} = RT::CurrentUser->new; | |
84fb5b46 MKG |
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 | |
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 | ||
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; | |
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 | 557 | 1; |