Auth.pm 19.8 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
#
# 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
10
# Copyright (c) 2011, 2012, 2013, 2014, 2015 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
package Sympa::Auth;
salaun's avatar
salaun committed
26

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

32
use Sympa;
salaun's avatar
salaun committed
33
use Conf;
34
use Sympa::Database;
35
use Sympa::DatabaseManager;
36
use Sympa::Log;
37
38
39
use Sympa::Report;
use Sympa::Robot;
use Sympa::Session;
40
use tools;
41
42
use Sympa::Tools::Data;
use Sympa::Tools::Time;
43
use Sympa::User;
salaun's avatar
salaun committed
44

45
46
my $log = Sympa::Log->instance;

47
48
49
## return the password finger print (this proc allow futur replacement of md5
## by sha1 or ....)
sub password_fingerprint {
50

51
    $log->syslog('debug', '');
52
53

    my $pwd = shift;
54
    if (Conf::get_robot_conf('*', 'password_case') eq 'insensitive') {
55
        return Digest::MD5::md5_hex(lc($pwd));
56
    } else {
57
        return Digest::MD5::md5_hex($pwd);
58
    }
59
60
}

61
## authentication : via email or uid
62
63
64
65
sub check_auth {
    my $robot = shift;
    my $auth  = shift;    ## User email or UID
    my $pwd   = shift;    ## Password
66
    $log->syslog('debug', '(%s)', $auth);
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

    my ($canonic, $user);

    if (tools::valid_email($auth)) {
        return authentication($robot, $auth, $pwd);
    } else {
        ## This is an UID
        foreach my $ldap (@{$Conf::Conf{'auth_services'}{$robot}}) {
            # only ldap service are to be applied here
            next unless ($ldap->{'auth_type'} eq 'ldap');

            $canonic =
                ldap_authentication($robot, $ldap, $auth, $pwd, 'uid_filter');
            last if ($canonic);    ## Stop at first match
        }
        if ($canonic) {

            unless ($user = Sympa::User::get_global_user($canonic)) {
                $user = {'email' => $canonic};
            }
            return {
                'user'       => $user,
                'auth'       => 'ldap',
                'alt_emails' => {$canonic => 'ldap'}
            };

        } else {
94
            Sympa::Report::reject_report_web('user', 'incorrect_passwd', {})
95
                unless ($ENV{'SYMPA_SOAP'});
96
            $log->syslog('err', "Incorrect LDAP password");
97
98
99
100
            return undef;
        }
    }
}
salaun's avatar
salaun committed
101

102
## This subroutine if Sympa may use its native authentication for a given user
103
104
## It might not if no user_table paragraph is found in auth.conf or if the
## regexp or
105
106
107
108
109
110
111
112
## 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
113
114
115
116
117
118
119
120
121
122
123
124
    foreach my $auth_service (@{$Conf::Conf{'auth_services'}{$robot}}) {
        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;
125
    }
126

127
128
    return $ok;
}
salaun's avatar
salaun committed
129
130

