View | Details | Raw Unified | Return to bug 7174
Collapse All | Expand All

(-)a/Koha/Auth.pm (+227 lines)
Line 0 Link Here
1
package Koha::Auth;
2
3
# Copyright 2015 Vaara-kirjastot
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
#Define common packages
21
use Modern::Perl;
22
use Scalar::Util qw(blessed);
23
use Try::Tiny;
24
25
#Define Koha packages
26
use Koha::Auth::RequestNormalizer;
27
use Koha::Auth::Route::Password;
28
use Koha::Auth::Route::Cookie;
29
use Koha::Auth::Route::RESTV1;
30
31
#Define Exceptions
32
use Koha::Exception::BadParameter;
33
use Koha::Exception::Logout;
34
use Koha::Exception::UnknownProgramState;
35
36
use C4::Branch;
37
38
#Define the headers, POST-parameters and cookies extracted from the various web-frameworks'
39
# request-objects and passed to the authentication system as normalized values.
40
our @authenticationHeaders = ('X-Koha-Date', 'Authorization');
41
our @authenticationPOSTparams = ('password', 'userid', 'PT', 'branch', 'logout.x', 'koha_login_context');
42
our @authenticationCookies = ('CGISESSID'); #Really we should have only one of these.
43
44
=head authenticate
45
46
@PARAM3 HASHRef of authentication directives. Supported values:
47
            'inOPAC' => 1,    #Authentication context is in OPAC
48
            'inREST' => 'v1', #Authentication context is in REST API V1
49
            'inSC'   => 1,    #Authentication context is in the staff client
50
            'authnotrequired' => 1, #Disregard all Koha::Exception::LoginFailed||NoPermission-exceptions,
51
                                    #and authenticate as an anonymous user if normal authentication
52
                                    #fails.
53
@THROWS Koha::Exception::VersionMismatch
54
        Koha::Exception::BadSystemPreference
55
        Koha::Exception::BadParameter
56
        Koha::Exception::ServiceTemporarilyUnavailable
57
        Koha::Exception::LoginFailed
58
        Koha::Exception::NoPermission
59
        Koha::Exception::Logout, catch this and redirect the request to the logout page.
60
=cut
61
62
sub authenticate {
63
    my ($controller, $permissions, $authParams) = @_;
64
    my $rae = _authenticate_validateAndNormalizeParameters(@_); #Get the normalized request authentication elements
65
66
    my $borrower; #Each authentication route returns a Koha::Borrower-object on success. We use this to generate the Context()
67
68
    ##Select the Authentication route.
69
    ##Routes are introduced in priority order, and if one matches, the other routes are ignored.
70
    try {
71
        #0. Logout
72
        if ($rae->{postParams}->{'logout.x'}) {
73
            clearUserEnvironment($rae, $authParams);
74
            Koha::Exception::Logout->throw(error => "User logged out. Please redirect me!");
75
        }
76
        #1. Check for password authentication, including LDAP.
77
        elsif ($rae->{postParams}->{koha_login_context} && $rae->{postParams}->{userid} && $rae->{postParams}->{password}) {
78
            $borrower = Koha::Auth::Route::Password::challenge($rae, $permissions, $authParams);
79
        }
80
        #2. Check for REST's signature-based authentication.
81
        #elsif ($rae->{headers}->{'Authorization'} && $rae->{headers}->{'Authorization'} =~ /Koha/) {
82
        elsif ($rae->{headers}->{'Authorization'}) {
83
            $borrower = Koha::Auth::Route::RESTV1::challenge($rae, $permissions, $authParams);
84
        }
85
        #3. Check for the cookie. If cookies go stale, they block all subsequent authentication methods, so keep it down on this list.
86
        elsif ($rae->{cookies}->{CGISESSID}) {
87
            $borrower = Koha::Auth::Route::Cookie::challenge($rae, $permissions, $authParams);
88
        }
89
        else { #HTTP CAS ticket or shibboleth or Persona not implemented
90
            #We don't know how to authenticate, or there is no authentication attempt.
91
            Koha::Exception::LoginFailed->throw(error => "Koha doesn't understand your authentication protocol.");
92
        }
93
    } catch {
94
        if (blessed($_)) {
95
            if ($_->isa('Koha::Exception::LoginFailed') || $_->isa('Koha::Exception::NoPermission')) {
96
                if ($authParams->{authnotrequired}) { #We failed to login, but we can continue anonymously.
97
                    $borrower = Koha::Borrower->new();
98
                }
99
                else {
100
                    $_->rethrow(); #Anonymous login not allowed this time
101
                }
102
            }
103
            else {
104
                die $_; #Propagate other errors to the calling Controller to redirect as it wants.
105
            }
106
        }
107
        else {
108
            die $_; #Not a Koha::Exception-object
109
        }
110
    };
111
112
    my $session = setUserEnvironment($controller, $rae, $borrower, $authParams);
113
    my $cookie = Koha::Auth::RequestNormalizer::getSessionCookie($controller, $session);
114
115
    return ($borrower, $cookie);
116
}
117
118
=head _authenticate_validateAndNormalizeParameters
119
120
@PARAM1 CGI- or Mojolicious::Controller-object, this is used to identify which web framework to use.
121
@PARAM2 HASHRef or undef, Permissions HASH telling which Koha permissions the user must have, to access the resource.
122
@PARAM3 HASHRef or undef, Special authentication parameters, see authenticate()
123
@THROWS Koha::Exception::BadParameter, if validating parameters fails.
124
=cut
125
126
sub _authenticate_validateAndNormalizeParameters {
127
    my ($controller, $permissions, $authParams) = @_;
128
129
    #Validate $controller.
130
    my $requestAuthElements;
131
    if (blessed($controller) && $controller->isa('CGI')) {
132
        $requestAuthElements = Koha::Auth::RequestNormalizer::normalizeCGI($controller, \@authenticationHeaders, \@authenticationPOSTparams, \@authenticationCookies);
133
    }
134
    elsif (blessed($controller) && $controller->isa('Mojolicious::Controller')) {
135
        $requestAuthElements = Koha::Auth::RequestNormalizer::normalizeMojolicious($controller, \@authenticationHeaders, \@authenticationPOSTparams, \@authenticationCookies);
136
    }
137
    else {
138
        Koha::Exception::BadParameter->throw(error => "Koha::Auth::authenticate():> The first parameter MUST be either a 'CGI'-object or a 'Mojolicious::Controller'-object");
139
    }
140
    #Validate $permissions
141
    unless (not($permissions) || (ref $permissions eq 'HASH')) {
142
        Koha::Exception::BadParameter->throw(error => "Koha::Auth::authenticate():> The second parameter MUST be 'undef' or a HASHRef of Koha permissions. See C4::Auth::haspermission().");
143
    }
144
    #Validate $authParams
145
    unless (not($authParams) || (ref $authParams eq 'HASH')) {
146
        Koha::Exception::BadParameter->throw(error => "Koha::Auth::authenticate():> The third parameter MUST be 'undef' or a HASHRef.");
147
    }
148
149
    return $requestAuthElements;
150
}
151
152
=head setUserEnvironment
153
Set the C4::Context::user_env() and CGI::Session.
154
155
Any idea why there is both the CGI::Session and C4::Context::usernenv??
156
=cut
157
158
sub setUserEnvironment {
159
    my ($controller, $rae, $borrower, $authParams) = @_;
160
161
    my $session = C4::Auth::get_session( $rae->{cookies}->{CGISESSID} || '' );
162
    C4::Context->_new_userenv( $session->id );
163
164
    _determineUserBranch($rae, $borrower, $authParams, $session);
165
166
    #Then start setting remaining session parameters
167
    $session->param( 'number',       $borrower->borrowernumber );
168
    $session->param( 'id',           $borrower->userid );
169
    $session->param( 'cardnumber',   $borrower->cardnumber );
170
    $session->param( 'firstname',    $borrower->firstname );
171
    $session->param( 'surname',      $borrower->surname );
172
    $session->param( 'emailaddress', $borrower->email );
173
    $session->param( 'ip',           $session->remote_addr() );
174
    $session->param( 'lasttime',     time() );
175
176
    #Finally configure the userenv.
177
    C4::Context->set_userenv(
178
        $session->param('number'),       $session->param('id'),
179
        $session->param('cardnumber'),   $session->param('firstname'),
180
        $session->param('surname'),      $session->param('branch'),
181
        $session->param('branchname'),   undef,
182
        $session->param('emailaddress'), $session->param('branchprinter'),
183
        $session->param('persona'),      $session->param('shibboleth')
184
    );
185
186
    return $session;
187
}
188
189
sub _determineUserBranch {
190
    my ($rae, $borrower, $authParams, $session) = @_;
191
192
    my ($branchcode, $branchname);
193
    if ($rae->{postParams}->{branch}) {
194
        #We are instructed to change the active branch
195
        $branchcode = $rae->{postParams}->{branch};
196
    }
197
    elsif ($session->param('branch') && $session->param('branch') ne 'NO_LIBRARY_SET') {
198
        ##Branch is already set
199
        $branchcode = $session->param('branch');
200
    }
201
    elsif ($borrower->branchcode) {
202
        #Default to the borrower's branch
203
        $branchcode = $borrower->branchcode;
204
    }
205
    else {
206
        #No borrower branch? This must be the superuser.
207
        $branchcode = 'NO_LIBRARY_SET';
208
        $branchname = 'NO_LIBRARY_SET';
209
    }
210
    $session->param( 'branch',     $branchcode );
211
    $session->param( 'branchname', ($branchname || C4::Branch::GetBranchName($branchcode) || 'NO_LIBRARY_SET'));
212
}
213
214
=head clearUserEnvironment
215
216
Removes all active authentications
217
=cut
218
219
sub clearUserEnvironment {
220
    my ($rae, $authParams) = @_;
221
222
    my $session = C4::Auth::get_session( $rae->{cookies}->{CGISESSID} );
223
    $session->delete();
224
    $session->flush;
225
    #Do we need to unset this if it has never been set? C4::Context::_unset_userenv( $rae->{cookies}->{CGISESSID} );
226
}
227
1;
(-)a/Koha/Auth/Challenge.pm (+74 lines)
Line 0 Link Here
1
package Koha::Auth::Challenge;
2
3
# Copyright 2015 Vaara-kirjastot
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
=head1 NAME Koha::Auth::Challenge
23
24
=head2 SYNOPSIS
25
26
This is a authentication challenge parent class.
27
All Challenge-objects must implement the challenge()-method.
28
29
=head SUBLASSING
30
31
package Koha::Auth::Challenge::YetAnotherChallenge;
32
33
use base qw('Koha::Auth::Challenge');
34
35
sub challenge {
36
    #Implement the parent method to make this subclass interoperable.
37
}
38
39
=head2 USAGE
40
41
    use Scalar::Util qw(blessed);
