Auth.pm 16.3 KB
Newer Older
1
2
3
4
# -*- indent-tabs-mode: nil; -*-
# vim:ft=perl:et:sw=4
# $Id$

salaun's avatar
salaun committed
5
# Sympa - SYsteme de Multi-Postage Automatique
6
7
8
9
10
#
# Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel
# Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005,
# 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites
# Copyright (c) 2011, 2012, 2013, 2014 GIP RENATER
salaun's avatar
salaun committed
11
12
13
14
15
16
17
18
19
20
21
22
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
23
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
salaun's avatar
salaun committed
24
25
26

package Auth;

27
28
use strict;
use warnings;
29
use POSIX qw();
salaun's avatar
salaun committed
30
31

use Conf;
32
use LDAPSource;
salaun's avatar
salaun committed
33
use List;
34
use Log;
35
use report;
36
use SDM;
salaun's avatar
salaun committed
37

38
39
40
## return the password finger print (this proc allow futur replacement of md5 by sha1 or ....)
sub password_fingerprint{

sikeda's avatar
sikeda committed
41
    Log::do_log('debug', 'Auth::password_fingerprint');
42
43

    my $pwd = shift;
sikeda's avatar
sikeda committed
44
45
    if(Conf::get_robot_conf('*','password_case') eq 'insensitive') {
	return tools::md5_fingerprint(lc($pwd));
46
    }else{
sikeda's avatar
sikeda committed
47
	return tools::md5_fingerprint($pwd);
48
49
50
    }    
}

salaun's avatar
salaun committed
51

52
## authentication : via email or uid
salaun's avatar
salaun committed
53
 sub check_auth{
54
     my $robot = shift;
salaun's avatar
salaun committed
55
56
     my $auth = shift; ## User email or UID
     my $pwd = shift; ## Password
sikeda's avatar
sikeda committed
57
     Log::do_log('debug', 'Auth::check_auth(%s)', $auth);
salaun's avatar
salaun committed
58
59
60

     my ($canonic, $user);

sikeda's avatar
sikeda committed
61
62
     if( tools::valid_email($auth)) {
	 return authentication($robot, $auth,$pwd);
salaun's avatar
salaun committed
63
64
     }else{
	 ## This is an UID
sikeda's avatar
sikeda committed
65
	 foreach my $ldap (@{$Conf::Conf{'auth_services'}{$robot}}){
66
67
68
	     # only ldap service are to be applied here
	     next unless ($ldap->{'auth_type'} eq 'ldap');
	     
sikeda's avatar
sikeda committed
69
	     $canonic = ldap_authentication($robot, $ldap, $auth,$pwd,'uid_filter');
70
71
72
73
	     last if ($canonic); ## Stop at first match
	 }
	 if ($canonic){
	     
74
	     unless($user = Sympa::User::get_global_user($canonic)){
salaun's avatar
salaun committed
75
76
		 $user = {'email' => $canonic};
	     }
77
78
79
80
	     return {'user' => $user,
		     'auth' => 'ldap',
		     'alt_emails' => {$canonic => 'ldap'}
		 };
81
	     
salaun's avatar
salaun committed
82
	 }else{
sikeda's avatar
sikeda committed
83
	     report::reject_report_web('user','incorrect_passwd',{}) unless ($ENV{'SYMPA_SOAP'});
84
	     Log::do_log('err', "Incorrect LDAP password");
salaun's avatar
salaun committed
85
86
87
88
89
	     return undef;
	 }
     }
 }

90
91
92
93
94
95
96
97
98
99
## This subroutine if Sympa may use its native authentication for a given user
## It might not if no user_table paragraph is found in auth.conf or if the regexp or
## negative_regexp exclude this user
## IN : robot, user email
## OUT : boolean
sub may_use_sympa_native_auth {
    my ($robot, $user_email) = @_;

    my $ok = 0;
    ## check each auth.conf paragrpah
sikeda's avatar
sikeda committed
100
    foreach my $auth_service (@{$Conf::Conf{'auth_services'}{$robot}}){
101
102
103
104
105
106
107
108
109
110
	next unless ($auth_service->{'auth_type'} eq 'user_table');

	next if ($auth_service->{'regexp'} && ($user_email !~ /$auth_service->{'regexp'}/i));
	next if ($auth_service->{'negative_regexp'} && ($user_email =~ /$auth_service->{'negative_regexp'}/i));
	
	$ok = 1; last;
    }
    
    return $ok;
}
salaun's avatar
salaun committed
111
112

