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