42
    try {
43
        ...
44
        Koha::Auth::Challenge::Version::challenge();
45
        Koha::Auth::Challenge::OPACMaintenance::challenge();
46
        Koha::Auth::Challenge::YetAnotherChallenge::challenge();
47
        ...
48
    } catch {
49
        if (blessed($_)) {
50
            if ($_->isa('Koha::Exception::VersionMismatch')) {
51
                ##handle exception
52
            }
53
            elsif ($_->isa('Koha::Exception::AnotherKindOfException')) {
54
                ...
55
            }
56
            ...
57
            else {
58
                warn "Unknown exception class ".ref($_)."\n";
59
                die $_; #Unhandled exception case
60
            }
61
        }
62
        else {
63
            die $_; #Not a Koha::Exception-object
64
        }
65
    };
66
67
=cut
68
69
sub challenge {
70
    #@OVERLOAD this "interface"
71
    warn caller()." doesn't implement challenge()\n";
72
}
73
74
1;
(-)a/Koha/Auth/Challenge/Cookie.pm (+83 lines)
Line 0 Link Here
1
package Koha::Auth::Challenge::Cookie;
2
3
# Copyright 2015 Vaara-kirjastot
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use C4::Context;
23
use C4::Auth;
24
use Koha::AuthUtils;
25
use Koha::Borrowers;
26
27
use Koha::Exception::LoginFailed;
28
29
use base qw(Koha::Auth::Challenge);
30
31
=head challenge
32
STATIC
33
34
    Koha::Auth::Challenge::Cookie::challenge($cookieValue);
35
36
Checks if the given authentication cookie value matches a session, and checks if
37
the session is still active.
38
@PARAM1 String, hashed session key identifying a session in koha.sessions
39
@RETURNS Koha::Borrower matching the verified and active session
40
@THROWS Koha::Exception::LoginFailed, if no session is found,
41
                                      if the session has expired,
42
                                      if the session IP address changes,
43
                                      if no borrower was found for the session
44
=cut
45
46
sub challenge {
47
    my ($cookie, $originIps) = @_;
48
49
    my $session = C4::Auth::get_session($cookie);
50
    Koha::Exception::LoginFailed->throw(error => "No session matching the given session identifier '$session'.") unless $session;
51
52
    # See if the given session is timed out
53
    if ( ($session->param('lasttime') || 0) < (time()- C4::Auth::_timeout_syspref()) ) {
54
        $session->delete();
55
        $session->flush;
56
        C4::Context::_unset_userenv($cookie);
57
        Koha::Exception::LoginFailed->throw(error => "Session expired, please login again.");
58
    }
59
    # Check if we still access using the same IP than when the session was initialized.
60
    elsif ( C4::Context->preference('SessionRestrictionByIP')) {
61
62
        my $sameIpFound = grep {$session->param('ip') eq $_} @$originIps;
63
64
        unless ($sameIpFound) {
65
            $session->delete();
66
            $session->flush;
67
            C4::Context::_unset_userenv($cookie);
68
            Koha::Exception::LoginFailed->throw(error => "Session's client address changed, please login again.");
69
        }
70
    }
71
72
    #Get the Borrower-object
73
    my $userid   = $session->param('id');
74
    my $borrower = Koha::AuthUtils::checkKohaSuperuserFromUserid($userid);
75
    $borrower = Koha::Borrowers->find({userid => $userid}) if not($borrower) && $userid;
76
    Koha::Exception::LoginFailed->throw(error => "Cookie authentication succeeded, but no borrower found with userid '".($userid || '')."'.")
77
            unless $borrower;
78
79
    $session->param( 'lasttime', time() );
80
    return $borrower;
81
}
82
83
1;
(-)a/Koha/Auth/Challenge/IndependentBranchesAutolocation.pm (+52 lines)
Line 0 Link Here
1
package Koha::Auth::Challenge::IndependentBranchesAutolocation;
2
3
# Copyright 2015 Vaara-kirjastot
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use C4::Context;
23
use C4::Branch;
24
25
use Koha::Exception::LoginFailed;
26
27
use base qw(Koha::Auth::Challenge);
28
29
=head challenge
30
31
If sysprefs 'IndependentBranches' and 'Autolocation' are active, checks if the user
32
is in the correct network region to login.
33
@PARAM1 String, branchcode of the branch the current user is authenticating in to.
34
@THROWS Koha::Exception::LoginFailed, if the user is in the wrong network segment.
35
=cut
36
37
sub challenge {
38
    my ($currentBranchcode) = @_;
39
40
    if ( $currentBranchcode && C4::Context->boolean_preference('IndependentBranches') && C4::Context->boolean_preference('Autolocation') ) {
41
        my $ip = $ENV{'REMOTE_ADDR'};
42
43
        my $branches = C4::Branch::GetBranches();
44
        # we have to check they are coming from the right ip range
45
        my $domain = $branches->{$currentBranchcode}->{'branchip'};
46
        if ( $ip !~ /^$domain/ ) {
47
            Koha::Exception::LoginFailed->throw(error => "Branch '$currentBranchcode' is inaccessible from this network.");
48
        }
49
    }
50
}
51
52
1;
(-)a/Koha/Auth/Challenge/OPACMaintenance.pm (+44 lines)
Line 0 Link Here
1
package Koha::Auth::Challenge::OPACMaintenance;
2
3
# Copyright 2015 Vaara-kirjastot
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use C4::Context;
23
24
use base qw(Koha::Auth::Challenge);
25
26
use Koha::Exception::ServiceTemporarilyUnavailable;
27
28
=head challenge
29
STATIC
30
31
    Koha::Auth::Challenge::OPACMaintenance::challenge();
