Auth.pm 13.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, 2016, 2017 GIP RENATER
11
12
# Copyright 2018, 2019 The Sympa Community. See the AUTHORS.md file at
# the top-level directory of this distribution and at
13
# <https://github.com/sympa-community/sympa.git>.
salaun's avatar
salaun committed
14
15
16
17
18
19
20
21
22
23
24
25
#
# 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
26
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
salaun's avatar
salaun committed
27

IKEDA Soji's avatar
IKEDA Soji committed
28
package Sympa::WWW::Auth;
salaun's avatar
salaun committed
29

30
31
use strict;
use warnings;
32
use Digest::MD5;
33
BEGIN { eval 'use Net::LDAP::Util'; }
salaun's avatar
salaun committed
34

35
use Sympa;
salaun's avatar
salaun committed
36
use Conf;
37
use Sympa::Database;
38
use Sympa::Log;
39
use Sympa::Robot;
40
use Sympa::Tools::Data;
41
use Sympa::Tools::Text;
42
use Sympa::User;
IKEDA Soji's avatar
IKEDA Soji committed
43
use Sympa::WWW::Report;
salaun's avatar
salaun committed
44

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

47
48
# Moved to: Sympa::User::password_fingerprint().
#sub password_fingerprint;
49

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

    my ($canonic, $user);

59
    if (Sympa::Tools::Text::valid_email($auth)) {
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
        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 {
IKEDA Soji's avatar
IKEDA Soji committed
77
78
                'user' => $user,
                'auth' => 'ldap',
79
80
81
            };

        } else {
Luc Didry's avatar
Luc Didry committed
82
83
            Sympa::WWW::Report::reject_report_web('user', 'incorrect_passwd',
                {})
84
                unless ($ENV{'SYMPA_SOAP'});
85
            $log->syslog('err', "Incorrect LDAP password");
86
87
88
89
            return undef;
        }
    }
}
salaun's avatar
salaun committed
90

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

116
117
    return $ok;
}
salaun's avatar
salaun committed
118
119

sub authentication {
120
121
    my ($robot, $email, $pwd) = @_;
    my ($user, $canonic);
122
    $log->syslog('debug', '(%s)', $email);
salaun's avatar
salaun committed
123

124
    unless ($user = Sympa::User::get_global_user($email)) {
125
126
        $user = {'email' => $email};
    }
salaun's avatar
salaun committed
127
    unless ($user->{'password'}) {
128
        $user->{'password'} = '';
salaun's avatar
salaun committed
129
    }
130

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

        ## 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') {
155
156
157
158
159
            # Old style RC4 encrypted password.
            if ($user->{'password'} and $user->{'password'} =~ /\Acrypt[.]/) {
                $log->syslog('notice',
                    'Password in database seems encrypted. Run upgrade_sympa_password.pl to rehash passwords'
                );
IKEDA Soji's avatar
IKEDA Soji committed
160
161
                Sympa::send_notify_to_listmaster('*', 'password_encrypted',
                    {});
162
163
164
                return undef;
            }

165
166
            my $fingerprint =
                Sympa::User::password_fingerprint($pwd, $user->{'password'});
167
168

            if ($fingerprint eq $user->{'password'}) {
169
                Sympa::User::update_password_hash($user, $pwd);
170
171
172
                Sympa::User::update_global_user($email,
                    {wrong_login_count => 0});
                return {
IKEDA Soji's avatar
IKEDA Soji committed
173
174
                    'user' => $user,
                    'auth' => 'classic',
175
176
177
178
179
180
                };
            }
        } elsif ($auth_service->{'auth_type'} eq 'ldap') {
            if ($canonic = ldap_authentication(
                    $robot, $auth_service, $email, $pwd, 'email_filter'
                )
Luc Didry's avatar
Luc Didry committed
181
            ) {
182
183
184
185
186
187
                unless ($user = Sympa::User::get_global_user($canonic)) {
                    $user = {'email' => $canonic};
                }
                Sympa::User::update_global_user($canonic,
                    {wrong_login_count => 0});
                return {
IKEDA Soji's avatar
IKEDA Soji committed
188
189
                    'user' => $user,
                    'auth' => 'ldap',
190
191
192
                };
            }
        }
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

IKEDA Soji's avatar
IKEDA Soji committed
199
    Sympa::WWW::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
205
206

    return undef;
}

sub ldap_authentication {
207
208
209
210
211
212
    $log->syslog('debug2', '(%s, %s, %s, *, %s)', @_[0 .. 2, 4]);
    my $robot       = shift;
    my $ldap        = shift;
    my $auth        = shift;
    my $pwd         = shift;
    my $whichfilter = shift;
213

214
    die 'bug in logic. Ask developer' unless $ldap->{auth_type} eq 'ldap';
215
216
217
218
    unless ($Net::LDAP::Util::VERSION) {
        $log->syslog('err', 'Net::LDAP::Util required. Install it');
        return undef;
    }
219

220
221
222
223
224
225
    # Skip ldap auth mechanism if an email address was provided and it does
    # not match the corresponding regexp.
    return undef
        if $auth =~ /\@/
        and defined $ldap->{regexp}
        and $auth !~ /$ldap->{regexp}/i;
226

227
    my $entry;
228

229
230
231
232
233
234
    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'};
    }
235
236
    my $escaped_auth = Net::LDAP::Util::escape_filter_value($auth);
    $filter =~ s/\[sender\]/$escaped_auth/ig;
237

238
    # Get the user's entry.
239
    my $db = Sympa::Database->new('LDAP', %$ldap);