sub authentication {
113
    my ($robot, $email,$pwd) = @_;
salaun's avatar
salaun committed
114
    my ($user,$canonic);
sikeda's avatar
sikeda committed
115
    Log::do_log('debug', 'Auth::authentication(%s)', $email);
salaun's avatar
salaun committed
116

salaun's avatar
salaun committed
117

118
    unless ($user = Sympa::User::get_global_user($email)) {
119
	$user = {'email' => $email };
salaun's avatar
salaun committed
120
121
    }    
    unless ($user->{'password'}) {
122
	$user->{'password'} = '';
salaun's avatar
salaun committed
123
124
    }
    
sikeda's avatar
sikeda committed
125
    if ($user->{'wrong_login_count'} > Conf::get_robot_conf($robot, 'max_wrong_password')){
126
	# too many wrong login attemp
127
	Sympa::User::update_global_user($email,{wrong_login_count => $user->{'wrong_login_count'}+1}) ;
sikeda's avatar
sikeda committed
128
129
	report::reject_report_web('user','too_many_wrong_login',{}) unless ($ENV{'SYMPA_SOAP'});
	Log::do_log('err','login is blocked : too many wrong password submission for %s', $email);
130
131
	return undef;
    }
sikeda's avatar
sikeda committed
132
    foreach my $auth_service (@{$Conf::Conf{'auth_services'}{$robot}}){
133
	next if ($auth_service->{'auth_type'} eq 'authentication_info_url');
salaun's avatar
salaun committed
134
135
	next if ($email !~ /$auth_service->{'regexp'}/i);
	next if (($email =~ /$auth_service->{'negative_regexp'}/i)&&($auth_service->{'negative_regexp'}));
david.verdin's avatar
david.verdin committed
136
137
138

	## Only 'user_table' and 'ldap' backends will need that Sympa collects the user passwords
	## Other backends are Single Sign-On solutions
salaun's avatar
salaun committed
139
	if ($auth_service->{'auth_type'} eq 'user_table') {
sikeda's avatar
sikeda committed
140
	    my $fingerprint = password_fingerprint ($pwd);	    	    
141
	    
142
	    if ($fingerprint eq $user->{'password'}) {
143
		Sympa::User::update_global_user($email,{wrong_login_count => 0}) ;
144
145
146
147
		return {'user' => $user,
			'auth' => 'classic',
			'alt_emails' => {$email => 'classic'}
			};
salaun's avatar
salaun committed
148
149
	    }
	}elsif($auth_service->{'auth_type'} eq 'ldap') {
sikeda's avatar
sikeda committed
150
	    if ($canonic = ldap_authentication($robot, $auth_service, $email,$pwd,'email_filter')){
151
		unless($user = Sympa::User::get_global_user($canonic)){
salaun's avatar
salaun committed
152
153
		    $user = {'email' => $canonic};
		}
154
		Sympa::User::update_global_user($canonic,{wrong_login_count => 0}) ;
155
156
157
158
		return {'user' => $user,
			'auth' => 'ldap',
			'alt_emails' => {$email => 'ldap'}
			};
salaun's avatar
salaun committed
159
160
161
	    }
	}
    }
salaun's avatar
salaun committed
162

163
    # increment wrong login count.
164
    Sympa::User::update_global_user($email,{wrong_login_count =>$user->{'wrong_login_count'}+1}) ;
165

sikeda's avatar
sikeda committed
166
167
    report::reject_report_web('user','incorrect_passwd',{}) unless ($ENV{'SYMPA_SOAP'});
    Log::do_log('err','authentication: incorrect password for user %s', $email);
salaun's avatar
salaun committed
168

169
    my $param; #FIXME FIXME: not used.
salaun's avatar
salaun committed
170
    $param->{'init_email'} = $email;
sikeda's avatar
sikeda committed
171
    $param->{'escaped_init_email'} = tools::escape_chars($email);
salaun's avatar
salaun committed
172
173
174
175
176
    return undef;
}