32
33
Checks if OPAC is under maintenance.
34
35
@THROWS Koha::Exception::ServiceTemporarilyUnavailable
36
=cut
37
38
sub challenge {
39
    if ( C4::Context->preference('OpacMaintenance') ) {
40
        Koha::Exception::ServiceTemporarilyUnavailable->throw(error => 'OPAC is under maintenance');
41
    }
42
}
43
44
1;
(-)a/Koha/Auth/Challenge/Password.pm (+127 lines)
Line 0 Link Here
1
package Koha::Auth::Challenge::Password;
2
3
# Copyright 2015 Vaara-kirjastot
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Koha::Borrowers;
23
use Koha::AuthUtils;
24
25
use base qw(Koha::Auth::Challenge);
26
27
use Koha::Exception::LoginFailed;
28
29
our @usernameAliasColumns = ('userid', 'cardnumber'); #Possible columns to treat as the username when authenticating. Must be UNIQUE in DB.
30
31
=head NAME Koha::Auth::Challenge::Password
32
33
=head SYNOPSIS
34
35
This module implements the more specific behaviour of the password authentication component.
36
37
=cut
38
39
=head challenge
40
STATIC
41
42
    Koha::Auth::Challenge::Password::challenge();
43
44
@RETURN Koha::Borrower-object if check succeedes, otherwise throws exceptions.
45
@THROWS Koha::Exception::LoginFailed from Koha::AuthUtils password checks.
46
=cut
47
48
sub challenge {
49
    my ($userid, $password) = @_;
50
51
    my $borrower;
52
    if (C4::Context->config('useldapserver')) {
53
        $borrower = Koha::Auth::Challenge::Password::checkLDAPPassword($userid, $password);
54
        return $borrower if $borrower;
55
    }
56
    if (C4::Context->preference('casAuthentication')) {
57
        warn("Koha::Auth doesn't support CAS-authentication yet. Please refactor the CAS client implementation to work with Koha::Auth. It cant be too hard :)");
58
    }
59
    if (C4::Context->config('useshibboleth')) {
60
        warn("Koha::Auth doesn't support Shibboleth-authentication yet. Please refactor the Shibboleth client implementation to work with Koha::Auth. It cant be too hard :)");
61
    }
62
63
    return Koha::Auth::Challenge::Password::checkKohaPassword($userid, $password);
64
}
65
66
=head checkKohaPassword
67
68
    my $borrower = Koha::Auth::Challenge::Password::checkKohaPassword($userid, $password);
69
70
Checks if the given username and password match anybody in the Koha DB
71
@PARAM1 String, user identifier, either the koha.borrowers.userid, or koha.borrowers.cardnumber
72
@PARAM2 String, clear text password from the authenticating user
73
@RETURN Koha::Borrower, if login succeeded.
74
                Sets Koha::Borrower->isSuperuser() if the user is a superuser.
75
@THROWS Koha::Exception::LoginFailed, if no matching password was found for all username aliases in Koha.
76
=cut
77
78
sub checkKohaPassword {
79
    my ($userid, $password) = @_;
80
    my $borrower; #Find the borrower to return
81
82
    $borrower = Koha::AuthUtils::checkKohaSuperuser($userid, $password);
83
    return $borrower if $borrower;
84
85
    my $usernameFound = 0; #Report to the user if userid/barcode was found, even if the login failed.
86
    #Check for each username alias if we can confirm a login with that.
87
    for my $unameAlias (@usernameAliasColumns) {
88
        my $borrower = Koha::Borrowers->find({$unameAlias => $userid});
89
        if ( $borrower ) {
90
            $usernameFound = 1;
91
            return $borrower if ( Koha::AuthUtils::checkHash( $password, $borrower->password ) );
92
        }
93
    }
94
95
    Koha::Exception::LoginFailed->throw(error => "Password authentication failed for the given ".( ($usernameFound) ? "password" : "username and password").".");
96
}
97
98
=head checkLDAPPassword
99
100
Checks if the given username and password match anybody in the LDAP service
101
@PARAM1 String, user identifier
102
@PARAM2 String, clear text password from the authenticating user
103
@RETURN Koha::Borrower, or
104
            undef if we couldn't reliably contact the LDAP server so we should
105
            fallback to local Koha Password authentication.
106
@THROWS Koha::Exception::LoginFailed, if LDAP login failed
107
=cut
108
109
sub checkLDAPPassword {
110
    my ($userid, $password) = @_;
111
112
    #Lazy load dependencies because somebody might never need them.
113
    require C4::Auth_with_ldap;
114
115
    my ($retval, $cardnumber, $local_userid) = C4::Auth_with_ldap::checkpw_ldap($userid, $password);    # EXTERNAL AUTH
116
    if ($retval == -1) {
117
        Koha::Exception::LoginFailed->throw(error => "LDAP authentication failed for the given username and password");
118
    }
119
120
    if ($retval) {
121
        my $borrower = Koha::Borrowers->find({userid => $local_userid});
122
        return $borrower;
123
    }
124
    return undef;
125
}
126
127
1;
(-)a/Koha/Auth/Challenge/Permission.pm (+42 lines)
Line 0 Link Here
1
package Koha::Auth::Challenge::Permission;
2
3
# Copyright 2015 Vaara-kirjastot
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Koha::Auth::PermissionManager;
23
24
use base qw(Koha::Auth::Challenge);
25
26
=head challenge
27
STATIC
28
29
    Koha::Auth::Challenge::Permission::challenge($borrower, $permissionsRequired);
30
31
@THROWS Koha::Exception::NoPermission with the missing permission if permissions
32
                are inadequate
33
=cut
34
35
sub challenge {
36
    my ($borrower, $permissionsRequired) = @_;
37
38
    my $permissionManager = Koha::Auth::PermissionManager->new();
39
    $permissionManager->hasPermissions($borrower, $permissionsRequired);
40
}
41
42
1;
(-)a/Koha/Auth/Challenge/RESTV1.pm (+169 lines)
Line 0 Link Here
1
package Koha::Auth::Challenge::RESTV1;
2
3
# Copyright 2015 Vaara-kirjastot
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
use DateTime::Format::HTTP;
22
use DateTime;
23
24
use Koha::Borrowers;
25
26
use base qw(Koha::Auth::Challenge);
27
28
use Koha::Exception::LoginFailed;
29
use Koha::Exception::BadParameter;
30
31
=head challenge
32
33
    my $borrower = Koha::Auth::Challenge::RESTV1::challenge();
