Externalauth changed to accommodate local fix.
[usit-rt.git] / local / plugins / RT-Authen-ExternalAuth / lib / RT / Authen / ExternalAuth / LDAP.pm
CommitLineData
84fb5b46
MKG
1package RT::Authen::ExternalAuth::LDAP;\r
2\r
3use Net::LDAP qw(LDAP_SUCCESS LDAP_PARTIAL_RESULTS);\r
4use Net::LDAP::Util qw(ldap_error_name);\r
5use Net::LDAP::Filter;\r
6\r
7use strict;\r
8\r
9require Net::SSLeay if $RT::ExternalServiceUsesSSLorTLS;\r
10\r
11sub GetAuth {\r
ecefa3a7 12 \r
84fb5b46
MKG
13 my ($service, $username, $password) = @_;\r
14 \r
15 my $config = $RT::ExternalSettings->{$service};\r
16 $RT::Logger->debug( "Trying external auth service:",$service);\r
17\r
18 my $base = $config->{'base'};\r
19 my $filter = $config->{'filter'};\r
20 my $group = $config->{'group'};\r
21 my $group_attr = $config->{'group_attr'};\r
22 my $attr_map = $config->{'attr_map'};\r
23 my @attrs = ('dn');\r
24\r
25 # Empty parentheses as filters cause Net::LDAP to barf.\r
26 # We take care of this by using Net::LDAP::Filter, but\r
27 # there's no harm in fixing this right now.\r
28 if ($filter eq "()") { undef($filter) };\r
29\r
30 # Now let's get connected\r
31 my $ldap = _GetBoundLdapObj($config);\r
32 return 0 unless ($ldap);\r
33\r
ecefa3a7
MKG
34 $filter = Net::LDAP::Filter->new( '(&(' . \r
35 $attr_map->{'Name'} . \r
36 '=' . \r
37 $username . \r
38 ')' . \r
39 $filter . \r
40 ')'\r
41 );\r
42\r
43 $RT::Logger->debug( "LDAP Search === ",\r
44 "Base:",\r
45 $base,\r
46 "== Filter:", \r
47 $filter->as_string,\r
48 "== Attrs:", \r
49 join(',',@attrs));\r
50\r
51 my $ldap_msg = $ldap->search( base => $base,\r
52 filter => $filter,\r
53 attrs => \@attrs);\r
54\r
55 unless ($ldap_msg->code == LDAP_SUCCESS || $ldap_msg->code == LDAP_PARTIAL_RESULTS) {\r
56 $RT::Logger->debug( "search for", \r
57 $filter->as_string, \r
58 "failed:", \r
59 ldap_error_name($ldap_msg->code), \r
60 $ldap_msg->code);\r
61 # Didn't even get a partial result - jump straight to the next external auth service\r
62 return 0;\r
63 }\r
84fb5b46
MKG
64\r
65 unless ($ldap_msg->count == 1) {\r
66 $RT::Logger->info( $service,\r
67 "AUTH FAILED:", \r
68 $username,\r
69 "User not found or more than one user found");\r
70 # We got no user, or too many users.. jump straight to the next external auth service\r
71 return 0;\r
72 }\r
73\r
74 my $ldap_dn = $ldap_msg->first_entry->dn;\r
75 $RT::Logger->debug( "Found LDAP DN:", \r
76 $ldap_dn);\r
77\r
78 # THIS bind determines success or failure on the password.\r
79 $ldap_msg = $ldap->bind($ldap_dn, password => $password);\r
80\r
81 unless ($ldap_msg->code == LDAP_SUCCESS) {\r
82 $RT::Logger->info( $service,\r
83 "AUTH FAILED", \r
84 $username, \r
85 "(can't bind:", \r
86 ldap_error_name($ldap_msg->code), \r
87 $ldap_msg->code, \r
88 ")");\r
89 # Could not bind to the LDAP server as the user we found with the password\r
90 # we were given, therefore the password must be wrong so we fail and\r
91 # jump straight to the next external auth service\r
92 return 0;\r
93 }\r
94\r
95 # The user is authenticated ok, but is there an LDAP Group to check?\r
96 if ($group) {\r
97 # If we've been asked to check a group...\r
ecefa3a7
MKG
98 $filter = Net::LDAP::Filter->new("(${group_attr}=${ldap_dn})");\r
99 \r
100 $RT::Logger->debug( "LDAP Search === ",\r
101 "Base:",\r
102 $base,\r
103 "== Filter:", \r
104 $filter->as_string,\r
105 "== Attrs:", \r
106 join(',',@attrs));\r
107 \r
108 $ldap_msg = $ldap->search( base => $group,\r
109 filter => $filter,\r
110 attrs => \@attrs,\r
111 scope => 'base');\r
112\r
113 # And the user isn't a member:\r
114 unless ($ldap_msg->code == LDAP_SUCCESS || \r
115 $ldap_msg->code == LDAP_PARTIAL_RESULTS) {\r
116 $RT::Logger->critical( "Search for", \r
117 $filter->as_string, \r
118 "failed:",\r
119 ldap_error_name($ldap_msg->code), \r
120 $ldap_msg->code);\r
121\r
122 # Fail auth - jump to next external auth service\r
123 return 0;\r
124 }\r
84fb5b46
MKG
125\r
126 unless ($ldap_msg->count == 1) {\r
127 $RT::Logger->info( $service,\r
128 "AUTH FAILED:", \r
129 $username);\r
130 \r
131 # Fail auth - jump to next external auth service\r
132 return 0;\r
133 }\r
134 }\r
135 \r
136 # Any other checks you want to add? Add them here.\r
137\r
138 # If we've survived to this point, we're good.\r
139 $RT::Logger->info( (caller(0))[3], \r
140 "External Auth OK (",\r
141 $service,\r
142 "):", \r
143 $username);\r
144 return 1;\r
145\r
146}\r
147\r
148\r
149sub CanonicalizeUserInfo {\r
150 \r
ecefa3a7
MKG
151 my ($service, $key, $value) = @_;\r
152\r
153 my $found = 0;\r
154 my %params = (Name => undef,\r
155 EmailAddress => undef,\r
156 RealName => undef);\r
84fb5b46
MKG
157\r
158 # Load the config\r
159 my $config = $RT::ExternalSettings->{$service};\r
ecefa3a7
MKG
160 \r
161 # Figure out what's what\r
162 my $base = $config->{'base'};\r
163 my $filter = $config->{'filter'};\r
84fb5b46 164\r
ecefa3a7
MKG
165 # Get the list of unique attrs we need\r
166 my @attrs = values(%{$config->{'attr_map'}});\r
167\r
168 # This is a bit confusing and probably broken. Something to revisit..\r
169 my $filter_addition = ($key && $value) ? "(". $key . "=$value)" : "";\r
170 if(defined($filter) && ($filter ne "()")) {\r
171 $filter = Net::LDAP::Filter->new( "(&" . \r
172 $filter . \r
173 $filter_addition . \r
174 ")"\r
175 ); \r
176 } else {\r
177 $RT::Logger->debug( "LDAP Filter invalid or not present.");\r
178 }\r
84fb5b46 179\r
84fb5b46
MKG
180 unless (defined($base)) {\r
181 $RT::Logger->critical( (caller(0))[3],\r
182 "LDAP baseDN not defined");\r
183 # Drop out to the next external information service\r
ecefa3a7 184 return ($found, %params);\r
84fb5b46
MKG
185 }\r
186\r
187 # Get a Net::LDAP object based on the config we provide\r
188 my $ldap = _GetBoundLdapObj($config);\r
189\r
190 # Jump to the next external information service if we can't get one, \r
191 # errors should be logged by _GetBoundLdapObj so we don't have to.\r
ecefa3a7
MKG
192 return ($found, %params) unless ($ldap);\r
193\r
194 # Do a search for them in LDAP\r
195 $RT::Logger->debug( "LDAP Search === ",\r
196 "Base:",\r
197 $base,\r
198 "== Filter:", \r
199 $filter->as_string,\r
200 "== Attrs:", \r
201 join(',',@attrs));\r
202\r
203 my $ldap_msg = $ldap->search(base => $base,\r
204 filter => $filter,\r
205 attrs => \@attrs);\r
206\r
207 # If we didn't get at LEAST a partial result, just die now.\r
208 if ($ldap_msg->code != LDAP_SUCCESS and \r
209 $ldap_msg->code != LDAP_PARTIAL_RESULTS) {\r
210 $RT::Logger->critical( (caller(0))[3],\r
211 ": Search for ",\r
212 $filter->as_string,\r
213 " failed: ",\r
214 ldap_error_name($ldap_msg->code), \r
215 $ldap_msg->code);\r
216 # $found remains as 0\r
217 \r
218 # Drop out to the next external information service\r
219 $ldap_msg = $ldap->unbind();\r
220 if ($ldap_msg->code != LDAP_SUCCESS) {\r
221 $RT::Logger->critical( (caller(0))[3],\r
222 ": Could not unbind: ", \r
223 ldap_error_name($ldap_msg->code), \r
224 $ldap_msg->code);\r
225 }\r
226 undef $ldap;\r
227 undef $ldap_msg;\r
228 return ($found, %params);\r
229 \r
230 } else {\r
231 # If there's only one match, we're good; more than one and\r
232 # we don't know which is the right one so we skip it.\r
233 if ($ldap_msg->count == 1) {\r
234 my $entry = $ldap_msg->first_entry();\r
235 foreach my $key (keys(%{$config->{'attr_map'}})) {\r
236 if ($RT::LdapAttrMap->{$key} eq 'dn') {\r
237 $params{$key} = $entry->dn();\r
238 } else {\r
239 $params{$key} = \r
240 ($entry->get_value($config->{'attr_map'}->{$key}))[0];\r
241 }\r
242\r
243 }\r
244 $found = 1;\r
84fb5b46 245 } else {\r
ecefa3a7
MKG
246 # Drop out to the next external information service\r
247 $ldap_msg = $ldap->unbind();\r
248 if ($ldap_msg->code != LDAP_SUCCESS) {\r
249 $RT::Logger->critical( (caller(0))[3],\r
250 ": Could not unbind: ", \r
251 ldap_error_name($ldap_msg->code), \r
252 $ldap_msg->code);\r
253 }\r
254 undef $ldap;\r
255 undef $ldap_msg;\r
256 return ($found, %params);\r
84fb5b46
MKG
257 }\r
258 }\r
ecefa3a7
MKG
259 $ldap_msg = $ldap->unbind();\r
260 if ($ldap_msg->code != LDAP_SUCCESS) {\r
261 $RT::Logger->critical( (caller(0))[3],\r
262 ": Could not unbind: ", \r
263 ldap_error_name($ldap_msg->code), \r
264 $ldap_msg->code);\r
265 }\r
266\r
267 undef $ldap;\r
268 undef $ldap_msg;\r
269\r
270 return ($found, %params);\r
84fb5b46
MKG
271}\r
272\r
273sub UserExists {\r
274 my ($username,$service) = @_;\r
275 $RT::Logger->debug("UserExists params:\nusername: $username , service: $service"); \r
276 my $config = $RT::ExternalSettings->{$service};\r
277 \r
ecefa3a7 278 my $base = $config->{'base'};\r
84fb5b46
MKG
279 my $filter = $config->{'filter'};\r
280\r
281 # While LDAP filters must be surrounded by parentheses, an empty set\r
282 # of parentheses is an invalid filter and will cause failure\r
283 # This shouldn't matter since we are now using Net::LDAP::Filter below,\r
284 # but there's no harm in doing this to be sure\r
285 if ($filter eq "()") { undef($filter) };\r
286\r
287 if (defined($config->{'attr_map'}->{'Name'})) {\r
288 # Construct the complex filter\r
289 $filter = Net::LDAP::Filter->new( '(&' . \r
290 $filter . \r
291 '(' . \r
292 $config->{'attr_map'}->{'Name'} . \r
293 '=' . \r
294 $username . \r
295 '))'\r
296 );\r
297 }\r
298\r
299 my $ldap = _GetBoundLdapObj($config);\r
300 return unless $ldap;\r
301\r
ecefa3a7
MKG
302 my @attrs = values(%{$config->{'attr_map'}});\r
303\r
84fb5b46 304 # Check that the user exists in the LDAP service\r
ecefa3a7
MKG
305 $RT::Logger->debug( "LDAP Search === ",\r
306 "Base:",\r
307 $base,\r
308 "== Filter:", \r
309 $filter->as_string,\r
310 "== Attrs:", \r
311 join(',',@attrs));\r
312 \r
313 my $user_found = $ldap->search( base => $base,\r
314 filter => $filter,\r
315 attrs => \@attrs);\r
84fb5b46
MKG
316\r
317 if($user_found->count < 1) {\r
318 # If 0 or negative integer, no user found or major failure\r
319 $RT::Logger->debug( "User Check Failed :: (",\r
320 $service,\r
321 ")",\r
322 $username,\r
323 "User not found"); \r
324 return 0; \r
325 } elsif ($user_found->count > 1) {\r
326 # If more than one result returned, die because we the username field should be unique!\r
327 $RT::Logger->debug( "User Check Failed :: (",\r
328 $service,\r
329 ")",\r
330 $username,\r
331 "More than one user with that username!");\r
332 return 0;\r
333 }\r
334 undef $user_found;\r
335 \r
336 # If we havent returned now, there must be a valid user.\r
337 return 1;\r
338}\r
339\r
340sub UserDisabled {\r
341\r
342 my ($username,$service) = @_;\r
343\r
344 # FIRST, check that the user exists in the LDAP service\r
345 unless(UserExists($username,$service)) {\r
346 $RT::Logger->debug("User (",$username,") doesn't exist! - Assuming not disabled for the purposes of disable checking");\r
347 return 0;\r
348 }\r
349 \r
350 my $config = $RT::ExternalSettings->{$service};\r
351 my $base = $config->{'base'};\r
352 my $filter = $config->{'filter'};\r
353 my $d_filter = $config->{'d_filter'};\r
354 my $search_filter;\r
355\r
356 # While LDAP filters must be surrounded by parentheses, an empty set\r
357 # of parentheses is an invalid filter and will cause failure\r
358 # This shouldn't matter since we are now using Net::LDAP::Filter below,\r
359 # but there's no harm in doing this to be sure\r
360 if ($filter eq "()") { undef($filter) };\r
361 if ($d_filter eq "()") { undef($d_filter) };\r
362\r
363 unless ($d_filter) {\r
364 # If we don't know how to check for disabled users, consider them all enabled.\r
365 $RT::Logger->debug("No d_filter specified for this LDAP service (",\r
366 $service,\r
367 "), so considering all users enabled");\r
368 return 0;\r
369 }\r
370\r
371 if (defined($config->{'attr_map'}->{'Name'})) {\r
372 # Construct the complex filter\r
373 $search_filter = Net::LDAP::Filter->new( '(&' . \r
374 $filter . \r
375 $d_filter . \r
376 '(' . \r
377 $config->{'attr_map'}->{'Name'} . \r
378 '=' . \r
379 $username . \r
380 '))'\r
381 );\r
382 } else {\r
383 $RT::Logger->debug("You haven't specified an LDAP attribute to match the RT \"Name\" attribute for this service (",\r
384 $service,\r
385 "), so it's impossible look up the disabled status of this user (",\r
386 $username,\r
387 ") so I'm just going to assume the user is not disabled");\r
388 return 0;\r
389 \r
390 }\r
391\r
392 my $ldap = _GetBoundLdapObj($config);\r
393 next unless $ldap;\r
394\r
ecefa3a7
MKG
395 # We only need the UID for confirmation now, \r
396 # the other information would waste time and bandwidth\r
397 my @attrs = ('uid'); \r
398 \r
399 $RT::Logger->debug( "LDAP Search === ",\r
400 "Base:",\r
401 $base,\r
402 "== Filter:", \r
403 $search_filter->as_string,\r
404 "== Attrs:", \r
405 join(',',@attrs));\r
406 \r
407 my $disabled_users = $ldap->search(base => $base, \r
408 filter => $search_filter, \r
409 attrs => \@attrs);\r
84fb5b46
MKG
410 # If ANY results are returned, \r
411 # we are going to assume the user should be disabled\r
412 if ($disabled_users->count) {\r
413 undef $disabled_users;\r
414 return 1;\r
415 } else {\r
416 undef $disabled_users;\r
417 return 0;\r
418 }\r
419}\r
420# {{{ sub _GetBoundLdapObj\r
421\r
422sub _GetBoundLdapObj {\r
423\r
424 # Config as hashref\r
425 my $config = shift;\r
426\r
427 # Figure out what's what\r
428 my $ldap_server = $config->{'server'};\r
429 my $ldap_user = $config->{'user'};\r
430 my $ldap_pass = $config->{'pass'};\r
431 my $ldap_tls = $config->{'tls'};\r
432 my $ldap_ssl_ver = $config->{'ssl_version'};\r
433 my $ldap_args = $config->{'net_ldap_args'};\r
434 \r
435 my $ldap = new Net::LDAP($ldap_server, @$ldap_args);\r
436 \r
437 unless ($ldap) {\r
438 $RT::Logger->critical( (caller(0))[3],\r
439 ": Cannot connect to",\r
440 $ldap_server);\r
441 return undef;\r
442 }\r
443\r
444 if ($ldap_tls) {\r
445 $Net::SSLeay::ssl_version = $ldap_ssl_ver;\r
446 # Thanks to David Narayan for the fault tolerance bits\r
447 eval { $ldap->start_tls; };\r
448 if ($@) {\r
449 $RT::Logger->critical( (caller(0))[3], \r
450 "Can't start TLS: ",\r
451 $@);\r
452 return;\r
453 }\r
454\r
455 }\r
456\r
457 my $msg = undef;\r
458\r
459 if (($ldap_user) and ($ldap_pass)) {\r
460 $msg = $ldap->bind($ldap_user, password => $ldap_pass);\r
461 } elsif (($ldap_user) and ( ! $ldap_pass)) {\r
462 $msg = $ldap->bind($ldap_user);\r
463 } else {\r
464 $msg = $ldap->bind;\r
465 }\r
466\r
467 unless ($msg->code == LDAP_SUCCESS) {\r
468 $RT::Logger->critical( (caller(0))[3], \r
469 "Can't bind:", \r
470 ldap_error_name($msg->code), \r
471 $msg->code);\r
472 return undef;\r
473 } else {\r
474 return $ldap;\r
475 }\r
476}\r
477\r
84fb5b46
MKG
478# }}}\r
479\r
4801;\r