sub authentication {
131
132
    my ($robot, $email, $pwd) = @_;
    my ($user, $canonic);
133
    $log->syslog('debug', '(%s)', $email);
salaun's avatar
salaun committed
134

135
    unless ($user = Sympa::User::get_global_user($email)) {
136
137
        $user = {'email' => $email};
    }
salaun's avatar
salaun committed
138
    unless ($user->{'password'}) {
139
        $user->{'password'} = '';
salaun's avatar
salaun committed
140
    }
141

142
    if (($user->{'wrong_login_count'} || 0) >
143
144
145
146
        Conf::get_robot_conf($robot, 'max_wrong_password')) {
        # too many wrong login attemp
        Sympa::User::update_global_user($email,
            {wrong_login_count => $user->{'wrong_login_count'} + 1});
147
        Sympa::Report::reject_report_web('user', 'too_many_wrong_login', {})
148
            unless ($ENV{'SYMPA_SOAP'});
149
        $log->syslog('err',
150
            'Login is blocked: too many wrong password submission for %s',
151
152
            $email);
        return undef;
153
    }
154
155
156
157
    foreach my $auth_service (@{$Conf::Conf{'auth_services'}{$robot}}) {
        next if ($auth_service->{'auth_type'} eq 'authentication_info_url');
        next if ($email !~ /$auth_service->{'regexp'}/i);
        next
158
159
            if $auth_service->{'negative_regexp'}
                and $email =~ /$auth_service->{'negative_regexp'}/i;
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192

        ## Only 'user_table' and 'ldap' backends will need that Sympa collects
        ## the user passwords
        ## Other backends are Single Sign-On solutions
        if ($auth_service->{'auth_type'} eq 'user_table') {
            my $fingerprint = password_fingerprint($pwd);

            if ($fingerprint eq $user->{'password'}) {
                Sympa::User::update_global_user($email,
                    {wrong_login_count => 0});
                return {
                    'user'       => $user,
                    'auth'       => 'classic',
                    'alt_emails' => {$email => 'classic'}
                };
            }
        } elsif ($auth_service->{'auth_type'} eq 'ldap') {
            if ($canonic = ldap_authentication(
                    $robot, $auth_service, $email, $pwd, 'email_filter'
                )
                ) {
                unless ($user = Sympa::User::get_global_user($canonic)) {
                    $user = {'email' => $canonic};
                }
                Sympa::User::update_global_user($canonic,
                    {wrong_login_count => 0});
                return {
                    'user'       => $user,
                    'auth'       => 'ldap',
                    'alt_emails' => {$email => 'ldap'}
                };
            }
        }
salaun's avatar
salaun committed
193
    }
salaun's avatar
salaun committed
194

195
    # increment wrong login count.
196
    Sympa::User::update_global_user($email,
197
        {wrong_login_count => ($user->{'wrong_login_count'} || 0) + 1});
198

199
    Sympa::Report::reject_report_web('user', 'incorrect_passwd', {})
200
        unless $ENV{'SYMPA_SOAP'};
201
    $log->syslog('err', 'Incorrect password for user %s', $email);
salaun's avatar
salaun committed
202

203
204
    my $param;    #FIXME FIXME: not used.
    $param->{'init_email'}         = $email;
sikeda's avatar
sikeda committed
205
    $param->{'escaped_init_email'} = tools::escape_chars($email);
salaun's avatar
salaun committed
206
207
208
209
    return undef;
}