34
35
For authentication to succeed, the client have to send 2 HTTP
36
headers:
37
 - X-Koha-Date: the standard HTTP Date header complying to RFC 1123, simply wrapped to X-Koha-Date,
38
                since the w3-specification forbids setting the Date-header from javascript.
39
 - Authorization: the standard HTTP Authorization header, see below for how it is constructed.
40
41
=head2 HTTP Request example
42
43
GET /api/v1/borrowers/12 HTTP/1.1
44
Host: api.yourkohadomain.fi
45
X-Koha-Date: Mon, 26 Mar 2007 19:37:58 +0000
46
Authorization: Koha admin69:frJIUN8DYpKDtOLCwo//yllqDzg=
47
48
=head2 Constructing the Authorization header
49
50
-You brand the authorization header with "Koha"
51
-Then you give the userid/cardnumber of the user authenticating.
52
-Then the hashed signature.
53
54
The signature is a HMAC-SHA256-HEX hash of several elements of the request,
55
separated by spaces:
56
 - HTTP method (uppercase)
57
 - userid/cardnumber
58
 - X-Koha-Date-header
59
Signed with the Borrowers API key
60
61
The server then tries to rebuild the signature with each of the user's API keys.
62
If one matches the received signature, then authentication is almost OK.
63
64
To avoid requests to be replayed, the last request's X-Koha-Date-header is stored
65
in database and the authentication succeeds only if the stored Date
66
is lesser than the X-Koha-Date-header.
67
68
=head2 Constructing the signature example
69
70
Signature = HMAC-SHA256-HEX("HTTPS" + " " +
71
                            "/api/v1/borrowers/12?howdoyoudo=voodoo" + " " +
72
                            "admin69" + " " +
73
                            "760818212" + " " +
74
                            "frJIUN8DYpKDtOLCwo//yllqDzg="
75
                           );
76
77
=head
78
79
@PARAM1 HASHRef of Header name => values
80
@PARAM2 String, upper case request method name, eg. HTTP or HTTPS
81
@PARAM3 String the request uri
82
@RETURNS Koha::Borrower if authentication succeeded.
83
@THROWS Koha::Exception::LoginFailed, if API key signature verification failed
84
@THROWS Koha::Exception::BadParameter
85
@THROWS Koha::Exception::UnknownObject, if we cannot find a Borrower with the given input.
86
=cut
87
88
sub challenge {
89
    my ($headers, $method, $uri) = @_;
90
91
    my $req_dt;
92
    eval {
93
        $req_dt = DateTime::Format::HTTP->parse_datetime( $headers->{'X-Koha-Date'} ); #Returns DateTime
94
    };
95
    my $authorizationHeader = $headers->{'Authorization'};
96
    my ($req_username, $req_signature);
97
    if ($authorizationHeader =~ /^Koha (\S+?):(\w+)$/) {
98
        $req_username = $1;
99
        $req_signature = $2;
100
    }
101
    else {
102
        Koha::Exception::BadParameter->throw(error => "Authorization HTTP-header is not well formed. It needs to be of format 'Authorization: Koha userid:signature'");
103
    }
104
    unless ($req_dt) {
105
        Koha::Exception::BadParameter->throw(error => "X-Koha-Date HTTP-header is not well formed. It needs to be of RFC 1123 -date format, eg. 'X-Koha-Date: Wed, 09 Feb 1994 22:23:32 +0200'");
106
    }
107
108
    my $borrower = Koha::Borrowers->cast($req_username);
109
110
    my @apikeys = Koha::ApiKeys->search({
111
        borrowernumber => $borrower->borrowernumber,
112
        active => 1,
113
    });
114
    Koha::Exception::LoginFailed->throw(error => "User has no API keys. Please add one using the Staff interface or OPAC.") unless @apikeys;
115
116
    my $matchingApiKey;
117
    foreach my $apikey (@apikeys) {
118
        my $signature = makeSignature($method, $req_username, $headers->{'X-Koha-Date'}, $apikey);
119
120
        if ($signature eq $req_signature) {
121
            $matchingApiKey = $apikey;
122
            last();
123
        }
124
    }
125
126
    unless ($matchingApiKey) {
127
        Koha::Exception::LoginFailed->throw(error => "API key authentication failed");
128
    }
129
130
    unless ($matchingApiKey->last_request_time < $req_dt->epoch()) {
131
        Koha::Exception::BadParameter->throw(error => "X-Koha-Date HTTP-header is stale, expected later date than '".DateTime::Format::HTTP->format_datetime($req_dt)."'");
132
    }
133
134
    $matchingApiKey->set({last_request_time => $req_dt->epoch()});
135
    $matchingApiKey->store();
136
137
    return $borrower;
138
}
139
140
sub makeSignature {
141
    my ($method, $userid, $headerXKohaDate, $apiKey) = @_;
142
143
    my $message = join(' ', uc($method), $userid, $headerXKohaDate);
144
    return Digest::SHA::hmac_sha256_hex($message, $apiKey->api_key);
145
}
146
147
=head prepareAuthenticationHeaders
148
@PARAM1 Koha::Borrower, to authenticate
149
@PARAM2 DateTime, OPTIONAL, the timestamp of the HTTP request
150
@PARAM3 HTTP verb, 'get', 'post', 'patch', 'put', ...
151
@RETURNS HASHRef of authentication HTTP header names and their values. {
152
            "X-Koha-Date" => "Mon, 26 Mar 2007 19:37:58 +0000",
153
            "Authorization" => "Koha admin69:frJIUN8DYpKDtOLCwo//yllqDzg=",
154
        }
155
=cut
156
157
sub prepareAuthenticationHeaders {
158
    my ($borrower, $dateTime, $method) = @_;
159
    $borrower = Koha::Borrowers->cast($borrower);
160
161
    my $headerXKohaDate = DateTime::Format::HTTP->format_datetime(
162
                                                ($dateTime || DateTime->now( time_zone => C4::Context->tz() ))
163
                          );
164
    my $headerAuthorization = "Koha ".$borrower->userid.":".makeSignature($method, $borrower->userid, $headerXKohaDate, $borrower->getApiKey('active'));
165
    return {'X-Koha-Date' => $headerXKohaDate,
166
            'Authorization' => $headerAuthorization};
167
}
168
169
1;
(-)a/Koha/Auth/Challenge/Version.pm (+56 lines)
Line 0 Link Here
1
package Koha::Auth::Challenge::Version;
2
3
# Copyright 2015 Vaara-kirjastot
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use C4::Context;
23
use Koha;
24
25
use base qw(Koha::Auth::Challenge);
26
27
use Koha::Exception::VersionMismatch;
28
use Koha::Exception::BadSystemPreference;
29
30
=head challenge
31
STATIC
32
33
    Koha::Auth::Challenge::Version::challenge();
34
35
Checks if the DB version is valid.
36
37
@THROWS Koha::Exception::VersionMismatch, if versions do not match
38
@THROWS Koha::Exception::BadSystemPreference, if "Version"-syspref is not set.
39
                        This probably means that Koha has not been installed yet.
