1 package RT::Authen::ExternalAuth::LDAP;
\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
9 require Net::SSLeay if $RT::ExternalServiceUsesSSLorTLS;
\r
13 my ($service, $username, $password) = @_;
\r
15 my $config = $RT::ExternalSettings->{$service};
\r
16 $RT::Logger->debug( "Trying external auth service:",$service);
\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
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
30 # Now let's get connected
\r
31 my $ldap = _GetBoundLdapObj($config);
\r
32 return 0 unless ($ldap);
\r
34 $filter = Net::LDAP::Filter->new( '(&(' .
\r
35 $attr_map->{'Name'} .
\r
43 $RT::Logger->debug( "LDAP Search === ",
\r
51 my $ldap_msg = $ldap->search( base => $base,
\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
59 ldap_error_name($ldap_msg->code),
\r
61 # Didn't even get a partial result - jump straight to the next external auth service
\r
65 unless ($ldap_msg->count == 1) {
\r
66 $RT::Logger->info( $service,
\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
74 my $ldap_dn = $ldap_msg->first_entry->dn;
\r
75 $RT::Logger->debug( "Found LDAP DN:",
\r
78 # THIS bind determines success or failure on the password.
\r
79 $ldap_msg = $ldap->bind($ldap_dn, password => $password);
\r
81 unless ($ldap_msg->code == LDAP_SUCCESS) {
\r
82 $RT::Logger->info( $service,
\r
86 ldap_error_name($ldap_msg->code),
\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
95 # The user is authenticated ok, but is there an LDAP Group to check?
\r
97 # If we've been asked to check a group...
\r
98 $filter = Net::LDAP::Filter->new("(${group_attr}=${ldap_dn})");
\r
100 $RT::Logger->debug( "LDAP Search === ",
\r
104 $filter->as_string,
\r
108 $ldap_msg = $ldap->search( base => $group,
\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
119 ldap_error_name($ldap_msg->code),
\r
122 # Fail auth - jump to next external auth service
\r
126 unless ($ldap_msg->count == 1) {
\r
127 $RT::Logger->info( $service,
\r
131 # Fail auth - jump to next external auth service
\r
136 # Any other checks you want to add? Add them here.
\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
149 sub CanonicalizeUserInfo {
\r
151 my ($service, $key, $value) = @_;
\r
154 my %params = (Name => undef,
\r
155 EmailAddress => undef,
\r
156 RealName => undef);
\r
159 my $config = $RT::ExternalSettings->{$service};
\r
161 # Figure out what's what
\r
162 my $base = $config->{'base'};
\r
163 my $filter = $config->{'filter'};
\r
165 # Get the list of unique attrs we need
\r
166 my @attrs = values(%{$config->{'attr_map'}});
\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
173 $filter_addition .
\r
177 $RT::Logger->debug( "LDAP Filter invalid or not present.");
\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
187 # Get a Net::LDAP object based on the config we provide
\r
188 my $ldap = _GetBoundLdapObj($config);
\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
194 # Do a search for them in LDAP
\r
195 $RT::Logger->debug( "LDAP Search === ",
\r
199 $filter->as_string,
\r
203 my $ldap_msg = $ldap->search(base => $base,
\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
212 $filter->as_string,
\r
214 ldap_error_name($ldap_msg->code),
\r
216 # $found remains as 0
\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
228 return ($found, %params);
\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
240 ($entry->get_value($config->{'attr_map'}->{$key}))[0];
\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
256 return ($found, %params);
\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
270 return ($found, %params);
\r
274 my ($username,$service) = @_;
\r
275 $RT::Logger->debug("UserExists params:\nusername: $username , service: $service");
\r
276 my $config = $RT::ExternalSettings->{$service};
\r
278 my $base = $config->{'base'};
\r
279 my $filter = $config->{'filter'};
\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
287 if (defined($config->{'attr_map'}->{'Name'})) {
\r
288 # Construct the complex filter
\r
289 $filter = Net::LDAP::Filter->new( '(&' .
\r
292 $config->{'attr_map'}->{'Name'} .
\r
299 my $ldap = _GetBoundLdapObj($config);
\r
300 return unless $ldap;
\r
302 my @attrs = values(%{$config->{'attr_map'}});
\r
304 # Check that the user exists in the LDAP service
\r
305 $RT::Logger->debug( "LDAP Search === ",
\r
309 $filter->as_string,
\r
313 my $user_found = $ldap->search( base => $base,
\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
323 "User not found");
\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
331 "More than one user with that username!");
\r
336 # If we havent returned now, there must be a valid user.
\r
342 my ($username,$service) = @_;
\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
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
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
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
367 "), so considering all users enabled");
\r
371 if (defined($config->{'attr_map'}->{'Name'})) {
\r
372 # Construct the complex filter
\r
373 $search_filter = Net::LDAP::Filter->new( '(&' .
\r
377 $config->{'attr_map'}->{'Name'} .
\r
383 $RT::Logger->debug("You haven't specified an LDAP attribute to match the RT \"Name\" attribute for this service (",
\r
385 "), so it's impossible look up the disabled status of this user (",
\r
387 ") so I'm just going to assume the user is not disabled");
\r
392 my $ldap = _GetBoundLdapObj($config);
\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
399 $RT::Logger->debug( "LDAP Search === ",
\r
403 $search_filter->as_string,
\r
407 my $disabled_users = $ldap->search(base => $base,
\r
408 filter => $search_filter,
\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
416 undef $disabled_users;
\r
420 # {{{ sub _GetBoundLdapObj
\r
422 sub _GetBoundLdapObj {
\r
424 # Config as hashref
\r
425 my $config = shift;
\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
435 my $ldap = new Net::LDAP($ldap_server, @$ldap_args);
\r
438 $RT::Logger->critical( (caller(0))[3],
\r
439 ": Cannot connect to",
\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
449 $RT::Logger->critical( (caller(0))[3],
\r
450 "Can't start TLS: ",
\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
464 $msg = $ldap->bind;
\r
467 unless ($msg->code == LDAP_SUCCESS) {
\r
468 $RT::Logger->critical( (caller(0))[3],
\r
470 ldap_error_name($msg->code),
\r