sub ldap_authentication {
177
178
     my ($robot, $ldap, $auth, $pwd, $whichfilter) = @_;
     my ($mesg, $host,$ldap_passwd,$ldap_anonymous);
sikeda's avatar
sikeda committed
179
180
     Log::do_log('debug2','Auth::ldap_authentication(%s,%s,%s)', $auth,'****',$whichfilter);
     Log::do_log('debug3','Password used: %s',$pwd);
salaun's avatar
salaun committed
181

182
     unless (tools::search_fullpath($robot, 'auth.conf')) {
salaun's avatar
salaun committed
183
184
185
186
	 return undef;
     }

     ## No LDAP entry is defined in auth.conf
sikeda's avatar
sikeda committed
187
188
     if ($#{$Conf::Conf{'auth_services'}{$robot}} < 0) {
	 Log::do_log('notice', 'Skipping empty auth.conf');
salaun's avatar
salaun committed
189
190
191
	 return undef;
     }

192
193
194
195
196
197
198
199
200
201
202
203
204
205
     # only ldap service are to be applied here
     return undef unless ($ldap->{'auth_type'} eq 'ldap');
     
     # skip ldap auth service if the an email address was provided
     # and this email address does not match the corresponding regexp 
     return undef if ($auth =~ /@/ && $auth !~ /$ldap->{'regexp'}/i);
     
     my @alternative_conf = split(/,/,$ldap->{'alternative_email_attribute'});
     my $attrs = $ldap->{'email_attribute'};
     my $filter = $ldap->{'get_dn_by_uid_filter'} if($whichfilter eq 'uid_filter');
     $filter = $ldap->{'get_dn_by_email_filter'} if($whichfilter eq 'email_filter');
     $filter =~ s/\[sender\]/$auth/ig;
     
     ## bind in order to have the user's DN
sikeda's avatar
sikeda committed
206
     my $param = tools::dup_var($ldap);
207
     my $ds = LDAPSource->new($param);
208
209
     
     unless (defined $ds && ($ldap_anonymous = $ds->connect())) {
sikeda's avatar
sikeda committed
210
       Log::do_log('err',"Unable to connect to the LDAP server '%s'", $ldap->{'host'});
211
       return undef;
salaun's avatar
salaun committed
212
     }
213
214
215
216
217
218
219
220
     
     
     $mesg = $ldap_anonymous->search(base => $ldap->{'suffix'},
				     filter => "$filter",
				     scope => $ldap->{'scope'} ,
				     timeout => $ldap->{'timeout'});
     
     if ($mesg->count() == 0) {
221
       Log::do_log('notice','No entry in the LDAP Directory Tree of %s for %s',$ldap->{'host'},$auth);
222
223
       $ds->disconnect();
       return undef;
salaun's avatar
salaun committed
224
     }
225
226
227
228
229
230
231
232
233
     
     my $refhash=$mesg->as_struct();
     my (@DN) = keys(%$refhash);
     $ds->disconnect();
     
     ##  bind with the DN and the pwd
     
     ## Duplicate structure first
     ## Then set the bind_dn and password according to the current user
sikeda's avatar
sikeda committed
234
     $param = tools::dup_var($ldap);
235
236
237
     $param->{'ldap_bind_dn'} = $DN[0];
     $param->{'ldap_bind_password'} = $pwd;
     
238
     $ds = LDAPSource->new($param);
239
240
     
     unless (defined $ds && ($ldap_passwd = $ds->connect())) {
sikeda's avatar
sikeda committed
241
       Log::do_log('err',"Unable to connect to the LDAP server '%s'", $param->{'host'});
242
       return undef;
salaun's avatar
salaun committed
243
244
     }
     
245
246
247
248
249
250
251
     $mesg= $ldap_passwd->search ( base => $ldap->{'suffix'},
				   filter => "$filter",
				   scope => $ldap->{'scope'},
				   timeout => $ldap->{'timeout'}
				 );
     
     if ($mesg->count() == 0 || $mesg->code() != 0) {
252
       Log::do_log('notice',"No entry in the LDAP Directory Tree of %s", $ldap->{'host'});
253
254
       $ds->disconnect();
       return undef;
salaun's avatar
salaun committed
255
     }
256
257
258
259
260
261
262
263
264
265
266
267
     
     ## To get the value of the canonic email and the alternative email
     my (@canonic_email, @alternative);
     
     ## Keep previous alt emails not from LDAP source
     my $previous = {};
     foreach my $alt (keys %{$param->{'alt_emails'}}) {
       $previous->{$alt} = $param->{'alt_emails'}{$alt} if ($param->{'alt_emails'}{$alt} ne 'ldap');
     }
     $param->{'alt_emails'} = {};
     
     my $entry = $mesg->entry(0);
268
     @canonic_email = $entry->get_value($attrs, 'alloptions' => 1);
269
270
271
272
273
274
     foreach my $email (@canonic_email){
       my $e = lc($email);
       $param->{'alt_emails'}{$e} = 'ldap' if ($e);
     }
     
     foreach my $attribute_value (@alternative_conf){
275
       @alternative = $entry->get_value($attribute_value, 'alloptions' => 1);
276
277
278
279
280
281
282
283
284
285
286
       foreach my $alter (@alternative){
	 my $a = lc($alter); 
	 $param->{'alt_emails'}{$a} = 'ldap' if($a) ;
       }
     }
     
     ## Restore previous emails
     foreach my $alt (keys %{$previous}) {
       $param->{'alt_emails'}{$alt} = $previous->{$alt};
     }
     
sikeda's avatar
sikeda committed
287
288
     $ds->disconnect() or Log::do_log('notice', "unable to unbind");
     Log::do_log('debug3',"canonic: $canonic_email[0]");
289
290
     ## If the identifier provided was a valid email, return the provided email.
     ## Otherwise, return the canonical email guessed after the login.
sikeda's avatar
sikeda committed
291
     if( tools::valid_email($auth) && !Conf::get_robot_conf($robot,'ldap_force_canonical_email')) {
292
293
294
295
296
	 return ($auth);
     }else{
	 return lc($canonic_email[0]);
     } 
}
salaun's avatar
salaun committed
297
298
299


