]> git.uio.no Git - usit-rt.git/blame - local/lib/RT/User_Local.not-pm
Merge branch 'master' of git.uio.no:usit-rt
[usit-rt.git] / local / lib / RT / User_Local.not-pm
CommitLineData
84fb5b46
MKG
1# BEGIN LICENSE BLOCK
2#
3# Copyright (c) 2004 Petter Reinholdtsen <pere@hungry.com>
4#
5# (Except where explictly superceded by other copyright notices)
6#
7# This work is made available to you under the terms of Version 2 of
8# the GNU General Public License. A copy of that license should have
9# been provided with this software, but in any event can be snarfed
10# from www.gnu.org.
11#
12# This work is distributed in the hope that it will be useful, but
13# WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15# General Public License for more details.
16#
17# Unless otherwise specified, all modifications, corrections or
18# extensions to this work which alter its source code become the
19# property of Best Practical Solutions, LLC when submitted for
20# inclusion in the work.
21#
22#
23# END LICENSE BLOCK
24
25
26# LDAP integration in RT 3. These overrides provide LDAP
27# authentication and user info syncronizing.
28#
29# Written by Petter Reinholdtsen <pere@hungry.com> based on Code from
30# Marcelo Bartsch <bartschm_cl@hotmail.com>, Stewart James
31# <stewart.james@vu.edu.au> and Carl Makin <carl@xena.IPAustralia.gov.au>.
32#
33# Copy this file into rt3/local/lib/RT/User_Local.pm to active it.
34
35
36# Modification Originally by Marcelo Bartsch <bartschm_cl@hotmail.com>
37# Update by Stewart James <stewart.james@vu.edu.au for rt3.
38# Update with TLS support and more flexible LDAP code by Petter Reinholdtsen.
39# Drop this file in /opt/rt3/lib/RT/User_Local.pm
40# Drop something like below in yout RT_SiteConfig.pm
41#
42# Set($LDAPExternalAuth, 1); # Enable LDAP auth
43# Set($LdapServer, "ldap.domain.com");
44# Set($LdapCAFile, "/site/w3-sertifikater/w3_cacert.pem");
45# Set($LdapAuthStartTLS, 1); # Need to use TLS or ldaps to check passwords
46# Set($LdapUser, ""); # Can search without username and password
47# Set($LdapAuthPass, "");
48# Set($LdapAuthBase, "ou=users,dc=domain,dc=com");
49# Set($LdapAuthUidAttr, "uid");
50# Set($LdapAuthFilter, "(objectclass=posixAccount)");
51
52# Tomas Olaj, 2005-11-01, endret koden etter tips fra Ruslan Zakirov:
53# <<<
54# 9. Edit /opt/rt3/lib/RT/User_Local.pm to fix a small logic error in
55# UIO's code. At line 374, change the != comparison to a ne comparison,
56# so that the line reads:
57#
58# if (defined($RT::LdapUser) && $RT::LdapUser ne '') {
59# >>>
60# Equive perl code for this is:
61# if ( $RT::LdapUser ) {
62# ...
63# }
64package RT::User;
65no warnings qw(redefine);
66
67# {{{ sub LookupExternalUserInfo
68
69=item LookupExternalUserInfo
70
71 LookupExternalUserInfo is a site-definable method for synchronizing
72 incoming users with an external data source.
73
74 This routine takes a tuple of EmailAddress and FriendlyName
75 EmailAddress is the user's email address, ususally taken from
76 an email message's From: header.
77 RealName is a freeform string, ususally taken from the "comment"
78 portion of an email message\'s From: header.
79
80 It returns (FoundInExternalDatabase, ParamHash);
81
82 FoundInExternalDatabase must be set to 1 before return if the user
83 was found in the external database.
84
85 ParamHash is a Perl parameter hash which can contain at least the
86 following fields. These fields are used to populate RT\'s users
87 database when the user is created
88
89 EmailAddress is the email address that RT should use for this user.
90 Name is the 'Name' attribute RT should use for this user.
91 'Name' is used for things like access control and user lookups.
92 RealName is what RT should display as the user\'s name when displaying
93 'friendly' names
94
95=cut
96
97sub LookupExternalUserInfo {
98 my %UserInfo = ();
99 $UserInfo{'EmailAddress'} = shift;
100 $UserInfo{'RealName'} = shift;
101 $UserInfo{'RealName'} =~ s/\"//g;
102
103 my $FoundInExternalDatabase = 0;
104
105 # Name is the RT username you want to use for this user.
106 my %LdapUserInfo = LdapUserFindByMailaddr($UserInfo{'EmailAddress'});
107 if ($LdapUserInfo{'Name'}) {
108 $FoundInExternalDatabase = 1;
109 $RT::Logger->debug("LookupExternalUserInfo: Mapping '".
110 $UserInfo{'EmailAddress'} .
111 "' to '" .
112 $LdapUserInfo{'Name'} . "'");
113 foreach my $key (keys %LdapUserInfo) {
114 $UserInfo{$key} = $LdapUserInfo{$key};
115 }
116 } else {
117 $RT::Logger->debug("LookupExternalUserInfo: Fail to find username for '".
118 $UserInfo{'EmailAddress'}."'");
119 }
120
121 return ($FoundInExternalDatabase, %UserInfo);
122}
123
124# }}}
125
126# {{{ sub CanonicalizeUserInfo
127
128sub CanonicalizeUserInfo {
129 my $self = shift;
130 my $args = shift;
131 my $success = 1;
132
133 my ($UserFoundInExternalDatabase, %ExternalUserInfo) =
134 LookupExternalUserInfo( $args->{'EmailAddress'},
135 $args->{'RealName'} );
136 if ($UserFoundInExternalDatabase) {
137 for my $key (keys %ExternalUserInfo) {
138 $args->{$key} = $ExternalUserInfo{$key};
139 }
140 }
141
142 return ($success);
143}
144
145# }}}
146
147# {{{ sub SetPasswordExternal
148
149=head2 SetPasswordExternal
150
151Takes a string, and try to set this string as the users password in an
152external system, if the user is listed in the external system.
153
154Returns 1 if the password was set successfully, undef if it failed,
155and -1 if the user is unknown to the external system.
156
157This hook is called from SetPassword.
158
159=cut
160
161sub SetPasswordExternal {
162 my $self = shift;
163 my $password = shift;
164
165 # Not allowed to set password for users in LDAP
166 if ($RT::LDAPExternalAuth) {
167 my $ldap = LdapConnect();
168 my $mesg;
169 if ( $mesg = LdapFindUser( $ldap, $self->Name )
170 && defined $mesg && $mesg->count ) {
171 LdapDisconnect($ldap);
172 return ( undef,
173 $self->loc("LDAP users must change password in LDAP") );
174 }
175 LdapDisconnect($ldap);
176 }
177 return (-1, "No such user in LDAP");
178}
179
180# }}}
181
182# {{{ sub SetPassword
183
184=head2 SetPassword
185
186Takes a string. Checks the string's length and sets this user's password
187to that string.
188
189Override for function in User_Overlay.pm, with modification for LDAP
190authentication.
191
192=cut
193
194sub SetPassword {
195 my $self = shift;
196 my $password = shift;
197
198 unless ( $self->CurrentUserCanModify('Password') ) {
199 return ( 0, $self->loc('Password: Permission Denied') );
200 }
201
202 my ($code, $msg) = $self->SetPasswordExternal($password);
203 return ($code, $msg) unless (-1 == $code);
204
205 if ( !$password ) {
206 return ( 0, $self->loc("No password set") );
207 }
208 elsif ( length($password) < $RT::MinimumPasswordLength ) {
209 return ( 0, $self->loc("Password needs to be at least [_1] characters long", $RT::MinimumPasswordLength) );
210 }
211 else {
212 my $new = !$self->HasPassword;
213 $password = $self->_GeneratePassword($password);
214 my ( $val, $msg ) = $self->SUPER::SetPassword($password);
215 if ($val) {
216 return ( 1, $self->loc("Password set") ) if $new;
217 return ( 1, $self->loc("Password changed") );
218 }
219 else {
220 return ( $val, $msg );
221 }
222 }
223
224}
225
226# }}}
227
228# {{{ sub IsPasswordExternal
229
230=head2 IsPasswordExternal
231
232Returns true if the passed in value is this user's password. Return
233undef if the password don't match. Return -1 if the user is unknown
234in the external system.
235
236This hook is called from IsPassword.
237
238=cut
239
240sub IsPasswordExternal {
241 my $self = shift;
242 my $value = shift;
243 # Let LDAP be authorative for users in LDAP, and only fall
244 # through for users without LDAP entry.
245 if ($RT::LDAPExternalAuth) {
246 return IsLdapPassword($self->Name, $value);
247 }
248}
249
250# }}}
251
252# {{{ sub IsPassword
253
254=head2 IsPassword
255
256Check the users password using LDAP. Override for function in
257User_Overlay.pm, with modification for LDAP authentication.
258
259=cut
260
261sub IsPassword {
262 my $self = shift;
263 my $value = shift;
264
265 #TODO there isn't any apparent way to legitimately ACL this
266
267 # RT does not allow null passwords
268 if ( ( !defined($value) ) or ( $value eq '' ) ) {
269 return (undef);
270 }
271
272 if ( $self->PrincipalObj->Disabled ) {
273 $RT::Logger->info(
274 "Disabled user " . $self->Name . " tried to log in" );
275 return (undef);
276 }
277
278 my $code = $self->IsPasswordExternal($value);
279 return ($code) unless (-1 == $code);
280
281 unless ($self->HasPassword) {
282 return(undef);
283 }
284
285 my $stored = $self->__Value('Password');
286 if (length $stored == 40) {
287 # The truncated SHA256(salt,MD5(passwd)) form from 2010/12 is 40 characters long
288 my $hash = MIME::Base64::decode_base64($stored);
289 # The first 4 bytes are the salt, the rest is substr(SHA256,0,26)
290 my $salt = substr($hash, 0, 4, "");
291 return substr(Digest::SHA::sha256($salt . Digest::MD5::md5($value)), 0, 26) eq $hash;
292 } elsif (length $stored == 32) {
293 # Hex nonsalted-md5
294 return 0 unless Digest::MD5::md5_hex(encode_utf8($value)) eq $stored;
295 } elsif (length $stored == 22) {
296 # Base64 nonsalted-md5
297 return 0 unless Digest::MD5::md5_base64(encode_utf8($value)) eq $stored;
298 } elsif (length $stored == 13) {
299 # crypt() output
300 return 0 unless crypt(encode_utf8($value), $stored) eq $stored;
301 } else {
302 $RT::Logger->warn("Unknown password form");
303 return 0;
304 }
305
306 # We got here by validating successfully, but with a legacy
307 # password form. Update to the most recent form.
308 my $obj = $self->isa("RT::CurrentUser") ? $self->UserObj : $self;
309 $obj->_Set(Field => 'Password', Value => $self->_GeneratePassword($value) );
310 return 1;
311
312}
313
314# }}}
315
316# {{{ sub LdapUserFindByMailaddr
317
318=head2 LdapUserFindByMailaddr
319
320Lookup user owning a given email address on UiO, returning user info
321in a hash or undef if not known or the search failed.
322
323The following configure options are used by this function in addition
324to the ones used by LdapConnect().
325
326 $RT::LdapMailBase
327 $RT::LdapMailFilter
328 $RT::LdapMailScope
329 $RT::LdapMailSearchAttr
330 $RT::LdapMailMap
331
332=cut
333
334# Example search
335# ldapsearch -x -b ou=mail,dc=uio,dc=no -ZZ -h ldap.uio.no -D uid=pre,ou=users,dc=uio,dc=no -W target=mathiasm
336
337sub LdapUserFindByMailaddr {
338 my $mailaddr = shift;
339 my %UserInfo = ();
340 $ldap = LdapConnect();
341 my $filter = "(&($RT::LdapMailSearchAttr=$mailaddr)$RT::LdapMailFilter)";
342 my @attr = keys %RT::LdapMailSearchResultMap;
343 $RT::Logger->debug( "LdapUserFindByMailaddr: Looking for ",
344 join(" ", @attr), " filter=", $filter );
345 $mesg = $ldap->search(
346 base => $RT::LdapMailBase,
347 scope => $RT::LdapMailScope,
348 filter => $filter,
349 attributes => [@attr],
350 );
351 if ( ($mesg->code != LDAP_SUCCESS) and
352 ($mesg->code != LDAP_PARTIAL_RESULTS) ) {
353 $RT::Logger->critical("LdapUserFindByMailaddr: Search failed: ",
354 "retval=", $mesg->code, " ",
355 ldap_error_name($mesg->code));
356 LdapDisconnect($ldap);
357 return undef;
358 }
359
360 if (1 != $mesg->count) {
361 LdapDisconnect($ldap);
362 return undef;
363 }
364
365 while( my $entry = $mesg->shift_entry) {
366 foreach my $attr (keys %RT::LdapMailSearchResultMap) {
367 foreach my $value ($entry->get_value($attr)) {
368 $UserInfo{$RT::LdapMailSearchResultMap{$attr}} = $value;
369 }
370 }
371 }
372 LdapDisconnect($ldap);
373 return %UserInfo;
374}
375
376# {{{ sub LdapConnect
377
378=head2 LdapConnect
379
380Connect to the LDAP databsae.
381
382The following configure options are used by this function:
383
384 $RT::LdapServer
385 $RT::LdapUser
386 $RT::LdapPass
387
388=cut
389
390sub LdapConnect {
391 use Net::LDAP qw(LDAP_SUCCESS LDAP_PARTIAL_RESULTS);
392 use Net::LDAP::Util qw (ldap_error_name);
393
394 print $@ if $@;
395
396 my $mesg;
397 my $ldap = Net::LDAP->new($RT::LdapServer,
398 version => 3);
399
400 unless ($ldap) {
401 $RT::Logger->critical("IsLdapPassword: Cannot connect to",
402 "LDAP server ", $RT::LdapServer);
403 return undef;
404 }
405
406 # I seem to have problems if I try and bind with a NULL username
407 # by hand So this now checks to see if we are really going to bind
408 # with a username.
409 if (defined($RT::LdapUser) && $RT::LdapUser ne '') {
410 $mesg = $ldap->bind($RT::LdapUser,
411 password => $RT::LdapPass );
412 } else {
413 # This bind is redundant with LDAP protocol version 3
414 $mesg = $ldap->bind;
415 }
416 if ($mesg->code != LDAP_SUCCESS) {
417 $RT::Logger->critical("IsLdapPassword: Cannot bind to LDAP: ",
418 "retval=", $mesg->code, " ",
419 ldap_error_name($mesg->code));
420 return undef;
421 }
422 return $ldap;
423}
424
425# }}}
426
427# {{{ sub LdapDisconnect
428
429=head2 LdapDisconnect
430
431Disconnect from the LDAP database.
432
433=cut
434
435sub LdapDisconnect {
436 my $ldap = shift;
437 my $mesg = $ldap->unbind();
438 if ($mesg->code != LDAP_SUCCESS) {
439 $RT::Logger->critical("LdapDisconnect: unbind failed: ",
440 "retval=", $mesg->code, " ",
441 ldap_error_name($mesg->code));
442 }
443}
444
445# }}}
446
447# {{{ sub LdapFindUser
448
449=head2 LdapFindUser
450
451Locate info on a giver user given the username.
452
453Configure options used by this function:
454
455 $RT::LdpaAuthBase
456 $RT::LdpaAuthFilter
457 $RT::LdpaAuthUidAttr
458
459=cut
460
461sub LdapFindUser {
462 my $ldap = shift;
463 my $username = shift;
464
465 my $filter;
466 if ($RT::LdapAuthFilter) {
467 $filter = "(&(" .$RT::LdapAuthUidAttr . "=$username)$RT::LdapAuthFilter)";
468 } else {
469 $filter = "(" .$RT::LdapAuthUidAttr . "=$username)";
470 }
471
472 $RT::Logger->debug("IsLdapPassword: First search filter '$filter'");
473 my $mesg = $ldap->search(base => $RT::LdapAuthBase,
474 filter => $filter,
475 attrs => ['dn']);
476 if (!(($mesg->code == LDAP_SUCCESS) or
477 ($mesg->code == LDAP_PARTIAL_RESULTS)))
478 {
479 $RT::Logger->debug("IsLdapPassword: Could not search for $filter: ",
480 "retval=", $mesg->code, " ",
481 ldap_error_name($mesg->code));
482 return undef;
483 }
484 return $mesg;
485}
486
487# }}}
488
489# {{{ sub IsLdapPassword
490
491=head2 IsLdapPassword
492
493Takes a username and password as argument, and check if the password
494is correct for the given user. Return undef if password check failed,
495-1 if the user is unknown, and 1 if the password check succeeded.
496
497=cut
498
499sub IsLdapPassword {
500 my $username = shift;
501 my $value = shift;
502
503 $RT::Logger->debug("IsLdapPassword: executing");
504 my $ldap = LdapConnect();
505 return undef unless $ldap;
506
507 my $mesg = LdapFindUser($ldap, $username);
508 unless ($mesg) {
509 LdapDisconnect($ldap);
510 return undef;
511 }
512 $RT::Logger->debug("IsLdapPassword: First search produced ",
513 $mesg->count, " results");
514 if (! $mesg->count)
515 {
516 $RT::Logger->info("IsLdapPassword: AUTH FAILED $username");
517 LdapDisconnect($ldap);
518 return -1;
519 }
520 $ldap->start_tls( verify => 'require',
521 cafile => $RT::LdapCAFile ) if ($RT::LdapAuthStartTLS);
522
523 my $userdn = $mesg->first_entry->dn;
524 $RT::Logger->debug("IsLdapPassword: Trying to bind using DN=$userdn");
525 my $mesg2 = $ldap->bind($userdn,
526 password => $value );
527 if ($mesg2->code != LDAP_SUCCESS) {
528 $RT::Logger->info("IsLdapPassword: Unable to bind as $userdn: ",
529 "retval=", $mesg2->code, " ",
530 ldap_error_name($mesg2->code));
531 LdapDisconnect($ldap);
532 return undef;
533 }
534 else
535 {
536 $RT::Logger->info("IsLdapPassword: AUTH OK $username ($userdn) base:",
537 $RT::LdapAuthBase);
538 LdapDisconnect($ldap);
539 return 1;
540 }
541}
542
543# }}}
544
5451;