240
241
242
    unless ($db and $db->connect) {
        $log->syslog('err', 'Unable to connect to the LDAP Server "%s": %s',
            $ldap->{host}, ($db and $db->error));
243
244
        return undef;
    }
245
    my $mesg = $db->do_operation(
246
        'search',
247
        base    => $ldap->{'suffix'},
248
        filter  => $filter,
249
250
251
        scope   => $ldap->{'scope'},
        timeout => $ldap->{'timeout'}
    );
252
253
254
255
256
257
    unless ($mesg and $entry = $mesg->shift_entry) {
        $log->syslog(
            'notice', 'Authentication for "%s" failed: %s',
            $auth, $mesg ? 'No entry' : $db->error
        );
        $db->disconnect;
258
259
        return undef;
    }
260
    $db->disconnect;
261

262
    # Bind again with user's DN and the password.
263
264
265
    $db = Sympa::Database->new(
        'LDAP',
        %$ldap,
266
        bind_dn       => $entry->dn,
267
        bind_password => $pwd,
268
    );
269
270
271
    unless ($db and $db->connect) {
        $log->syslog('notice', 'Authentication for "%s" failed: %s',
            $auth, ($db and $db->error));
272
273
        return undef;
    }
274
275
276
277
278
279
280
281
282
283
284
285
286
    $db->disconnect;

    # If the identifier provided was a valid email, return the provided email.
    # Otherwise, return the canonical email guessed after the login.
    my $do_canonicalize =
        Conf::get_robot_conf($robot, 'ldap_force_canonical_email')
        || !Sympa::Tools::Text::valid_email($auth);
    if ($do_canonicalize and $ldap->{email_attribute}) {
        my $values =
            $entry->get_value($ldap->{email_attribute}, alloptions => 1);
        ($auth) =
            grep { Sympa::Tools::Text::valid_email($_) }
            map { @{$values->{$_}} } sort keys %{$values || {}};
287
288
    }

289
290
291
    $log->syslog('debug3', 'Canonic: %s', $auth);
    return undef unless Sympa::Tools::Text::valid_email($auth);
    return $auth;
292
}
salaun's avatar
salaun committed
293

294
# fetch user email using their cas net_id and the paragrapah number in auth.conf
295
# NOTE: This might be moved to Robot package.
296
sub get_email_by_net_id {
297
298
299

    my $robot      = shift;
    my $auth_id    = shift;
300
    my $attributes = shift;
301

302
    $log->syslog('debug', '(%s, %s)', $auth_id, $attributes->{'uid'});
303
304
305
306
307
308
309
310
311

    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 =
312
            Sympa::Robot::get_netidtoemail_db($robot, $netid_cookie,
313
314
315
            $Conf::Conf{'auth_services'}{$robot}[$auth_id]{'service_id'});

        return $email;
316
    }
317

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

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

322
    unless ($db and $db->connect()) {
323
        $log->syslog('err', 'Unable to connect to the LDAP server "%s"',
324
            $ldap->{'host'});
325
        return undef;
salaun's avatar
salaun committed
326
327
    }

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

331
332
    my $mesg = $db->do_operation(
        'search',
333
        base    => $ldap->{'suffix'},
334
        filter  => $filter,
335
336
337
        scope   => $ldap->{'scope'},
        timeout => $ldap->{'timeout'},
        attrs   => [$ldap->{'email_attribute'}],
338
339
    );

340
    unless ($mesg and $mesg->count()) {
341
        $log->syslog('notice', "No entry in the LDAP Directory Tree of %s",
342
            $ldap->{'host'});
343
        $db->disconnect();
344
345
        return undef;
    }
salaun's avatar
salaun committed
346

347
    $db->disconnect();
348

349
    ## return only the first attribute
350
    my @results = $mesg->entries;
351
    foreach my $result (@results) {
352
        return (lc($result->get_value($ldap->{'email_attribute'})));
353
    }
salaun's avatar
salaun committed
354

355
}
salaun's avatar
salaun committed
356

357
358
# check trusted_application_name et trusted_application_password : return 1 or
# undef;
359
sub remote_app_check_password {
360
    my ($trusted_application_name, $password, $robot, $service) = @_;
361
    $log->syslog('debug', '(%s, %s, %s)', $trusted_application_name, $robot,
362
        $service);
363

364
    my $md5 = Digest::MD5::md5_hex($password);
365

366
    # seach entry for trusted_application in Conf
367
368
    my @trusted_apps;

369
    # select trusted_apps from robot context or sympa context
370
371
372
373
374
375
    @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'}) {
376
                # $log->syslog('debug', 'Authentication succeed for %s',$application->{'name'});
377
                my %proxy_for_vars;
378
                my %set_vars;
379
380
381
382
                foreach my $varname (@{$application->{'proxy_for_variables'}})
                {
                    $proxy_for_vars{$varname} = 1;
                }
383
384
385
386
387
388
389
390
391
                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;
                    }
392
                    $log->syslog(
393
394
395
396
397
398
                        'info',   'Illegal command %s received from %s',
                        $service, $trusted_application_name
                    );
                    return;
                }
                return (\%proxy_for_vars, \%set_vars);
399
            } else {
400
                $log->syslog('info', 'Bad password from %s',
401
                    $trusted_application_name);
402
                return;
403
404
405
            }
        }
    }
406
    # no matching application found
407
    $log->syslog('info', 'Unknown application name %s',
408
        $trusted_application_name);
409
    return;
410
}
411

412
413
# Moved to Sympa::Ticket::create().
#sub create_one_time_ticket;
414

415
416
# Moved to Sympa::Tickect::load().
#sub get_one_time_ticket;
417

salaun's avatar
salaun committed
418
1;