# fetch user email using his cas net_id and the paragrapah number in auth.conf
300
sub get_email_by_net_id {
salaun's avatar
salaun committed
301
    
302
    my $robot = shift;
salaun's avatar
salaun committed
303
    my $auth_id = shift;
304
    my $attributes = shift;
305
    
sikeda's avatar
sikeda committed
306
    Log::do_log ('debug',"Auth::get_email_by_net_id($auth_id,$attributes->{'uid'})");
307
    
sikeda's avatar
sikeda committed
308
309
    if (defined $Conf::Conf{'auth_services'}{$robot}[$auth_id]{'internal_email_by_netid'}) {
	my $sso_config = @{$Conf::Conf{'auth_services'}{$robot}}[$auth_id];
310
311
312
313
	my $netid_cookie = $sso_config->{'netid_http_header'} ;
	
	$netid_cookie =~ s/(\w+)/$attributes->{$1}/ig;
	
sikeda's avatar
sikeda committed
314
	my $email = List::get_netidtoemail_db($robot, $netid_cookie, $Conf::Conf{'auth_services'}{$robot}[$auth_id]{'service_id'});
315
316
317
318
	
	return $email;
    }
 
sikeda's avatar
sikeda committed
319
    my $ldap = @{$Conf::Conf{'auth_services'}{$robot}}[$auth_id];
320

sikeda's avatar
sikeda committed
321
    my $param = tools::dup_var($ldap);
322
    my $ds = LDAPSource->new($param);
323
    my $ldap_anonymous;
salaun's avatar
salaun committed
324
    
325
    unless (defined $ds && ($ldap_anonymous = $ds->connect())) {
sikeda's avatar
sikeda committed
326
	Log::do_log('err',"Unable to connect to the LDAP server '%s'", $ldap->{'ldap_host'});
salaun's avatar
salaun committed
327
328
329
330
	return undef;
    }

    my $filter = $ldap->{'ldap_get_email_by_uid_filter'} ;
331
    $filter =~ s/\[([\w-]+)\]/$attributes->{$1}/ig;
salaun's avatar
salaun committed
332
333
334
335
336
337
338

#	my @alternative_conf = split(/,/,$ldap->{'alternative_email_attribute'});
		
	my $emails= $ldap_anonymous->search ( base => $ldap->{'ldap_suffix'},
				      filter => $filter,
				      scope => $ldap->{'ldap_scope'},
				      timeout => $ldap->{'ldap_timeout'},
339
				      attrs =>  [$ldap->{'ldap_email_attribute'}],
salaun's avatar
salaun committed
340
341
342
343
				      );
	my $count = $emails->count();

	if ($emails->count() == 0) {
344
            Log::do_log('notice', "No entry in the LDAP Directory Tree of %s",
345
                $ldap->{'ldap_host'});
346
347
	$ds->disconnect();
	return undef;
salaun's avatar
salaun committed
348
349
	}

350
351
352
    $ds->disconnect();
    
    ## return only the first attribute
salaun's avatar
salaun committed
353
354
355
356
357
358
359
	my @results = $emails->entries;
	foreach my $result (@results){
	    return (lc($result->get_value($ldap->{'ldap_email_attribute'})));
	}

 }

