]> git.uio.no Git - usit-rt.git/blob - local/plugins/RT-Authen-ExternalAuth.mikal-fixx/lib/RT/Authen/ExternalAuth/LDAP.pm
Initial commit 4.0.5-3
[usit-rt.git] / local / plugins / RT-Authen-ExternalAuth.mikal-fixx / lib / RT / Authen / ExternalAuth / LDAP.pm
1 package RT::Authen::ExternalAuth::LDAP;\r
2 \r
3 use Net::LDAP qw(LDAP_SUCCESS LDAP_PARTIAL_RESULTS);\r
4 use Net::LDAP::Util qw(ldap_error_name);\r
5 use Net::LDAP::Filter;\r
6 \r
7 use strict;\r
8 \r
9 require Net::SSLeay if $RT::ExternalServiceUsesSSLorTLS;\r
10 \r
11 sub GetAuth {\r
12     \r
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
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
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
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
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
149 sub CanonicalizeUserInfo {\r
150     \r
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
157 \r
158     # Load the config\r
159     my $config = $RT::ExternalSettings->{$service};\r
160    \r
161     # Figure out what's what\r
162     my $base            = $config->{'base'};\r
163     my $filter          = $config->{'filter'};\r
164 \r
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
179 \r
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
184         return ($found, %params);\r
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
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
245         } else {\r
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
257         }\r
258     }\r
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
271 }\r
272 \r
273 sub 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
278     my $base                = $config->{'base'};\r
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
302     my @attrs = values(%{$config->{'attr_map'}});\r
303 \r
304     # Check that the user exists in the LDAP service\r
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
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
340 sub 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
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
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
422 sub _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
478 # }}}\r
479 \r
480 1;\r