40
=cut
41
42
sub challenge {
43
    my $versionSyspref = C4::Context->preference('Version');
44
    unless ( $versionSyspref ) {
45
        Koha::Exception::BadSystemPreference->throw(error => "No Koha 'Version'-system preference defined. Koha needs to be installed.");
46
    }
47
48
    my $kohaversion = Koha::version();
49
    # remove the 3 last . to have a Perl number
50
    $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
51
    if ( $versionSyspref < $kohaversion ) {
52
        Koha::Exception::VersionMismatch->throw(error => "Database update needed. Database is 'v$versionSyspref' and Koha is 'v$kohaversion'");
53
    }
54
}
55
56
1;
(-)a/Koha/Auth/RequestNormalizer.pm (+166 lines)
Line 0 Link Here
1
package Koha::Auth::RequestNormalizer;
2
3
# Copyright 2015 Vaara-kirjastot
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Scalar::Util qw(blessed);
23
24
25
=head normalizeCGI
26
27
Takes a CGI-object and finds the authentication markers from it.
28
@PARAM1 CGI-object.
29
@PARAM2 ARRAYRef, authentication headers that should be extracted for authentication
30
@PARAM3 ARRAYRef, authentication POST parameters that should be extracted for authentication
31
@PARAM4 ARRAYRef, authentication cookies that should be extracted for authentication
32
@RETURNS List of : HASHRef of headers required for authentication, or undef
33
                   HASHRef of POST parameters required for authentication, or undef
34
                   HASHRef of the authenticaton cookie name => value, or undef