360
361
362
363
# check trusted_application_name et trusted_application_password : return 1 or undef;
sub remote_app_check_password {
    
    my ($trusted_application_name,$password,$robot) = @_;
sikeda's avatar
sikeda committed
364
    Log::do_log('debug','Auth::remote_app_check_password (%s,%s)',$trusted_application_name,$robot);
365
    
sikeda's avatar
sikeda committed
366
    my $md5 = tools::md5_fingerprint($password);
367
368
369
370
371
    
    my $vars;
    # seach entry for trusted_application in Conf
    my @trusted_apps ;
    
372
    # select trusted_apps from robot context or sympa context
sikeda's avatar
sikeda committed
373
    @trusted_apps = @{Conf::get_robot_conf($robot,'trusted_applications')};
374
375
376
    
    foreach my $application (@trusted_apps){
	
377
 	if (lc($application->{'name'}) eq lc($trusted_application_name)) {
378
 	    if ($md5 eq $application->{'md5password'}) {
sikeda's avatar
sikeda committed
379
 		# Log::do_log('debug', 'Auth::remote_app_check_password : authentication succeed for %s',$application->{'name'});
380
381
382
383
384
385
 		my %proxy_for_vars ;
 		foreach my $varname (@{$application->{'proxy_for_variables'}}) {
 		    $proxy_for_vars{$varname}=1;
 		}		
 		return (\%proxy_for_vars);
 	    }else{
sikeda's avatar
sikeda committed
386
 		Log::do_log('info', 'Auth::remote_app_check_password: bad password from %s', $trusted_application_name);
387
388
389
390
391
 		return undef;
 	    }
 	}
    }				 
    # no matching application found
sikeda's avatar
sikeda committed
392
    Log::do_log('info', 'Auth::remote_app-check_password: unknown application name %s', $trusted_application_name);
393
394
395
    return undef;
}
 
396
397
398
399
400
401
402
# create new entry in one_time_ticket table using a rand as id so later access is authenticated
#

