Auth.pm 19.6 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::Log;
36
37
use Sympa::Report;
use Sympa::Robot;
38
use SDM;
39
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
143
144
145
146

    if ($user->{'wrong_login_count'} >
        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
197
    Sympa::User::update_global_user($email,
        {wrong_login_count => $user->{'wrong_login_count'} + 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
232
233
234
235
236
237
238
239
240
        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);

    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
241
    my $db = Sympa::Database->new('LDAP', %$ldap);
242

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

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

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

    my $refhash = $mesg->as_struct();
    my (@DN) = keys(%$refhash);
267
    $db->disconnect();
268
269
270

    ##  bind with the DN and the pwd

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

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

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

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

    ## To get the value of the canonic email and the alternative email
    my (@canonic_email, @alternative);

303
    my $param = Sympa::Tools::Data::dup_var($ldap);
304
305
306
307
308
309
310
311
312
    ## 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);
313
314
    #FIXME: alloptions would be used.
    @canonic_email = $entry->get_value($attrs);
315
316
317
318
319
320
    foreach my $email (@canonic_email) {
        my $e = lc($email);
        $param->{'alt_emails'}{$e} = 'ldap' if ($e);
    }

    foreach my $attribute_value (@alternative_conf) {
321
322
        #FIXME: alloptions would be used.
        @alternative = $entry->get_value($attribute_value);
323
324
325
326
327
328
329
330
331
332
333
        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};
    }

334
335
    $db->disconnect() or $log->syslog('notice', 'Unable to unbind');
    $log->syslog('debug3', 'Canonic: %s', $canonic_email[0]);
336
337
338
339
340
341
342
343
344
345
    ## 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)
        && !Conf::get_robot_conf($robot, 'ldap_force_canonical_email')) {
        return ($auth);
    } else {
        return lc($canonic_email[0]);
    }
}
salaun's avatar
salaun committed
346
347

# fetch user email using his cas net_id and the paragrapah number in auth.conf
348
# NOTE: This might be moved to Robot package.
349
sub get_email_by_net_id {
350
351
352

    my $robot      = shift;
    my $auth_id    = shift;
353
    my $attributes = shift;
354

355
    $log->syslog('debug', '(%s, %s)', $auth_id, $attributes->{'uid'});
356
357
358
359
360
361
362
363
364

    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 =
365
            Sympa::Robot::get_netidtoemail_db($robot, $netid_cookie,
366
367
368
            $Conf::Conf{'auth_services'}{$robot}[$auth_id]{'service_id'});

        return $email;
369
    }
370

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

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

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

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

384
    # my @alternative_conf = split(/,/,$ldap->{'alternative_email_attribute'});
385

386
387
    my $mesg = $db->do_operation(
        'search',
388
        base    => $ldap->{'suffix'},
389
        filter  => $filter,
390
391
392
        scope   => $ldap->{'scope'},
        timeout => $ldap->{'timeout'},
        attrs   => [$ldap->{'email_attribute'}],
393
394
    );

395
    unless ($mesg and $mesg->count()) {
396
        $log->syslog('notice', "No entry in the LDAP Directory Tree of %s",
397
            $ldap->{'host'});
398
        $db->disconnect();
399
400
        return undef;
    }
salaun's avatar
salaun committed
401

402
    $db->disconnect();
403

404
    ## return only the first attribute
405
    my @results = $mesg->entries;
406
    foreach my $result (@results) {
407
        return (lc($result->get_value($ldap->{'email_attribute'})));
408
    }
salaun's avatar
salaun committed
409

410
}
salaun's avatar
salaun committed
411

412
413
# check trusted_application_name et trusted_application_password : return 1 or
# undef;
414
sub remote_app_check_password {
415
    my ($trusted_application_name, $password, $robot, $service) = @_;
416
    $log->syslog('debug', '(%s, %s, %s)', $trusted_application_name, $robot,
417
        $service);
418

419
    my $md5 = Digest::MD5::md5_hex($password);
420

421
    # seach entry for trusted_application in Conf
422
423
    my @trusted_apps;

424
    # select trusted_apps from robot context or sympa context
425
426
427
428
429
430
    @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'}) {
431
                # $log->syslog('debug', 'Authentication succeed for %s',$application->{'name'});
432
                my %proxy_for_vars;
433
                my %set_vars;
434
435
436
437
                foreach my $varname (@{$application->{'proxy_for_variables'}})
                {
                    $proxy_for_vars{$varname} = 1;
                }
438
439
440
441
442
443
444
445
446
                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;
                    }