35
=cut
36
37
sub normalizeCGI {
38
    my ($controller, $authenticationHeaders, $authenticationPOSTparams, $authenticationCookies) = @_;
39
40
    my ($headers, $postParams, $cookies) = ({}, {}, {});
41
    foreach my $authHeader (@$authenticationHeaders) {
42
        if (my $val = $controller->http($authHeader)) {
43
            $headers->{$authHeader} = $val;
44
        }
45
    }
46
    foreach my $authParam (@$authenticationPOSTparams) {
47
        if (my $val = $controller->param($authParam)) {
48
            $postParams->{$authParam} = $val;
49
        }
50
    }
51
    foreach my $authCookie (@$authenticationCookies) {
52
        if (my $val = $controller->cookie($authCookie)) {
53
            $cookies->{$authCookie} = $val;
54
        }
55
    }
56
57
    my $method = $1 if ($ENV{SERVER_PROTOCOL} =~ /^(.+?)\//);
58
59
    my @originIps = ($ENV{'REMOTE_ADDR'});
60
61
    my $requestAuthElements = { #Collect the authentication elements here.
62
        headers => $headers,
63
        postParams => $postParams,
64
        cookies => $cookies,
65
        originIps => \@originIps,
66
        method => $method,
67
        url => $ENV{REQUEST_URI},
68
    };
69
    return $requestAuthElements;
70
}
71
72
=head normalizeMojolicious
73
74
Takes a Mojolicious::Controller-object and finds the authentication markers from it.
75
@PARAM1 Mojolicious::Controller-object.
76
@PARAM2-4 See normalizeCGI()
77
@RETURNS HASHRef of the request's authentication elements marked for extraction, eg:
78
        {
79
            headers => { X-Koha-Signature => '32rFrFw3iojsev34AS',
80
                         X-Koha-Username => 'pavlov'},
81
            POSTparams => { password => '1234',
82
                            userid => 'pavlov'},
83
            cookies => { CGISESSID => '233FADFEV3as1asS' },
84
            method => 'https',
85
            url => '/borrower/12/holds'
86
        }
87
=cut
88
89
sub normalizeMojolicious {
90
    my ($controller, $authenticationHeaders, $authenticationPOSTparams, $authenticationCookies) = @_;
91
92
    my $request = $controller->req();
93
    my ($headers, $postParams, $cookies) = ({}, {}, {});
94
    my $headersHash = $request->headers()->to_hash();
95
    foreach my $authHeader (@$authenticationHeaders) {
96
        if (my $val = $headersHash->{$authHeader}) {
97
            $headers->{$authHeader} = $val;
98
        }
99
    }
100
    foreach my $authParam (@$authenticationPOSTparams) {
101
        if (my $val = $request->param($authParam)) {
102
            $postParams->{$authParam} = $val;
103
        }
104
    }
105
106
    my $requestCookies = $request->cookies;
107
    if (scalar(@$requestCookies)) {
108
        foreach my $authCookieName (@$authenticationCookies) {
109
            foreach my $requestCookie (@$requestCookies) {
110
                if ($authCookieName eq $requestCookie->name) {
111
                    $cookies->{$authCookieName} = $requestCookie->value;
112
                }
113
            }
114
        }
115
    }
116
117
    my @originIps = ($controller->tx->original_remote_address());
118
    push @originIps, $request->headers()->header('X-Forwarded-For') if $request->headers()->header('X-Forwarded-For');
119
120
    my $requestAuthElements = { #Collect the authentication elements here.
121
        headers => $headers,
122
        postParams => $postParams,
123
        cookies => $cookies,
124
        originIps => \@originIps,
125
        method => $controller->req->method,
126
        url => '/' . $controller->req->url->path_query,
127
    };
128
    return $requestAuthElements;
129
}
130
131
=head getSessionCookie
132
133
@PARAM1 CGI- or Mojolicious::Controller-object, this is used to identify which web framework to use.
134
@PARAM2 CGI::Session.
135
@RETURNS a Mojolicious cookie or a CGI::Cookie.
136
=cut
137
138
sub getSessionCookie {
139
    my ($controller, $session) = @_;
140
141
    my $cookie = {
142
            name     => 'CGISESSID',
143
            value    => $session->id,
144
    };
145
    my $cookieOk;
146
147
    if (blessed($controller) && $controller->isa('CGI')) {
148
        $cookie->{HttpOnly} = 1;
149
        $cookieOk = $controller->cookie( $cookie );
150
    }
151
    elsif (blessed($controller) && $controller->isa('Mojolicious::Controller')) {
152
        $controller->res->cookies($cookie);
153
        foreach my $c (@{$controller->res->cookies}) {
154
            if ($c->name eq 'CGISESSID') {
155
                $cookieOk = $c;
156
                last;
157
            }
158
        }
159
    }
160
    unless ($cookieOk) {
161
        Koha::Exception::UnknownProgramState->throw(error => __PACKAGE__."::getSessionCookie():> Unable to get a proper cookie?");
162
    }
163
    return $cookieOk;
164
}
165
166
1;
(-)a/Koha/Auth/Route.pm (+75 lines)
Line 0 Link Here
1
package Koha::Auth::Route;
2
3
use Modern::Perl;
4
5
# Copyright 2015 Vaara-kirjastot
6
#
7
# This file is part of Koha.
8
#
9
# Koha is free software; you can redistribute it and/or modify it
10
# under the terms of the GNU General Public License as published by
11
# the Free Software Foundation; either version 3 of the License, or
12
# (at your option) any later version.
13
#
14
# Koha is distributed in the hope that it will be useful, but
15
# WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
# GNU General Public License for more details.
18
#
19
# You should have received a copy of the GNU General Public License
20
# along with Koha; if not, see <http://www.gnu.org/licenses>.
21
22
=head
23
24
=NAME Koha::Auth::Route
25
26
=SYNOPSIS
27
28
This is an interface definition for Koha::Auth::Route::* -subclasses.
29
This documentation explains how to subclass different routes.
30
31
=USAGE
32
33
    if ($userid && $password) {
34
        $borrower = Koha::Auth::Route::<RouteName>::challenge($requestAuthElements, $permissionsRequired, $routeParams);
35
    }
36
37
=head INPUT
38
39
Each Route gets three parameters:
40
    $requestAuthElements, HASHRef of HASHRefs:
41
        headers =>      HASHRef of HTTP Headers matching the @authenticationHeaders-package
42
                        variable in Koha::Auth,
43
                        Eg. { 'X-Koha-Signature' => "23in4ow2gas2opcnpa", ... }
44
        postParams =>   HASHRef of HTTP POST parameters matching the
45
                        @authenticationPOSTparams-package variable in Koha::Auth,
46
                        Eg. { password => '1234', 'userid' => 'admin'}
47
        cookies =>      HASHRef of HTTP Cookies matching the
48
                        @authenticationPOSTparams-package variable in Koha::Auth,
49
                        EG. { CGISESSID => '9821rj1kn3tr9ff2of2ln1' }
50
    $permissionsRequired:
51
                        HASHRef of Koha permissions.
52
                        See Koha::Auth::PermissionManager for example.
53
    $routeParams:       HASHRef of special Route-related data
54
                        {inOPAC => 1, authnotrequired => 0, ...}
55
56
=head OUTPUT
57
58
Each route must return a Koha::Borrower-object representing the authenticated user.
59
Even if the login succeeds with a superuser or similar virtual user, like
60
anonymous login, a mock Borrower-object must be returned.
61
If the login fails, each route must throw Koha::Exceptions to notify the cause
62
of the failure.
63
64
=head ROUTE STRUCTURE
65
66
Each route consists of Koha::Auth::Challenge::*-objects to test for various
67
authentication challenges.
68
69
See. Koha::Auth::Challenge for more information.
70
71
=cut
72
73
sub challenge {}; #@OVERLOAD this "interface"
74
75
1;
(-)a/Koha/Auth/Route/Cookie.pm (+44 lines)
Line 0 Link Here
1
package Koha::Auth::Route::Cookie;
2
3
# Copyright 2015 Vaara-kirjastot
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Koha::Auth::Challenge::OPACMaintenance;
23
use Koha::Auth::Challenge::Version;
24
use Koha::Auth::Challenge::Cookie;
25
use Koha::Auth::Challenge::Permission;
26
27
use base qw(Koha::Auth::Route);
28
29
=head challenge
30
See Koha::Auth::Route, for usage documentation.
31
@THROWS Koha::Exceptions from authentication components.
32
=cut
33
34
sub challenge {
35
    my ($rae, $permissionsRequired, $routeParams) = @_;
36
37
    Koha::Auth::Challenge::OPACMaintenance::challenge() if $routeParams->{inOPAC};
38
    Koha::Auth::Challenge::Version::challenge();
39
    my $borrower = Koha::Auth::Challenge::Cookie::challenge($rae->{cookies}->{CGISESSID}, $rae->{originIps});
40
    Koha::Auth::Challenge::Permission::challenge($borrower, $permissionsRequired) if $permissionsRequired;
41
    return $borrower;
42
}
43
44
1;
(-)a/Koha/Auth/Route/Password.pm (+46 lines)
Line 0 Link Here
1
package Koha::Auth::Route::Password;
2
3
# Copyright 2015 Vaara-kirjastot
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Koha::Auth::Challenge::OPACMaintenance;
23
use Koha::Auth::Challenge::Version;
24
use Koha::Auth::Challenge::IndependentBranchesAutolocation;
25
use Koha::Auth::Challenge::Password;
26
use Koha::Auth::Challenge::Permission;
27
28
use base qw(Koha::Auth::Route);
29
30
=head challenge
31
See Koha::Auth::Route, for usage documentation.
32
@THROWS Koha::Exceptions from authentication components.
33
=cut
34
35
sub challenge {
36
    my ($rae, $permissionsRequired, $routeParams) = @_;
37
38
    Koha::Auth::Challenge::OPACMaintenance::challenge() if $routeParams->{inOPAC};
39
    Koha::Auth::Challenge::Version::challenge();
40
    Koha::Auth::Challenge::IndependentBranchesAutolocation::challenge($routeParams->{branch});
41
    my $borrower = Koha::Auth::Challenge::Password::challenge($rae->{postParams}->{userid}, $rae->{postParams}->{password});
42
    Koha::Auth::Challenge::Permission::challenge($borrower, $permissionsRequired) if $permissionsRequired;
43
    return $borrower;
44
}
45
46
1;
(-)a/Koha/Auth/Route/RESTV1.pm (+43 lines)
Line 0 Link Here
1
package Koha::Auth::Route::RESTV1;
2
3
# Copyright 2015 Vaara-kirjastot
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Koha::Auth::Challenge::Version;
23
use Koha::Auth::Challenge::RESTV1;
24
use Koha::Auth::Challenge::Permission;
25
26
use base qw(Koha::Auth::Route);
27
28
=head challenge
29
See Koha::Auth::Route, for usage documentation.
30
@THROWS Koha::Exceptions from authentication components.
31
=cut
32
33
sub challenge {
34
    my ($rae, $permissionsRequired, $routeParams) = @_;
35
36
    #Koha::Auth::Challenge::RESTMaintenance::challenge() if $routeParams->{inREST}; #NOT IMPLEMENTED YET
37
    Koha::Auth::Challenge::Version::challenge();
38
    my $borrower = Koha::Auth::Challenge::RESTV1::challenge($rae->{headers}, $rae->{method}, $rae->{url});
39
    Koha::Auth::Challenge::Permission::challenge($borrower, $permissionsRequired) if $permissionsRequired;
40
    return $borrower;
41
}
42
43
1;
(-)a/Koha/AuthUtils.pm (-1 / +64 lines)
Lines 24-29 use Fcntl qw/O_RDONLY/; # O_RDONLY is used in generate_salt Link Here
24
24
25
use Koha::Borrower;
25
use Koha::Borrower;
26
26
27
use Koha::Exception::LoginFailed;
28
27
use base 'Exporter';
29
use base 'Exporter';
28
30
29
our $VERSION = '1.01';
31
our $VERSION = '1.01';
Lines 136-141 sub generate_salt { Link Here
136
    return $string;
138
    return $string;
137
}
139
}
138
140
141
=head checkHash
142
143
    my $passwordOk = Koha::AuthUtils::checkHash($password1, $password2)
144
145
Checks if a clear-text String/password matches the given hash when
146
MD5 or Bcrypt hashing algorith is applied to it.
147
148
Bcrypt is applied if @PARAM2 starts with '$2'
149
MD5 otherwise
150
151
@PARAM1 String, clear text passsword or any other String
152
@PARAM2 String, hashed text password or any other String.
153
@RETURN Boolean, 1 if given parameters match
154
               , 0 if not
155
=cut
156
157
sub checkHash {
158
    my ( $password, $stored_hash ) = @_;
159
160
    $password = Encode::encode( 'UTF-8', $password )
161
            if Encode::is_utf8($password);
162
163
    return if $stored_hash eq '!';
164
165
    my $hash;
166
    if ( substr( $stored_hash, 0, 2 ) eq '$2' ) {
167
        $hash = hash_password( $password, $stored_hash );
168
    } else {
169
        #@DEPRECATED Digest::MD5, don't use it or you will get hurt.
170
        require Digest::MD5;
171
        $hash = Digest::MD5::md5_base64($password);
172
    }
173
    return $hash eq $stored_hash;
174
}
175
176
=head checkKohaSuperuser
177
178
    my $borrower = Koha::AuthUtils::checkKohaSuperuser($userid, $password);