sub ldap_authentication {
210
    my ($robot, $ldap, $auth, $pwd, $whichfilter) = @_;
211
    my $mesg;
212
213
    $log->syslog('debug2', '(%s, %s, %s)', $auth, '****', $whichfilter);
    $log->syslog('debug3', 'Password used: %s', $pwd);
214

215
    unless (Sympa::search_fullpath($robot, 'auth.conf')) {
216
217
218
219
220
        return undef;
    }

    ## No LDAP entry is defined in auth.conf
    if ($#{$Conf::Conf{'auth_services'}{$robot}} < 0) {
221
        $log->syslog('notice', 'Skipping empty auth.conf');
222
223
224
225
226
227
228
229
230
231
        return undef;
    }

    # 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);

232
233
234
235
236
237
238
239
240
    my @alt_attrs =
        split /\s*,\s*/, ($ldap->{'alternative_email_attribute'} || '');
    my $attr = $ldap->{'email_attribute'};
    my $filter;
    if ($whichfilter eq 'uid_filter') {
        $filter = $ldap->{'get_dn_by_uid_filter'};
    } elsif ($whichfilter eq 'email_filter') {
        $filter = $ldap->{'get_dn_by_email_filter'};
    }
241
242
243
    $filter =~ s/\[sender\]/$auth/ig;

    ## bind in order to have the user's DN
244
    my $db = Sympa::Database->new('LDAP', %$ldap);
245

246
    unless ($db and $db->connect()) {
247
        $log->syslog('err', 'Unable to connect to the LDAP server "%s"',
248
249
250
251
            $ldap->{'host'});
        return undef;
    }

252
253
    $mesg = $db->do_operation(
        'search',
254
255
256
257
258
259
        base    => $ldap->{'suffix'},
        filter  => "$filter",
        scope   => $ldap->{'scope'},
        timeout => $ldap->{'timeout'}
    );

260
    unless ($mesg and $mesg->count()) {
261
        $log->syslog('notice',
262
263
            'No entry in the LDAP Directory Tree of %s for %s',
            $ldap->{'host'}, $auth);
264
        $db->disconnect();
265
266
267
268
269
        return undef;
    }

    my $refhash = $mesg->as_struct();
    my (@DN) = keys(%$refhash);
270
    $db->disconnect();
271
272
273

    ##  bind with the DN and the pwd

274
    # Then set the bind_dn and password according to the current user
275
276
277
278
279
    $db = Sympa::Database->new(
        'LDAP',
        %$ldap,
        bind_dn       => $DN[0],
        bind_password => $pwd,
280
    );
salaun's avatar
salaun committed
281

282
    unless ($db and $db->connect()) {
283
        $log->syslog('err', 'Unable to connect to the LDAP server "%s"',
284
            $ldap->{'host'});
285
286
287
        return undef;
    }

288
289
    $mesg = $db->do_operation(
        'search',
290
291
292
293
294
295
        base    => $ldap->{'suffix'},
        filter  => "$filter",
        scope   => $ldap->{'scope'},
        timeout => $ldap->{'timeout'}
    );

296
    unless ($mesg and $mesg->count()) {
297
        $log->syslog('notice', "No entry in the LDAP Directory Tree of %s",
298
            $ldap->{'host'});
299
        $db->disconnect();
300
301
302
303
        return undef;
    }

    ## To get the value of the canonic email and the alternative email
304
    my (@emails, @alt_emails);
305

306
    #FIXME FIXME: After all, $param->{'alt_emails'} is never used!
307
    my $param = Sympa::Tools::Data::dup_var($ldap);
308
309
310
311
312
313
314
315
316
317
    ## 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);

318
319
320
321
322
323
324
325
326
327
328
329
330
    my $values = $entry->get_value($attr, alloptions => 1);
    @emails =
        map { lc $_ }
        grep {$_} map { @{$values->{$_}} } sort keys %{$values || {}};

    @alt_emails = map {
        my $values = $entry->get_value($_, alloptions => 1);
        map { lc $_ }
            grep {$_} map { @{$values->{$_}} } sort keys %{$values || {}};
    } @alt_attrs;

    foreach my $email (@emails, @alt_emails) {
        $param->{'alt_emails'}{$email} = 'ldap';
331
332
333
334
335
336
337
    }

    ## Restore previous emails
    foreach my $alt (keys %{$previous}) {
        $param->{'alt_emails'}{$alt} = $previous->{$alt};
    }

338
    $db->disconnect() or $log->syslog('notice', 'Unable to unbind');
339
    $log->syslog('debug3', 'Canonic: %s', $emails[0]);
340
341
342
343
    ## If the identifier provided was a valid email, return the provided
    ## email.
    ## Otherwise, return the canonical email guessed after the login.
    if (tools::valid_email($auth)
344
345
        and not Conf::get_robot_conf($robot, 'ldap_force_canonical_email')) {
        return $auth;
346
    } else {
347
        return $emails[0];
348
349
    }
}
salaun's avatar
salaun committed
350
351

# fetch user email using his cas net_id and the paragrapah number in auth.conf
352
# NOTE: This might be moved to Robot package.
353
sub get_email_by_net_id {
354
355
356

    my $robot      = shift;
    my $auth_id    = shift;
357
    my $attributes = shift;
358

359
    $log->syslog('debug', '(%s, %s)', $auth_id, $attributes->{'uid'});
360
361
362
363
364
365
366
367
368

    if (defined $Conf::Conf{'auth_services'}{$robot}[$auth_id]
        {'internal_email_by_netid'}) {
        my $sso_config   = @{$Conf::Conf{'auth_services'}{$robot}}[$auth_id];
        my $netid_cookie = $sso_config->{'netid_http_header'};

        $netid_cookie =~ s/(\w+)/$attributes->{$1}/ig;

        my $email =
369
            Sympa::Robot::get_netidtoemail_db($robot, $netid_cookie,
370
371
372
            $Conf::Conf{'auth_services'}{$robot}[$auth_id]{'service_id'});

        return $email;
373
    }
374

375
    my $ldap = $Conf::Conf{'auth_services'}{$robot}->[$auth_id];
376

377
    my $db = Sympa::Database->new('LDAP', %$ldap);
378

379
    unless ($db and $db->connect()) {
380
        $log->syslog('err', 'Unable to connect to the LDAP server "%s"',
381
            $ldap->{'host'});
382
        return undef;
salaun's avatar
salaun committed
383
384
    }

385
    my $filter = $ldap->{'get_email_by_uid_filter'};
386
    $filter =~ s/\[([\w-]+)\]/$attributes->{$1}/ig;
salaun's avatar
salaun committed
387

388
389
    # my @alt_attrs =
    #     split /\s*,\s*/, $ldap->{'alternative_email_attribute'} || '';
390

391
392
    my $mesg = $db->do_operation(
        'search',
393
        base    => $ldap->{'suffix'},
394
        filter  => $filter,
395
396
397
        scope   => $ldap->{'scope'},
        timeout => $ldap->{'timeout'},
        attrs   => [$ldap->{'email_attribute'}],
398
399
    );

400
    unless ($mesg and $mesg->count()) {
401
        $log->syslog('notice', "No entry in the LDAP Directory Tree of %s",
402
            $ldap->{'host'});
403
        $db->disconnect();
404
405
        return undef;
    }
salaun's avatar
salaun committed
406

407
    $db->disconnect();
408

409
    ## return only the first attribute
410
    my @results = $mesg->entries;
411
    foreach my $result (@results) {
412
        return (lc($result->get_value($ldap->{'email_attribute'})));
413
    }
salaun's avatar
salaun committed
414

415
}
salaun's avatar
salaun committed
416