447
                    $log->syslog(
448
449
450
451
452
453
                        'info',   'Illegal command %s received from %s',
                        $service, $trusted_application_name
                    );
                    return;
                }
                return (\%proxy_for_vars, \%set_vars);
454
            } else {
455
                $log->syslog('info', 'Bad password from %s',
456
                    $trusted_application_name);
457
                return;
458
459
460
            }
        }
    }
461
    # no matching application found
462
    $log->syslog('info', 'Unknown application name %s',
463
        $trusted_application_name);
464
    return;
465
}
466

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

476
    my $ticket = Sympa::Session::get_random();
477
    #$log->syslog('info', '(%s, %s, %s, %s) Value = %s',
478
    #    $email, $robot, $data_string, $remote_addr, $ticket);
479
480
481

    my $date = time;
    my $sth;
482
483

    unless (
484
485
486
487
488
489
490
491
492
        SDM::do_prepared_query(
            q{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 (?, ?, ?, ?, ?, ?, ?)},
            $ticket, $robot,
            $email,       time, $data_string,
            $remote_addr, 'open'
493
494
        )
        ) {
495
        $log->syslog(
496
497
498
499
500
501
502
            'err',
            'Unable to insert new one time ticket for user %s, robot %s in the database',
            $email,
            $robot
        );
        return undef;
    }
503
504
505
506
507
    return $ticket;
}

# read one_time_ticket from table and remove it
sub get_one_time_ticket {
508
    $log->syslog('debug2', '(%s, %s, %s)', @_);
509
    my $robot         = shift;
510
    my $ticket_number = shift;
511
512
    my $addr          = shift;

513
    my $sth;
514
515

    unless (
516
517
518
519
520
521
522
523
524
525
526
        $sth = SDM::do_prepared_query(
            q{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 = ? AND robot_one_time_ticket = ?},
            $ticket_number, $robot
527
528
        )
        ) {
529
        $log->syslog('err',
530
531
532
            'Unable to retrieve one time ticket %s from database',
            $ticket_number);
        return {'result' => 'error'};
533
    }
534

535
    my $ticket = $sth->fetchrow_hashref('NAME_lc');
536
    $sth->finish;
537
538

    unless ($ticket) {
539
        $log->syslog('info', 'Unable to find one time ticket %s', $ticket);
540
        return {'result' => 'not_found'};
541
    }
542

543
    my $result;
544
545
    my $printable_date =
        POSIX::strftime("%d %b %Y at %H:%M:%S", localtime($ticket->{'date'}));
546
547
548
    my $lockout = Conf::get_robot_conf($robot, 'one_time_ticket_lockout')
        || 'open';
    my $lifetime =
549
        Sympa::Tools::Time::duration_conv(
550
        Conf::get_robot_conf($robot, 'one_time_ticket_lifetime') || 0);
551

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

    if ($result eq 'success') {
        unless (
            $sth = SDM::do_prepared_query(
                q{UPDATE one_time_ticket_table
sikeda's avatar
sikeda committed
575
                  SET status_one_time_ticket = ?
576
577
578
579
580
                  WHERE ticket_one_time_ticket = ? AND
                        robot_one_time_ticket = ?},
                $addr, $ticket_number, $robot
            )
            ) {
581
            $log->syslog('err',
582
583
584
585
                'Unable to set one time ticket %s status to %s',
                $ticket_number, $addr);
        } elsif (!$sth->rows) {
            # ticket may be removed by task.
586
            $log->syslog('info', 'Unable to find one time ticket %s',
587
588
589
                $ticket_number);
            return {'result' => 'not_found'};
        }
590
591
    }

592
    $log->syslog('info', 'Ticket: %s; Result: %s', $ticket_number, $result);
593
594
595
596
597
    return {
        'result'      => $result,
        'date'        => $ticket->{'date'},
        'email'       => $ticket->{'email'},
        'remote_addr' => $ticket->{'remote_addr'},
598
        'robot'       => $robot,
599
600
601
        'data'        => $ticket->{'data'},
        'status'      => $ticket->{'status'}
    };
602
}
603

salaun's avatar
salaun committed
604
1;