Auth.pm 14.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, 2016, 2017 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;
salaun's avatar
salaun committed
30

31
use Sympa;
salaun's avatar
salaun committed
32
use Conf;
33
use Sympa::Database;
34
use Sympa::Log;
35
36
use Sympa::Report;
use Sympa::Robot;
37
use Sympa::Tools::Data;
38
use Sympa::Tools::Text;
39
use Sympa::User;
salaun's avatar
salaun committed
40

41
42
my $log = Sympa::Log->instance;

43
44
# Moved to: Sympa::User::password_fingerprint().
#sub password_fingerprint;
45

46
## authentication : via email or uid
47
48
49
50
sub check_auth {
    my $robot = shift;
    my $auth  = shift;    ## User email or UID
    my $pwd   = shift;    ## Password
51
    $log->syslog('debug', '(%s)', $auth);
52
53
54

    my ($canonic, $user);

55
    if (Sympa::Tools::Text::valid_email($auth)) {
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
        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 {
79
            Sympa::Report::reject_report_web('user', 'incorrect_passwd', {})
80
                unless ($ENV{'SYMPA_SOAP'});
81
            $log->syslog('err', "Incorrect LDAP password");
82
83
84
85
            return undef;
        }
    }
}
salaun's avatar
salaun committed
86

87
## This subroutine if Sympa may use its native authentication for a given user
88
89
## It might not if no user_table paragraph is found in auth.conf or if the
## regexp or
90
91
92
93
94
95
96
97
## 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
98
99
100
101
102
103
104
105
106
107
108
109
    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;
110
    }
111

112
113
    return $ok;
}
salaun's avatar
salaun committed
114
115

sub authentication {
116
117
    my ($robot, $email, $pwd) = @_;
    my ($user, $canonic);
118
    $log->syslog('debug', '(%s)', $email);
salaun's avatar
salaun committed
119

120
    unless ($user = Sympa::User::get_global_user($email)) {
121
122
        $user = {'email' => $email};
    }
salaun's avatar
salaun committed
123
    unless ($user->{'password'}) {
124
        $user->{'password'} = '';
salaun's avatar
salaun committed
125
    }
126

127
    if (($user->{'wrong_login_count'} || 0) >
128
129
130
131
        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});
132
        Sympa::Report::reject_report_web('user', 'too_many_wrong_login', {})
133
            unless ($ENV{'SYMPA_SOAP'});
134
        $log->syslog('err',
135
            'Login is blocked: too many wrong password submission for %s',
136
137
            $email);
        return undef;
138
    }
139
140
141
142
    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
143
            if $auth_service->{'negative_regexp'}
144
            and $email =~ /$auth_service->{'negative_regexp'}/i;
145
146
147
148
149

        ## 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') {
150
151
152
            # supply old password hash in case the hash uses a salt
            my $fingerprint =
                Sympa::User::password_fingerprint($pwd, $user->{'password'});
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179

            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
180
    }
salaun's avatar
salaun committed
181

182
    # increment wrong login count.
183
    Sympa::User::update_global_user($email,
184
        {wrong_login_count => ($user->{'wrong_login_count'} || 0) + 1});
185

186
    Sympa::Report::reject_report_web('user', 'incorrect_passwd', {})
187
        unless $ENV{'SYMPA_SOAP'};
188
    $log->syslog('err', 'Incorrect password for user %s', $email);
salaun's avatar
salaun committed
189

190
    my $param;    #FIXME FIXME: not used.
191
    $param->{'init_email'} = $email;
salaun's avatar
salaun committed
192
193
194
195
    return undef;
}

sub ldap_authentication {
196
    my ($robot, $ldap, $auth, $pwd, $whichfilter) = @_;
197
    my $mesg;
198
199
    $log->syslog('debug2', '(%s, %s, %s)', $auth, '****', $whichfilter);
    $log->syslog('debug3', 'Password used: %s', $pwd);
200

201
    unless (Sympa::search_fullpath($robot, 'auth.conf')) {
202
203
204
205
206
        return undef;
    }

    ## No LDAP entry is defined in auth.conf
    if ($#{$Conf::Conf{'auth_services'}{$robot}} < 0) {
207
        $log->syslog('notice', 'Skipping empty auth.conf');
208
209
210
211
212
213
214
215
216
217
        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);

218
219
220
221
222
223
224
225
226
    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'};
    }
227
228
229
    $filter =~ s/\[sender\]/$auth/ig;

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

232
    unless ($db and $db->connect()) {
233
        $log->syslog('err', 'Unable to connect to the LDAP server "%s"',
234
235
236
237
            $ldap->{'host'});
        return undef;
    }

238
239
    $mesg = $db->do_operation(
        'search',
240
241
242
243
244
245
        base    => $ldap->{'suffix'},
        filter  => "$filter",
        scope   => $ldap->{'scope'},
        timeout => $ldap->{'timeout'}
    );