417
418
# check trusted_application_name et trusted_application_password : return 1 or
# undef;
419
sub remote_app_check_password {
420
    my ($trusted_application_name, $password, $robot, $service) = @_;
421
    $log->syslog('debug', '(%s, %s, %s)', $trusted_application_name, $robot,
422
        $service);
423

424
    my $md5 = Digest::MD5::md5_hex($password);
425

426
    # seach entry for trusted_application in Conf
427
428
    my @trusted_apps;

429
    # select trusted_apps from robot context or sympa context
430
431
432
433
434
435
    @trusted_apps = @{Conf::get_robot_conf($robot, 'trusted_applications')};

    foreach my $application (@trusted_apps) {

        if (lc($application->{'name'}) eq lc($trusted_application_name)) {
            if ($md5 eq $application->{'md5password'}) {
436
                # $log->syslog('debug', 'Authentication succeed for %s',$application->{'name'});
437
                my %proxy_for_vars;
438
                my %set_vars;
439
440
441
442
                foreach my $varname (@{$application->{'proxy_for_variables'}})
                {
                    $proxy_for_vars{$varname} = 1;
                }
443
444
445
446
447
448
449
450
451
                foreach my $varname (@{$application->{'set_variables'}}) {
                    $set_vars{$1} = $2 if $varname =~ /(\S+)=(.*)/;
                }
                if ($application->{'allow_commands'}) {
                    foreach my $cmdname (@{$application->{'allow_commands'}})
                    {
                        return (\%proxy_for_vars, \%set_vars)
                            if $cmdname eq $service;
                    }
452
                    $log->syslog(
453
454
455
456
457
458
                        'info',   'Illegal command %s received from %s',
                        $service, $trusted_application_name
                    );
                    return;
                }
                return (\%proxy_for_vars, \%set_vars);
459
            } else {
460
                $log->syslog('info', 'Bad password from %s',
461
                    $trusted_application_name);
462
                return;
463
464
465
            }
        }
    }
466
    # no matching application found
467
    $log->syslog('info', 'Unknown application name %s',
468
        $trusted_application_name);
469
    return;
470
}
471