sub create_one_time_ticket {
    my $email = shift;
    my $robot = shift;
    my $data_string = shift;
403
    my $remote_addr = shift; ## Value may be 'mail' if the IP address is not known
404

sikeda's avatar
sikeda committed
405
406
    my $ticket = SympaSession::get_random();
    Log::do_log('info', 'Auth::create_one_time_ticket(%s,%s,%s,%s) value = %s',$email,$robot,$data_string,$remote_addr,$ticket);
407
408
409
410

    my $date = time;
    my $sth;
    
sikeda's avatar
sikeda committed
411
412
    unless (SDM::do_query("INSERT INTO one_time_ticket_table (ticket_one_time_ticket, robot_one_time_ticket, email_one_time_ticket, date_one_time_ticket, data_one_time_ticket, remote_addr_one_time_ticket, status_one_time_ticket) VALUES (%s, %s, %s, %d, %s, %s, %s)",SDM::quote($ticket),SDM::quote($robot),SDM::quote($email),time,SDM::quote($data_string),SDM::quote($remote_addr),SDM::quote('open'))) {
	Log::do_log('err','Unable to insert new one time ticket for user %s, robot %s in the database',$email,$robot);
413
414
415
416
417
418
419
420
421
422
423
	return undef;
    }   
    return $ticket;
}

# read one_time_ticket from table and remove it
#
sub get_one_time_ticket {
    my $ticket_number = shift;
    my $addr = shift; 
    
sikeda's avatar
sikeda committed
424
    Log::do_log('debug2', '(%s)',$ticket_number);
425
426
427
    
    my $sth;
    
sikeda's avatar
sikeda committed
428
429
    unless ($sth = SDM::do_query("SELECT ticket_one_time_ticket AS ticket, robot_one_time_ticket AS robot, email_one_time_ticket AS email, date_one_time_ticket AS \"date\", data_one_time_ticket AS data, remote_addr_one_time_ticket AS remote_addr, status_one_time_ticket as status FROM one_time_ticket_table WHERE ticket_one_time_ticket = %s ", SDM::quote($ticket_number))) {
	Log::do_log('err','Unable to retrieve one time ticket %s from database',$ticket_number);
430
431
432
	return {'result'=>'error'};
    }
 
433
    my $ticket = $sth->fetchrow_hashref('NAME_lc');
434
435
    
    unless ($ticket) {	
sikeda's avatar
sikeda committed
436
	Log::do_log('info','Auth::get_one_time_ticket: Unable to find one time ticket %s', $ticket);
437
438
439
440
	return {'result'=>'not_found'};
    }
    
    my $result;
441
442
443
    my $printable_date = POSIX::strftime(
	"%d %b %Y at %H:%M:%S", localtime($ticket->{'date'})
    );
444
445
446

    if ($ticket->{'status'} ne 'open') {
	$result = 'closed';
sikeda's avatar
sikeda committed
447
	Log::do_log('info','Auth::get_one_time_ticket: ticket %s from %s has been used before (%s)',$ticket_number,$ticket->{'email'},$printable_date);
448
449
    }
    elsif (time - $ticket->{'date'} > 48 * 60 * 60) {
sikeda's avatar
sikeda committed
450
	Log::do_log('info','Auth::get_one_time_ticket: ticket %s from %s refused because expired (%s)',$ticket_number,$ticket->{'email'},$printable_date);
451
452
453
454
	$result = 'expired';
    }else{
	$result = 'success';
    }
sikeda's avatar
sikeda committed
455
456
    unless (SDM::do_query("UPDATE one_time_ticket_table SET status_one_time_ticket = %s WHERE (ticket_one_time_ticket=%s)", SDM::quote($addr), SDM::quote($ticket_number))) {
    	Log::do_log('err','Unable to set one time ticket %s status to %s',$ticket_number, $addr);
457
458
    }

sikeda's avatar
sikeda committed
459
    Log::do_log('info', 'Auth::get_one_time_ticket(%s) : result : %s',$ticket_number,$result);
460
461
462
463
464
465
466
467
468
469
    return {'result'=>$result,
	    'date'=>$ticket->{'date'},
	    'email'=>$ticket->{'email'},
	    'remote_addr'=>$ticket->{'remote_addr'},
	    'robot'=>$ticket->{'robot'},
	    'data'=>$ticket->{'data'},
	    'status'=>$ticket->{'status'}
	};
}
    
salaun's avatar
salaun committed
470
1;