246
    unless ($mesg and $mesg->count()) {
247
        $log->syslog('notice',
248
249
            'No entry in the LDAP Directory Tree of %s for %s',
            $ldap->{'host'}, $auth);
250
        $db->disconnect();
251
252
253
254
255
        return undef;
    }

    my $refhash = $mesg->as_struct();
    my (@DN) = keys(%$refhash);
256
    $db->disconnect();
257
258
259

    ##  bind with the DN and the pwd

260
    # Then set the bind_dn and password according to the current user
261
262
263
264
265
    $db = Sympa::Database->new(
        'LDAP',
        %$ldap,
        bind_dn       => $DN[0],
        bind_password => $pwd,
266
    );
salaun's avatar
salaun committed
267

268
    unless ($db and $db->connect()) {
269
        $log->syslog('err', 'Unable to connect to the LDAP server "%s"',
270
            $ldap->{'host'});
271
272
273
        return undef;
    }

274
275
    $mesg = $db->do_operation(
        'search',
276
277
278
279
280
281
        base    => $ldap->{'suffix'},
        filter  => "$filter",
        scope   => $ldap->{'scope'},
        timeout => $ldap->{'timeout'}
    );

282
    unless ($mesg and $mesg->count()) {
283
        $log->syslog('notice', "No entry in the LDAP Directory Tree of %s",
284
            $ldap->{'host'});
285
        $db->disconnect();
286
287
288
289
        return undef;
    }

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

292
    #FIXME FIXME: After all, $param->{'alt_emails'} is never used!
293
    my $param = Sympa::Tools::Data::dup_var($ldap);
294
295
296
297
298
299
300
301
302
303
    ## 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);

304
305
306
307
308
309
310
311
312
313
314
315
316
    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';
317
318
319
320
321
322
323
    }

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

324
    $db->disconnect() or $log->syslog('notice', 'Unable to unbind');
325
    $log->syslog('debug3', 'Canonic: %s', $emails[0]);
326
327
328
    ## If the identifier provided was a valid email, return the provided
    ## email.
    ## Otherwise, return the canonical email guessed after the login.
329
    if (Sympa::Tools::Text::valid_email($auth)
330
331
        and not Conf::get_robot_conf($robot, 'ldap_force_canonical_email')) {
        return $auth;
332
    } else {
333
        return $emails[0];
334
335
    }
}
salaun's avatar
salaun committed
336
337

# fetch user email using his cas net_id and the paragrapah number in auth.conf
338
# NOTE: This might be moved to Robot package.
339
sub get_email_by_net_id {
340
341
342

    my $robot      = shift;
    my $auth_id    = shift;
343
    my $attributes = shift;
344

345
    $log->syslog('debug', '(%s, %s)', $auth_id, $attributes->{'uid'});
346
347
348
349
350
351
352
353
354

    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 =
355
            Sympa::Robot::get_netidtoemail_db($robot, $netid_cookie,
356
357
358
            $Conf::Conf{'auth_services'}{$robot}[$auth_id]{'service_id'});

        return $email;
359
    }
360

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

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

365
    unless ($db and $db->connect()) {
366
        $log->syslog('err', 'Unable to connect to the LDAP server "%s"',
367
            $ldap->{'host'});
368
        return undef;
salaun's avatar
salaun committed
369
370
    }

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

374
375
    # my @alt_attrs =
    #     split /\s*,\s*/, $ldap->{'alternative_email_attribute'} || '';
376

377
378
    my $mesg = $db->do_operation(
        'search',
379
        base    => $ldap->{'suffix'},
380
        filter  => $filter,
381
382
383
        scope   => $ldap->{'scope'},
        timeout => $ldap->{'timeout'},
        attrs   => [$ldap->{'email_attribute'}],
384
385
    );

386
    unless ($mesg and $mesg->count()) {
387
        $log->syslog('notice', "No entry in the LDAP Directory Tree of %s",
388
            $ldap->{'host'});
389
        $db->disconnect();
390
391
        return undef;
    }
salaun's avatar
salaun committed
392

393
    $db->disconnect();
394

395
    ## return only the first attribute
396
    my @results = $mesg->entries;
397
    foreach my $result (@results) {
398
        return (lc($result->get_value($ldap->{'email_attribute'})));
399
    }
salaun's avatar
salaun committed
400

401
}
salaun's avatar
salaun committed
402

403
404
# check trusted_application_name et trusted_application_password : return 1 or
# undef;
405
sub remote_app_check_password {
406
    my ($trusted_application_name, $password, $robot, $service) = @_;
407
    $log->syslog('debug', '(%s, %s, %s)', $trusted_application_name, $robot,
408
        $service);
409

410
    my $md5 = Digest::MD5::md5_hex($password);
411

412
    # seach entry for trusted_application in Conf
413
414
    my @trusted_apps;

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

458
459
# Moved to Sympa::Ticket::create().
#sub create_one_time_ticket;
460

461
462
# Moved to Sympa::Tickect::load().
#sub get_one_time_ticket;
463

salaun's avatar
salaun committed
464
1;