472
473
# create new entry in one_time_ticket table using a rand as id so later
# access is authenticated
474
sub create_one_time_ticket {
475
476
    my $email       = shift;
    my $robot       = shift;
477
    my $data_string = shift;
478
479
    my $remote_addr = shift;
    ## Value may be 'mail' if the IP address is not known
480

481
    my $ticket = Sympa::Session::get_random();
482
    #$log->syslog('info', '(%s, %s, %s, %s) Value = %s',
483
    #    $email, $robot, $data_string, $remote_addr, $ticket);
484
485

    my $date = time;
486

487
    my $sdm = Sympa::DatabaseManager->instance;
488
    unless (
489
490
        $sdm
        and $sdm->do_prepared_query(
491
            q{INSERT INTO one_time_ticket_table
492
493
494
495
496
              (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 (?, ?, ?, ?, ?, ?, ?)},
497
            $ticket, $robot,
498
499
            $email,  time,
            $data_string,
500
            $remote_addr, 'open'
501
502
        )
        ) {
503
        $log->syslog(
504
505
506
507
508
509
510
            'err',
            'Unable to insert new one time ticket for user %s, robot %s in the database',
            $email,
            $robot
        );
        return undef;
    }
511
512
513
514
515
    return $ticket;
}

# read one_time_ticket from table and remove it
sub get_one_time_ticket {
516
    $log->syslog('debug2', '(%s, %s, %s)', @_);
517
    my $robot         = shift;
518
    my $ticket_number = shift;
519
520
    my $addr          = shift;

521
    my $sth;
522
    my $sdm = Sympa::DatabaseManager->instance;
523
    unless (
524
525
        $sdm
        and $sth = $sdm->do_prepared_query(
526
            q{SELECT ticket_one_time_ticket AS ticket,
527
528
529
530
531
532
533
534
                     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 = ? AND robot_one_time_ticket = ?},
535
            $ticket_number, $robot
536
537
        )
        ) {
538
        $log->syslog('err',
539
540
541
            'Unable to retrieve one time ticket %s from database',
            $ticket_number);
        return {'result' => 'error'};
542
    }
543

544
    my $ticket = $sth->fetchrow_hashref('NAME_lc');
545
    $sth->finish;
546
547

    unless ($ticket) {
548
        $log->syslog('info', 'Unable to find one time ticket %s', $ticket);
549
        return {'result' => 'not_found'};
550
    }
551

552
    my $result;
553
554
    my $printable_date =
        POSIX::strftime("%d %b %Y at %H:%M:%S", localtime($ticket->{'date'}));
555
556
557
    my $lockout = Conf::get_robot_conf($robot, 'one_time_ticket_lockout')
        || 'open';
    my $lifetime =
558
        Sympa::Tools::Time::duration_conv(
559
        Conf::get_robot_conf($robot, 'one_time_ticket_lifetime') || 0);
560

561
    if ($lockout eq 'one_time' and $ticket->{'status'} ne 'open') {
562
        $result = 'closed';
563
        $log->syslog('info', 'Ticket %s from %s has been used before (%s)',
564
            $ticket_number, $ticket->{'email'}, $printable_date);
565
566
567
568
    } elsif ($lockout eq 'remote_addr'
        and $ticket->{'status'} ne $addr
        and $ticket->{'status'} ne 'open') {
        $result = 'closed';
569
        $log->syslog('info',
570
571
572
            'ticket %s from %s refused because accessed by the other (%s)',
            $ticket_number, $ticket->{'email'}, $printable_date);
    } elsif ($lifetime and $ticket->{'date'} + $lifetime < time) {
573
        $log->syslog('info', 'Ticket %s from %s refused because expired (%s)',
574
            $ticket_number, $ticket->{'email'}, $printable_date);
575
576
577
        $result = 'expired';
    } else {
        $result = 'success';
578
    }
579
580
581

    if ($result eq 'success') {
        unless (
582
            $sth = $sdm->do_prepared_query(
583
                q{UPDATE one_time_ticket_table
sikeda's avatar
sikeda committed
584
                  SET status_one_time_ticket = ?
585
586
587
588
589
                  WHERE ticket_one_time_ticket = ? AND
                        robot_one_time_ticket = ?},
                $addr, $ticket_number, $robot
            )
            ) {
590
            $log->syslog('err',
591
592
593
594
                'Unable to set one time ticket %s status to %s',
                $ticket_number, $addr);
        } elsif (!$sth->rows) {
            # ticket may be removed by task.
595
            $log->syslog('info', 'Unable to find one time ticket %s',
596
597
598
                $ticket_number);
            return {'result' => 'not_found'};
        }
599
600
    }

601
    $log->syslog('info', 'Ticket: %s; Result: %s', $ticket_number, $result);
602
603
604
605
606
    return {
        'result'      => $result,
        'date'        => $ticket->{'date'},
        'email'       => $ticket->{'email'},
        'remote_addr' => $ticket->{'remote_addr'},
607
        'robot'       => $robot,
608
609
610
        'data'        => $ticket->{'data'},
        'status'      => $ticket->{'status'}
    };
611
}
612

salaun's avatar
salaun committed
613
1;