179
180
Check if the userid and password match the ones in the $KOHA_CONF
181
@PARAM1 String, user identifier, either the koha.borrowers.userid, or koha.borrowers.cardnumber
182
@PARAM2 String, clear text password from the authenticating user
183
@RETURNS Koha::Borrower branded as superuser with ->isSuperuser()
184
         or undef if user logging in is not a superuser.
185
@THROWS Koha::Exception::LoginFailed if user identifier matches, but password doesn't
186
=cut
187
188
sub checkKohaSuperuser {
189
    my ($userid, $password) = @_;
190
191
    if ( $userid && $userid eq C4::Context->config('user') ) {
192
        if ( $password && $password eq C4::Context->config('pass') ) {
193
            return _createTemporarySuperuser();
194
        }
195
        else {
196
            Koha::Exception::LoginFailed->throw(error => "Password authentication failed");
197
        }
198
    }
199
}
200
139
=head checkKohaSuperuserFromUserid
201
=head checkKohaSuperuserFromUserid
140
See checkKohaSuperuser(), with only the "user identifier"-@PARAM.
202
See checkKohaSuperuser(), with only the "user identifier"-@PARAM.
141
@THROWS nothing.
203
@THROWS nothing.
Lines 153-165 sub checkKohaSuperuserFromUserid { Link Here
153
215
154
Create a temporary superuser which should be instantiated only to the environment
216
Create a temporary superuser which should be instantiated only to the environment
155
and then discarded. So do not ->store() it!
217
and then discarded. So do not ->store() it!
156
@RETURN Koha::Borrower
218
@RETURN Koha::Borrower, stamped as superuser.
157
=cut
219
=cut
158
220
159
sub _createTemporarySuperuser {
221
sub _createTemporarySuperuser {
160
    my $borrower = Koha::Borrower->new();
222
    my $borrower = Koha::Borrower->new();
161
223
162
    my $superuserName = C4::Context->config('user');
224
    my $superuserName = C4::Context->config('user');
225
    $borrower->isSuperuser(1);
163
    $borrower->set({borrowernumber => 0,
226
    $borrower->set({borrowernumber => 0,
164
                       userid     => $superuserName,
227
                       userid     => $superuserName,
165
                       cardnumber => $superuserName,
228
                       cardnumber => $superuserName,
(-)a/Koha/Borrower.pm (+26 lines)
Lines 43-48 sub type { Link Here
43
    return 'Borrower';
43
    return 'Borrower';
44
}
44
}
45
45
46
=head isSuperuser
47
48
    $borrower->isSuperuser(1); #Set this borrower to be a superuser
49
    if ($borrower->isSuperuser()) {
50
        #All your base are belong to us
51
    }
52
53
Should be used from the authentication modules to mark this $borrower-object to
54
have unlimited access to all Koha-features.
55
This $borrower-object is the Koha DB user.
56
@PARAM1 Integer, 1 means this borrower is the super/DB user.
57
                "0" disables the previously set superuserness.
58
=cut
59
60
sub isSuperuser {
61
    my ($self, $Iam) = @_;
62
63
    if (defined $Iam && $Iam == 1) {
64
        $self->{superuser} = 1;
65
    }
66
    elsif (defined $Iam && $Iam eq "0") { #Dealing with zero is special in Perl
67
        $self->{superuser} = undef;
68
    }
69
    return (exists($self->{superuser}) && $self->{superuser}) ? 1 : undef;
70
}
71
46
=head1 AUTHOR
72
=head1 AUTHOR
47
73
48
Kyle M Hall <kyle@bywatersolutions.com>
74
Kyle M Hall <kyle@bywatersolutions.com>
(-)a/Koha/Schema/Result/BorrowerPermission.pm (+149 lines)
Line 0 Link Here
1
use utf8;
2
package Koha::Schema::Result::BorrowerPermission;
3
4
# Created by DBIx::Class::Schema::Loader
5
# DO NOT MODIFY THE FIRST PART OF THIS FILE
6
7
=head1 NAME
8
9
Koha::Schema::Result::BorrowerPermission
10
11
=cut
12
13
use strict;
14
use warnings;
15
16
use base 'DBIx::Class::Core';
17
18
=head1 TABLE: C<borrower_permissions>
19
20
=cut
21
22
__PACKAGE__->table("borrower_permissions");
23
24
=head1 ACCESSORS
25
26
=head2 borrower_permission_id
27
28
  data_type: 'integer'
29
  is_auto_increment: 1
30
  is_nullable: 0
31
32
=head2 borrowernumber
33
34
  data_type: 'integer'
35
  is_foreign_key: 1
36
  is_nullable: 0
37
38
=head2 permission_module_id
39
40
  data_type: 'integer'
41
  is_foreign_key: 1
42
  is_nullable: 0
43
44
=head2 permission_id
45
46
  data_type: 'integer'
47
  is_foreign_key: 1
48
  is_nullable: 0
49
50
=cut
51
52
__PACKAGE__->add_columns(
53
  "borrower_permission_id",
54
  { data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
55
  "borrowernumber",
56
  { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
57
  "permission_module_id",
58
  { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
59
  "permission_id",
60
  { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
61
);
62
63
=head1 PRIMARY KEY
64
65
=over 4
66
67
=item * L</borrower_permission_id>
68
69
=back
70
71
=cut
72
73
__PACKAGE__->set_primary_key("borrower_permission_id");
74
75
=head1 UNIQUE CONSTRAINTS
76
77
=head2 C<borrowernumber>
78
79
=over 4
80
81
=item * L</borrowernumber>
82
83
=item * L</permission_module_id>
84
85
=item * L</permission_id>
86
87
=back
88
89
=cut
90
91
__PACKAGE__->add_unique_constraint(
92
  "borrowernumber",
93
  ["borrowernumber", "permission_module_id", "permission_id"],
94
);
95
96
=head1 RELATIONS
97
98
=head2 borrowernumber
99
100
Type: belongs_to
101
102
Related object: L<Koha::Schema::Result::Borrower>
103
104
=cut
105
106
__PACKAGE__->belongs_to(
107
  "borrowernumber",
108
  "Koha::Schema::Result::Borrower",
109
  { borrowernumber => "borrowernumber" },
110
  { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
111
);
112
113
=head2 permission
114
115
Type: belongs_to
116
117
Related object: L<Koha::Schema::Result::Permission>
118
119
=cut
120
121
__PACKAGE__->belongs_to(
122
  "permission",
123
  "Koha::Schema::Result::Permission",
124
  { permission_id => "permission_id" },
125
  { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
126
);
127
128
=head2 permission_module
129
130
Type: belongs_to
131
132
Related object: L<Koha::Schema::Result::PermissionModule>
133
134
=cut
135
136
__PACKAGE__->belongs_to(
137
  "permission_module",
138
  "Koha::Schema::Result::PermissionModule",
139
  { permission_module_id => "permission_module_id" },
140
  { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
141
);
142
143
144
# Created by DBIx::Class::Schema::Loader v0.07039 @ 2015-07-17 12:21:37
145
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:WaapKkhLT6DkqDZGVFvbQg
146
147
148
# You can replace this text with custom code or comments, and it will be preserved on regeneration
149
1;
(-)a/Koha/Schema/Result/PermissionModule.pm (+119 lines)
Line 0 Link Here
1
use utf8;
2
package Koha::Schema::Result::PermissionModule;
3
4
# Created by DBIx::Class::Schema::Loader
5
# DO NOT MODIFY THE FIRST PART OF THIS FILE
6
7
=head1 NAME
8
9
Koha::Schema::Result::PermissionModule
10
11
=cut
12
13
use strict;
14
use warnings;
15
16
use base 'DBIx::Class::Core';
17
18
=head1 TABLE: C<permission_modules>
19
20
=cut
21
22
__PACKAGE__->table("permission_modules");
23
24
=head1 ACCESSORS
25
26
=head2 permission_module_id
27
28
  data_type: 'integer'
29
  is_auto_increment: 1
30
  is_nullable: 0
31
32
=head2 module
33
34
  data_type: 'varchar'
35
  is_nullable: 0
36
  size: 32
37
38
=head2 description
39
40
  data_type: 'varchar'
41
  is_nullable: 1
42
  size: 255
43
44
=cut
45
46
__PACKAGE__->add_columns(
47
  "permission_module_id",
48
  { data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
49
  "module",
50
  { data_type => "varchar", is_nullable => 0, size => 32 },
51
  "description",
52
  { data_type => "varchar", is_nullable => 1, size => 255 },
53
);
54
55
=head1 PRIMARY KEY
56
57
=over 4
58
59
=item * L</permission_module_id>
60
61
=back
62
63
=cut
64
65
__PACKAGE__->set_primary_key("permission_module_id");
66
67
=head1 UNIQUE CONSTRAINTS
68
69
=head2 C<module>
70
71
=over 4
72
73
=item * L</module>
74
75
=back
76
77
=cut
78
79
__PACKAGE__->add_unique_constraint("module", ["module"]);
80
81
=head1 RELATIONS
82
83
=head2 borrower_permissions
84
85
Type: has_many
86
87
Related object: L<Koha::Schema::Result::BorrowerPermission>
88
89
=cut
90
91
__PACKAGE__->has_many(
92
  "borrower_permissions",
93
  "Koha::Schema::Result::BorrowerPermission",
94
  { "foreign.permission_module_id" => "self.permission_module_id" },
95
  { cascade_copy => 0, cascade_delete => 0 },
96
);
97
98
=head2 permissions
99
100
Type: has_many
101
102
Related object: L<Koha::Schema::Result::Permission>
103
104
=cut
105
106
__PACKAGE__->has_many(
107
  "permissions",
108
  "Koha::Schema::Result::Permission",
109
  { "foreign.module" => "self.module" },
110
  { cascade_copy => 0, cascade_delete => 0 },
111
);
112
113
114
# Created by DBIx::Class::Schema::Loader v0.07039 @ 2015-07-17 12:21:37
115
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:qc8JEcG/PXIlFu44MB+ouQ
116
117
118
# You can replace this text with custom code or comments, and it will be preserved on regeneration
119
1;
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt (-1 / +3 lines)
Lines 43-49 Link Here
43
<form action="[% url %]" method="post" name="loginform" id="loginform">
43
<form action="[% url %]" method="post" name="loginform" id="loginform">
44
    <input type="hidden" name="koha_login_context" value="intranet" />
44
    <input type="hidden" name="koha_login_context" value="intranet" />
45
[% FOREACH INPUT IN INPUTS %]
45
[% FOREACH INPUT IN INPUTS %]
46
    <input type="hidden" name="[% INPUT.name |html %]" value="[% INPUT.value |html %]" />
46
	[% UNLESS INPUT.name == 'logout.x' #No reason to send the logout-signal again %]
47
        <input type="hidden" name="[% INPUT.name |html %]" value="[% INPUT.value |html %]" />
48
	[% END %]
47
[% END %]
49
[% END %]
48
<p><label for="userid">Username:</label>
50
<p><label for="userid">Username:</label>
49
<input type="text" name="userid" id="userid" class="input focus" value="[% userid %]" size="20" tabindex="1" />
51
<input type="text" name="userid" id="userid" class="input focus" value="[% userid %]" size="20" tabindex="1" />
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt (-1 / +3 lines)
Lines 142-148 Link Here
142
                                <input type="hidden" name="koha_login_context" value="opac" />
142
                                <input type="hidden" name="koha_login_context" value="opac" />
143
                                <fieldset class="brief">
143
                                <fieldset class="brief">
144
                                    [% FOREACH INPUT IN INPUTS %]
144
                                    [% FOREACH INPUT IN INPUTS %]
145
                                        <input type="hidden" name="[% INPUT.name |html %]" value="[% INPUT.value |html %]" />
145
                                        [% UNLESS INPUT.name == 'logout.x' #No reason to send the logout-signal again %]
146
                                            <input type="hidden" name="[% INPUT.name |html %]" value="[% INPUT.value |html %]" />
147
                                        [% END %]
146
                                    [% END %]
148
                                    [% END %]
147
                                    <label for="userid">Login</label>
149
                                    <label for="userid">Login</label>
148
                                    <input type="text"  size="25" id="userid"  name="userid" />
150
                                    <input type="text"  size="25" id="userid"  name="userid" />
(-)a/opac/opac-search-history.pl (-1 lines)
Lines 41-47 my ($template, $loggedinuser, $cookie) = get_template_and_user( Link Here
41
        query => $cgi,
41
        query => $cgi,
42
        type => "opac",
42
        type => "opac",
43
        authnotrequired => 1,
43
        authnotrequired => 1,
44
        flagsrequired => {borrowers => 1},
45
        debug => 1,
44
        debug => 1,
46
    }
45
    }
47
);
46
);
(-)a/opac/opac-user.pl (-1 / +1 lines)
Lines 349-355 foreach my $res (@reserves) { Link Here
349
$template->param( WAITING => \@waiting );
349
$template->param( WAITING => \@waiting );
350
350
351
# current alert subscriptions
351
# current alert subscriptions
352
my $alerts = getalert($borrowernumber) if $borrowernumber;
352
my $alerts = getalert($borrowernumber) if $borrowernumber; #Superuser has no borrowernumber
353
foreach ( @$alerts ) {
353
foreach ( @$alerts ) {
354
    $_->{ $_->{type} } = 1;
354
    $_->{ $_->{type} } = 1;
355
    $_->{relatedto} = findrelatedto( $_->{type}, $_->{externalid} );
355
    $_->{relatedto} = findrelatedto( $_->{type}, $_->{externalid} );
(-)a/t/db_dependent/Koha/Borrower.t (-1 / +55 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# Copyright 2015 Open Source Freedom Fighters
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Test::More; #Please don't set the test count here. It is nothing but trouble when rebasing against master and is of dubious help.
21
22
use Koha::Borrower;
23
24
25
26
testIsSuperuser();
27
28
29
30
31
32
################################################################################
33
#### Define test subroutines here ##############################################
34
################################################################################
35
36
=head testIsSuperuser
37
@UNIT_TEST
38
Tests Koha::Borrower->isSuperuser()
39
=cut
40
41
sub testIsSuperuser {
42
    my $borrower = Koha::Borrower->new();
43
    ok((not(defined($borrower->isSuperuser()))), "isSuperuser(): By default user is not defined as superuser.");
44
    ok(($borrower->isSuperuser(1) == 1), "isSuperuser(): Setting user as superuser returns 1.");
45
    ok(($borrower->isSuperuser() == 1), "isSuperuser(): Getting superuser status from a superuser returns 1.");
46
    ok((not(defined($borrower->isSuperuser(0)))), "isSuperuser(): Removing superuser status from a superuser OK and returns undef");
47
    ok((not(defined($borrower->isSuperuser()))), "isSuperuser(): Ex-superuser superuser status is undef");
48
}
49
50
51
52
53
#######################
54
done_testing(); #YAY!!
55
#######################

Return to bug 7174