Bugzilla – Attachment 41413 Details for
Bug 7174
Authentication rewriting
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 7174 - Authentication Rewrite - Core
Bug-7174---Authentication-Rewrite---Core.patch (text/plain), 66.06 KB, created by
Olli-Antti Kivilahti
on 2015-08-06 15:40:27 UTC
(
hide
)
Description:
Bug 7174 - Authentication Rewrite - Core
Filename:
MIME Type:
Creator:
Olli-Antti Kivilahti
Created:
2015-08-06 15:40:27 UTC
Size:
66.06 KB
patch
obsolete
>From 80747c3c50188ff01170f3aae7f0e01869b6f14d Mon Sep 17 00:00:00 2001 >From: Olli-Antti Kivilahti <olli-antti.kivilahti@jns.fi> >Date: Mon, 3 Aug 2015 15:10:20 +0300 >Subject: [PATCH] Bug 7174 - Authentication Rewrite - Core > >Depends heavily on Buugg 14540, which introduces many tests to spot regression >caused by this feature. > >Introduces a new modular authentication system based on Exception signaling >and reusable authentication components/challenges. >This system is cross-framework -compatible, currently supporting Mojolicious >and CGI, but adding support for any other framework is straightforward. > >Central idea is to provide a single authentication function to deal with all >types of authentication scenarios, REST API, Cookie, Password, LDAP... >Also it is important to make a system that is easy to extend and can deal with >many future authentication scenarios. > >Currently only CGI password, LDAP and cookie login is tested. Legacy behaviour >is used to deal with other types of authentication. > >See the attached schematic in Bugzilla for a more architectural overview. > >Also fixes few bugs related to anonymous_search_history > >The basic principle is: >1. We get an authentication request from any Web/GUI-framework, for ex CGI. >2. Request is normalized by extracting all necessary authentication data > elements to a separate data structure. >3. Based on the found authentication data elements, system decides which > authentication route to take. >4. Route implements all challenges needed to authenticate the request. > Route returns the authenticated Koha::Borrower or an Exception if login > failed. >5. The user environment/session is set/deleted based on the Route result. >6. a Koha::Borrower and the CGISESSID-cookie is returned to the calling > framework in the format the framework needs. >7. Framework needs to catch possible exceptions and deal with them. > Eg. login failed, no permission, under maintenance. >8. Authentication succeeds and session is set, or failure is reported to user. >--- > Koha/Auth.pm | 227 +++++++++++++++++++++ > Koha/Auth/Challenge.pm | 74 +++++++ > Koha/Auth/Challenge/Cookie.pm | 83 ++++++++ > .../Challenge/IndependentBranchesAutolocation.pm | 52 +++++ > Koha/Auth/Challenge/OPACMaintenance.pm | 44 ++++ > Koha/Auth/Challenge/Password.pm | 127 ++++++++++++ > Koha/Auth/Challenge/Permission.pm | 42 ++++ > Koha/Auth/Challenge/RESTV1.pm | 169 +++++++++++++++ > Koha/Auth/Challenge/Version.pm | 56 +++++ > Koha/Auth/RequestNormalizer.pm | 166 +++++++++++++++ > Koha/Auth/Route.pm | 75 +++++++ > Koha/Auth/Route/Cookie.pm | 44 ++++ > Koha/Auth/Route/Password.pm | 46 +++++ > Koha/Auth/Route/RESTV1.pm | 43 ++++ > Koha/AuthUtils.pm | 65 +++++- > Koha/Borrower.pm | 26 +++ > Koha/Schema/Result/BorrowerPermission.pm | 149 ++++++++++++++ > Koha/Schema/Result/PermissionModule.pm | 119 +++++++++++ > koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt | 4 +- > .../opac-tmpl/bootstrap/en/modules/opac-auth.tt | 4 +- > opac/opac-search-history.pl | 1 - > opac/opac-user.pl | 2 +- > t/db_dependent/Koha/Borrower.t | 55 +++++ > 23 files changed, 1668 insertions(+), 5 deletions(-) > create mode 100644 Koha/Auth.pm > create mode 100644 Koha/Auth/Challenge.pm > create mode 100644 Koha/Auth/Challenge/Cookie.pm > create mode 100644 Koha/Auth/Challenge/IndependentBranchesAutolocation.pm > create mode 100644 Koha/Auth/Challenge/OPACMaintenance.pm > create mode 100644 Koha/Auth/Challenge/Password.pm > create mode 100644 Koha/Auth/Challenge/Permission.pm > create mode 100644 Koha/Auth/Challenge/RESTV1.pm > create mode 100644 Koha/Auth/Challenge/Version.pm > create mode 100644 Koha/Auth/RequestNormalizer.pm > create mode 100644 Koha/Auth/Route.pm > create mode 100644 Koha/Auth/Route/Cookie.pm > create mode 100644 Koha/Auth/Route/Password.pm > create mode 100644 Koha/Auth/Route/RESTV1.pm > create mode 100644 Koha/Schema/Result/BorrowerPermission.pm > create mode 100644 Koha/Schema/Result/PermissionModule.pm > create mode 100644 t/db_dependent/Koha/Borrower.t > >diff --git a/Koha/Auth.pm b/Koha/Auth.pm >new file mode 100644 >index 0000000..d8509ba >--- /dev/null >+++ b/Koha/Auth.pm >@@ -0,0 +1,227 @@ >+package Koha::Auth; >+ >+# Copyright 2015 Vaara-kirjastot >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+#Define common packages >+use Modern::Perl; >+use Scalar::Util qw(blessed); >+use Try::Tiny; >+ >+#Define Koha packages >+use Koha::Auth::RequestNormalizer; >+use Koha::Auth::Route::Password; >+use Koha::Auth::Route::Cookie; >+use Koha::Auth::Route::RESTV1; >+ >+#Define Exceptions >+use Koha::Exception::BadParameter; >+use Koha::Exception::Logout; >+use Koha::Exception::UnknownProgramState; >+ >+use C4::Branch; >+ >+#Define the headers, POST-parameters and cookies extracted from the various web-frameworks' >+# request-objects and passed to the authentication system as normalized values. >+our @authenticationHeaders = ('X-Koha-Date', 'Authorization'); >+our @authenticationPOSTparams = ('password', 'userid', 'PT', 'branch', 'logout.x', 'koha_login_context'); >+our @authenticationCookies = ('CGISESSID'); #Really we should have only one of these. >+ >+=head authenticate >+ >+@PARAM3 HASHRef of authentication directives. Supported values: >+ 'inOPAC' => 1, #Authentication context is in OPAC >+ 'inREST' => 'v1', #Authentication context is in REST API V1 >+ 'inSC' => 1, #Authentication context is in the staff client >+ 'authnotrequired' => 1, #Disregard all Koha::Exception::LoginFailed||NoPermission-exceptions, >+ #and authenticate as an anonymous user if normal authentication >+ #fails. >+@THROWS Koha::Exception::VersionMismatch >+ Koha::Exception::BadSystemPreference >+ Koha::Exception::BadParameter >+ Koha::Exception::ServiceTemporarilyUnavailable >+ Koha::Exception::LoginFailed >+ Koha::Exception::NoPermission >+ Koha::Exception::Logout, catch this and redirect the request to the logout page. >+=cut >+ >+sub authenticate { >+ my ($controller, $permissions, $authParams) = @_; >+ my $rae = _authenticate_validateAndNormalizeParameters(@_); #Get the normalized request authentication elements >+ >+ my $borrower; #Each authentication route returns a Koha::Borrower-object on success. We use this to generate the Context() >+ >+ ##Select the Authentication route. >+ ##Routes are introduced in priority order, and if one matches, the other routes are ignored. >+ try { >+ #0. Logout >+ if ($rae->{postParams}->{'logout.x'}) { >+ clearUserEnvironment($rae, $authParams); >+ Koha::Exception::Logout->throw(error => "User logged out. Please redirect me!"); >+ } >+ #1. Check for password authentication, including LDAP. >+ elsif ($rae->{postParams}->{koha_login_context} && $rae->{postParams}->{userid} && $rae->{postParams}->{password}) { >+ $borrower = Koha::Auth::Route::Password::challenge($rae, $permissions, $authParams); >+ } >+ #2. Check for REST's signature-based authentication. >+ #elsif ($rae->{headers}->{'Authorization'} && $rae->{headers}->{'Authorization'} =~ /Koha/) { >+ elsif ($rae->{headers}->{'Authorization'}) { >+ $borrower = Koha::Auth::Route::RESTV1::challenge($rae, $permissions, $authParams); >+ } >+ #3. Check for the cookie. If cookies go stale, they block all subsequent authentication methods, so keep it down on this list. >+ elsif ($rae->{cookies}->{CGISESSID}) { >+ $borrower = Koha::Auth::Route::Cookie::challenge($rae, $permissions, $authParams); >+ } >+ else { #HTTP CAS ticket or shibboleth or Persona not implemented >+ #We don't know how to authenticate, or there is no authentication attempt. >+ Koha::Exception::LoginFailed->throw(error => "Koha doesn't understand your authentication protocol."); >+ } >+ } catch { >+ if (blessed($_)) { >+ if ($_->isa('Koha::Exception::LoginFailed') || $_->isa('Koha::Exception::NoPermission')) { >+ if ($authParams->{authnotrequired}) { #We failed to login, but we can continue anonymously. >+ $borrower = Koha::Borrower->new(); >+ } >+ else { >+ $_->rethrow(); #Anonymous login not allowed this time >+ } >+ } >+ else { >+ die $_; #Propagate other errors to the calling Controller to redirect as it wants. >+ } >+ } >+ else { >+ die $_; #Not a Koha::Exception-object >+ } >+ }; >+ >+ my $session = setUserEnvironment($controller, $rae, $borrower, $authParams); >+ my $cookie = Koha::Auth::RequestNormalizer::getSessionCookie($controller, $session); >+ >+ return ($borrower, $cookie); >+} >+ >+=head _authenticate_validateAndNormalizeParameters >+ >+@PARAM1 CGI- or Mojolicious::Controller-object, this is used to identify which web framework to use. >+@PARAM2 HASHRef or undef, Permissions HASH telling which Koha permissions the user must have, to access the resource. >+@PARAM3 HASHRef or undef, Special authentication parameters, see authenticate() >+@THROWS Koha::Exception::BadParameter, if validating parameters fails. >+=cut >+ >+sub _authenticate_validateAndNormalizeParameters { >+ my ($controller, $permissions, $authParams) = @_; >+ >+ #Validate $controller. >+ my $requestAuthElements; >+ if (blessed($controller) && $controller->isa('CGI')) { >+ $requestAuthElements = Koha::Auth::RequestNormalizer::normalizeCGI($controller, \@authenticationHeaders, \@authenticationPOSTparams, \@authenticationCookies); >+ } >+ elsif (blessed($controller) && $controller->isa('Mojolicious::Controller')) { >+ $requestAuthElements = Koha::Auth::RequestNormalizer::normalizeMojolicious($controller, \@authenticationHeaders, \@authenticationPOSTparams, \@authenticationCookies); >+ } >+ else { >+ Koha::Exception::BadParameter->throw(error => "Koha::Auth::authenticate():> The first parameter MUST be either a 'CGI'-object or a 'Mojolicious::Controller'-object"); >+ } >+ #Validate $permissions >+ unless (not($permissions) || (ref $permissions eq 'HASH')) { >+ Koha::Exception::BadParameter->throw(error => "Koha::Auth::authenticate():> The second parameter MUST be 'undef' or a HASHRef of Koha permissions. See C4::Auth::haspermission()."); >+ } >+ #Validate $authParams >+ unless (not($authParams) || (ref $authParams eq 'HASH')) { >+ Koha::Exception::BadParameter->throw(error => "Koha::Auth::authenticate():> The third parameter MUST be 'undef' or a HASHRef."); >+ } >+ >+ return $requestAuthElements; >+} >+ >+=head setUserEnvironment >+Set the C4::Context::user_env() and CGI::Session. >+ >+Any idea why there is both the CGI::Session and C4::Context::usernenv?? >+=cut >+ >+sub setUserEnvironment { >+ my ($controller, $rae, $borrower, $authParams) = @_; >+ >+ my $session = C4::Auth::get_session( $rae->{cookies}->{CGISESSID} || '' ); >+ C4::Context->_new_userenv( $session->id ); >+ >+ _determineUserBranch($rae, $borrower, $authParams, $session); >+ >+ #Then start setting remaining session parameters >+ $session->param( 'number', $borrower->borrowernumber ); >+ $session->param( 'id', $borrower->userid ); >+ $session->param( 'cardnumber', $borrower->cardnumber ); >+ $session->param( 'firstname', $borrower->firstname ); >+ $session->param( 'surname', $borrower->surname ); >+ $session->param( 'emailaddress', $borrower->email ); >+ $session->param( 'ip', $session->remote_addr() ); >+ $session->param( 'lasttime', time() ); >+ >+ #Finally configure the userenv. >+ C4::Context->set_userenv( >+ $session->param('number'), $session->param('id'), >+ $session->param('cardnumber'), $session->param('firstname'), >+ $session->param('surname'), $session->param('branch'), >+ $session->param('branchname'), undef, >+ $session->param('emailaddress'), $session->param('branchprinter'), >+ $session->param('persona'), $session->param('shibboleth') >+ ); >+ >+ return $session; >+} >+ >+sub _determineUserBranch { >+ my ($rae, $borrower, $authParams, $session) = @_; >+ >+ my ($branchcode, $branchname); >+ if ($rae->{postParams}->{branch}) { >+ #We are instructed to change the active branch >+ $branchcode = $rae->{postParams}->{branch}; >+ } >+ elsif ($session->param('branch') && $session->param('branch') ne 'NO_LIBRARY_SET') { >+ ##Branch is already set >+ $branchcode = $session->param('branch'); >+ } >+ elsif ($borrower->branchcode) { >+ #Default to the borrower's branch >+ $branchcode = $borrower->branchcode; >+ } >+ else { >+ #No borrower branch? This must be the superuser. >+ $branchcode = 'NO_LIBRARY_SET'; >+ $branchname = 'NO_LIBRARY_SET'; >+ } >+ $session->param( 'branch', $branchcode ); >+ $session->param( 'branchname', ($branchname || C4::Branch::GetBranchName($branchcode) || 'NO_LIBRARY_SET')); >+} >+ >+=head clearUserEnvironment >+ >+Removes all active authentications >+=cut >+ >+sub clearUserEnvironment { >+ my ($rae, $authParams) = @_; >+ >+ my $session = C4::Auth::get_session( $rae->{cookies}->{CGISESSID} ); >+ $session->delete(); >+ $session->flush; >+ #Do we need to unset this if it has never been set? C4::Context::_unset_userenv( $rae->{cookies}->{CGISESSID} ); >+} >+1; >\ No newline at end of file >diff --git a/Koha/Auth/Challenge.pm b/Koha/Auth/Challenge.pm >new file mode 100644 >index 0000000..02c45f1 >--- /dev/null >+++ b/Koha/Auth/Challenge.pm >@@ -0,0 +1,74 @@ >+package Koha::Auth::Challenge; >+ >+# Copyright 2015 Vaara-kirjastot >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+=head1 NAME Koha::Auth::Challenge >+ >+=head2 SYNOPSIS >+ >+This is a authentication challenge parent class. >+All Challenge-objects must implement the challenge()-method. >+ >+=head SUBLASSING >+ >+package Koha::Auth::Challenge::YetAnotherChallenge; >+ >+use base qw('Koha::Auth::Challenge'); >+ >+sub challenge { >+ #Implement the parent method to make this subclass interoperable. >+} >+ >+=head2 USAGE >+ >+ use Scalar::Util qw(blessed); >+ try { >+ ... >+ Koha::Auth::Challenge::Version::challenge(); >+ Koha::Auth::Challenge::OPACMaintenance::challenge(); >+ Koha::Auth::Challenge::YetAnotherChallenge::challenge(); >+ ... >+ } catch { >+ if (blessed($_)) { >+ if ($_->isa('Koha::Exception::VersionMismatch')) { >+ ##handle exception >+ } >+ elsif ($_->isa('Koha::Exception::AnotherKindOfException')) { >+ ... >+ } >+ ... >+ else { >+ warn "Unknown exception class ".ref($_)."\n"; >+ die $_; #Unhandled exception case >+ } >+ } >+ else { >+ die $_; #Not a Koha::Exception-object >+ } >+ }; >+ >+=cut >+ >+sub challenge { >+ #@OVERLOAD this "interface" >+ warn caller()." doesn't implement challenge()\n"; >+} >+ >+1; >diff --git a/Koha/Auth/Challenge/Cookie.pm b/Koha/Auth/Challenge/Cookie.pm >new file mode 100644 >index 0000000..5f7d3fa >--- /dev/null >+++ b/Koha/Auth/Challenge/Cookie.pm >@@ -0,0 +1,83 @@ >+package Koha::Auth::Challenge::Cookie; >+ >+# Copyright 2015 Vaara-kirjastot >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use C4::Context; >+use C4::Auth; >+use Koha::AuthUtils; >+use Koha::Borrowers; >+ >+use Koha::Exception::LoginFailed; >+ >+use base qw(Koha::Auth::Challenge); >+ >+=head challenge >+STATIC >+ >+ Koha::Auth::Challenge::Cookie::challenge($cookieValue); >+ >+Checks if the given authentication cookie value matches a session, and checks if >+the session is still active. >+@PARAM1 String, hashed session key identifying a session in koha.sessions >+@RETURNS Koha::Borrower matching the verified and active session >+@THROWS Koha::Exception::LoginFailed, if no session is found, >+ if the session has expired, >+ if the session IP address changes, >+ if no borrower was found for the session >+=cut >+ >+sub challenge { >+ my ($cookie, $originIps) = @_; >+ >+ my $session = C4::Auth::get_session($cookie); >+ Koha::Exception::LoginFailed->throw(error => "No session matching the given session identifier '$session'.") unless $session; >+ >+ # See if the given session is timed out >+ if ( ($session->param('lasttime') || 0) < (time()- C4::Auth::_timeout_syspref()) ) { >+ $session->delete(); >+ $session->flush; >+ C4::Context::_unset_userenv($cookie); >+ Koha::Exception::LoginFailed->throw(error => "Session expired, please login again."); >+ } >+ # Check if we still access using the same IP than when the session was initialized. >+ elsif ( C4::Context->preference('SessionRestrictionByIP')) { >+ >+ my $sameIpFound = grep {$session->param('ip') eq $_} @$originIps; >+ >+ unless ($sameIpFound) { >+ $session->delete(); >+ $session->flush; >+ C4::Context::_unset_userenv($cookie); >+ Koha::Exception::LoginFailed->throw(error => "Session's client address changed, please login again."); >+ } >+ } >+ >+ #Get the Borrower-object >+ my $userid = $session->param('id'); >+ my $borrower = Koha::AuthUtils::checkKohaSuperuserFromUserid($userid); >+ $borrower = Koha::Borrowers->find({userid => $userid}) if not($borrower) && $userid; >+ Koha::Exception::LoginFailed->throw(error => "Cookie authentication succeeded, but no borrower found with userid '".($userid || '')."'.") >+ unless $borrower; >+ >+ $session->param( 'lasttime', time() ); >+ return $borrower; >+} >+ >+1; >diff --git a/Koha/Auth/Challenge/IndependentBranchesAutolocation.pm b/Koha/Auth/Challenge/IndependentBranchesAutolocation.pm >new file mode 100644 >index 0000000..b01e452 >--- /dev/null >+++ b/Koha/Auth/Challenge/IndependentBranchesAutolocation.pm >@@ -0,0 +1,52 @@ >+package Koha::Auth::Challenge::IndependentBranchesAutolocation; >+ >+# Copyright 2015 Vaara-kirjastot >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use C4::Context; >+use C4::Branch; >+ >+use Koha::Exception::LoginFailed; >+ >+use base qw(Koha::Auth::Challenge); >+ >+=head challenge >+ >+If sysprefs 'IndependentBranches' and 'Autolocation' are active, checks if the user >+is in the correct network region to login. >+@PARAM1 String, branchcode of the branch the current user is authenticating in to. >+@THROWS Koha::Exception::LoginFailed, if the user is in the wrong network segment. >+=cut >+ >+sub challenge { >+ my ($currentBranchcode) = @_; >+ >+ if ( $currentBranchcode && C4::Context->boolean_preference('IndependentBranches') && C4::Context->boolean_preference('Autolocation') ) { >+ my $ip = $ENV{'REMOTE_ADDR'}; >+ >+ my $branches = C4::Branch::GetBranches(); >+ # we have to check they are coming from the right ip range >+ my $domain = $branches->{$currentBranchcode}->{'branchip'}; >+ if ( $ip !~ /^$domain/ ) { >+ Koha::Exception::LoginFailed->throw(error => "Branch '$currentBranchcode' is inaccessible from this network."); >+ } >+ } >+} >+ >+1; >diff --git a/Koha/Auth/Challenge/OPACMaintenance.pm b/Koha/Auth/Challenge/OPACMaintenance.pm >new file mode 100644 >index 0000000..8490379 >--- /dev/null >+++ b/Koha/Auth/Challenge/OPACMaintenance.pm >@@ -0,0 +1,44 @@ >+package Koha::Auth::Challenge::OPACMaintenance; >+ >+# Copyright 2015 Vaara-kirjastot >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use C4::Context; >+ >+use base qw(Koha::Auth::Challenge); >+ >+use Koha::Exception::ServiceTemporarilyUnavailable; >+ >+=head challenge >+STATIC >+ >+ Koha::Auth::Challenge::OPACMaintenance::challenge(); >+ >+Checks if OPAC is under maintenance. >+ >+@THROWS Koha::Exception::ServiceTemporarilyUnavailable >+=cut >+ >+sub challenge { >+ if ( C4::Context->preference('OpacMaintenance') ) { >+ Koha::Exception::ServiceTemporarilyUnavailable->throw(error => 'OPAC is under maintenance'); >+ } >+} >+ >+1; >diff --git a/Koha/Auth/Challenge/Password.pm b/Koha/Auth/Challenge/Password.pm >new file mode 100644 >index 0000000..7a423a2 >--- /dev/null >+++ b/Koha/Auth/Challenge/Password.pm >@@ -0,0 +1,127 @@ >+package Koha::Auth::Challenge::Password; >+ >+# Copyright 2015 Vaara-kirjastot >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Koha::Borrowers; >+use Koha::AuthUtils; >+ >+use base qw(Koha::Auth::Challenge); >+ >+use Koha::Exception::LoginFailed; >+ >+our @usernameAliasColumns = ('userid', 'cardnumber'); #Possible columns to treat as the username when authenticating. Must be UNIQUE in DB. >+ >+=head NAME Koha::Auth::Challenge::Password >+ >+=head SYNOPSIS >+ >+This module implements the more specific behaviour of the password authentication component. >+ >+=cut >+ >+=head challenge >+STATIC >+ >+ Koha::Auth::Challenge::Password::challenge(); >+ >+@RETURN Koha::Borrower-object if check succeedes, otherwise throws exceptions. >+@THROWS Koha::Exception::LoginFailed from Koha::AuthUtils password checks. >+=cut >+ >+sub challenge { >+ my ($userid, $password) = @_; >+ >+ my $borrower; >+ if (C4::Context->config('useldapserver')) { >+ $borrower = Koha::Auth::Challenge::Password::checkLDAPPassword($userid, $password); >+ return $borrower if $borrower; >+ } >+ if (C4::Context->preference('casAuthentication')) { >+ 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 :)"); >+ } >+ if (C4::Context->config('useshibboleth')) { >+ 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 :)"); >+ } >+ >+ return Koha::Auth::Challenge::Password::checkKohaPassword($userid, $password); >+} >+ >+=head checkKohaPassword >+ >+ my $borrower = Koha::Auth::Challenge::Password::checkKohaPassword($userid, $password); >+ >+Checks if the given username and password match anybody in the Koha DB >+@PARAM1 String, user identifier, either the koha.borrowers.userid, or koha.borrowers.cardnumber >+@PARAM2 String, clear text password from the authenticating user >+@RETURN Koha::Borrower, if login succeeded. >+ Sets Koha::Borrower->isSuperuser() if the user is a superuser. >+@THROWS Koha::Exception::LoginFailed, if no matching password was found for all username aliases in Koha. >+=cut >+ >+sub checkKohaPassword { >+ my ($userid, $password) = @_; >+ my $borrower; #Find the borrower to return >+ >+ $borrower = Koha::AuthUtils::checkKohaSuperuser($userid, $password); >+ return $borrower if $borrower; >+ >+ my $usernameFound = 0; #Report to the user if userid/barcode was found, even if the login failed. >+ #Check for each username alias if we can confirm a login with that. >+ for my $unameAlias (@usernameAliasColumns) { >+ my $borrower = Koha::Borrowers->find({$unameAlias => $userid}); >+ if ( $borrower ) { >+ $usernameFound = 1; >+ return $borrower if ( Koha::AuthUtils::checkHash( $password, $borrower->password ) ); >+ } >+ } >+ >+ Koha::Exception::LoginFailed->throw(error => "Password authentication failed for the given ".( ($usernameFound) ? "password" : "username and password")."."); >+} >+ >+=head checkLDAPPassword >+ >+Checks if the given username and password match anybody in the LDAP service >+@PARAM1 String, user identifier >+@PARAM2 String, clear text password from the authenticating user >+@RETURN Koha::Borrower, or >+ undef if we couldn't reliably contact the LDAP server so we should >+ fallback to local Koha Password authentication. >+@THROWS Koha::Exception::LoginFailed, if LDAP login failed >+=cut >+ >+sub checkLDAPPassword { >+ my ($userid, $password) = @_; >+ >+ #Lazy load dependencies because somebody might never need them. >+ require C4::Auth_with_ldap; >+ >+ my ($retval, $cardnumber, $local_userid) = C4::Auth_with_ldap::checkpw_ldap($userid, $password); # EXTERNAL AUTH >+ if ($retval == -1) { >+ Koha::Exception::LoginFailed->throw(error => "LDAP authentication failed for the given username and password"); >+ } >+ >+ if ($retval) { >+ my $borrower = Koha::Borrowers->find({userid => $local_userid}); >+ return $borrower; >+ } >+ return undef; >+} >+ >+1; >diff --git a/Koha/Auth/Challenge/Permission.pm b/Koha/Auth/Challenge/Permission.pm >new file mode 100644 >index 0000000..f284919 >--- /dev/null >+++ b/Koha/Auth/Challenge/Permission.pm >@@ -0,0 +1,42 @@ >+package Koha::Auth::Challenge::Permission; >+ >+# Copyright 2015 Vaara-kirjastot >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Koha::Auth::PermissionManager; >+ >+use base qw(Koha::Auth::Challenge); >+ >+=head challenge >+STATIC >+ >+ Koha::Auth::Challenge::Permission::challenge($borrower, $permissionsRequired); >+ >+@THROWS Koha::Exception::NoPermission with the missing permission if permissions >+ are inadequate >+=cut >+ >+sub challenge { >+ my ($borrower, $permissionsRequired) = @_; >+ >+ my $permissionManager = Koha::Auth::PermissionManager->new(); >+ $permissionManager->hasPermissions($borrower, $permissionsRequired); >+} >+ >+1; >diff --git a/Koha/Auth/Challenge/RESTV1.pm b/Koha/Auth/Challenge/RESTV1.pm >new file mode 100644 >index 0000000..8aa0660 >--- /dev/null >+++ b/Koha/Auth/Challenge/RESTV1.pm >@@ -0,0 +1,169 @@ >+package Koha::Auth::Challenge::RESTV1; >+ >+# Copyright 2015 Vaara-kirjastot >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+use DateTime::Format::HTTP; >+use DateTime; >+ >+use Koha::Borrowers; >+ >+use base qw(Koha::Auth::Challenge); >+ >+use Koha::Exception::LoginFailed; >+use Koha::Exception::BadParameter; >+ >+=head challenge >+ >+ my $borrower = Koha::Auth::Challenge::RESTV1::challenge(); >+ >+For authentication to succeed, the client have to send 2 HTTP >+headers: >+ - X-Koha-Date: the standard HTTP Date header complying to RFC 1123, simply wrapped to X-Koha-Date, >+ since the w3-specification forbids setting the Date-header from javascript. >+ - Authorization: the standard HTTP Authorization header, see below for how it is constructed. >+ >+=head2 HTTP Request example >+ >+GET /api/v1/borrowers/12 HTTP/1.1 >+Host: api.yourkohadomain.fi >+X-Koha-Date: Mon, 26 Mar 2007 19:37:58 +0000 >+Authorization: Koha admin69:frJIUN8DYpKDtOLCwo//yllqDzg= >+ >+=head2 Constructing the Authorization header >+ >+-You brand the authorization header with "Koha" >+-Then you give the userid/cardnumber of the user authenticating. >+-Then the hashed signature. >+ >+The signature is a HMAC-SHA256-HEX hash of several elements of the request, >+separated by spaces: >+ - HTTP method (uppercase) >+ - userid/cardnumber >+ - X-Koha-Date-header >+Signed with the Borrowers API key >+ >+The server then tries to rebuild the signature with each of the user's API keys. >+If one matches the received signature, then authentication is almost OK. >+ >+To avoid requests to be replayed, the last request's X-Koha-Date-header is stored >+in database and the authentication succeeds only if the stored Date >+is lesser than the X-Koha-Date-header. >+ >+=head2 Constructing the signature example >+ >+Signature = HMAC-SHA256-HEX("HTTPS" + " " + >+ "/api/v1/borrowers/12?howdoyoudo=voodoo" + " " + >+ "admin69" + " " + >+ "760818212" + " " + >+ "frJIUN8DYpKDtOLCwo//yllqDzg=" >+ ); >+ >+=head >+ >+@PARAM1 HASHRef of Header name => values >+@PARAM2 String, upper case request method name, eg. HTTP or HTTPS >+@PARAM3 String the request uri >+@RETURNS Koha::Borrower if authentication succeeded. >+@THROWS Koha::Exception::LoginFailed, if API key signature verification failed >+@THROWS Koha::Exception::BadParameter >+@THROWS Koha::Exception::UnknownObject, if we cannot find a Borrower with the given input. >+=cut >+ >+sub challenge { >+ my ($headers, $method, $uri) = @_; >+ >+ my $req_dt; >+ eval { >+ $req_dt = DateTime::Format::HTTP->parse_datetime( $headers->{'X-Koha-Date'} ); #Returns DateTime >+ }; >+ my $authorizationHeader = $headers->{'Authorization'}; >+ my ($req_username, $req_signature); >+ if ($authorizationHeader =~ /^Koha (\S+?):(\w+)$/) { >+ $req_username = $1; >+ $req_signature = $2; >+ } >+ else { >+ Koha::Exception::BadParameter->throw(error => "Authorization HTTP-header is not well formed. It needs to be of format 'Authorization: Koha userid:signature'"); >+ } >+ unless ($req_dt) { >+ 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'"); >+ } >+ >+ my $borrower = Koha::Borrowers->cast($req_username); >+ >+ my @apikeys = Koha::ApiKeys->search({ >+ borrowernumber => $borrower->borrowernumber, >+ active => 1, >+ }); >+ Koha::Exception::LoginFailed->throw(error => "User has no API keys. Please add one using the Staff interface or OPAC.") unless @apikeys; >+ >+ my $matchingApiKey; >+ foreach my $apikey (@apikeys) { >+ my $signature = makeSignature($method, $req_username, $headers->{'X-Koha-Date'}, $apikey); >+ >+ if ($signature eq $req_signature) { >+ $matchingApiKey = $apikey; >+ last(); >+ } >+ } >+ >+ unless ($matchingApiKey) { >+ Koha::Exception::LoginFailed->throw(error => "API key authentication failed"); >+ } >+ >+ unless ($matchingApiKey->last_request_time < $req_dt->epoch()) { >+ Koha::Exception::BadParameter->throw(error => "X-Koha-Date HTTP-header is stale, expected later date than '".DateTime::Format::HTTP->format_datetime($req_dt)."'"); >+ } >+ >+ $matchingApiKey->set({last_request_time => $req_dt->epoch()}); >+ $matchingApiKey->store(); >+ >+ return $borrower; >+} >+ >+sub makeSignature { >+ my ($method, $userid, $headerXKohaDate, $apiKey) = @_; >+ >+ my $message = join(' ', uc($method), $userid, $headerXKohaDate); >+ return Digest::SHA::hmac_sha256_hex($message, $apiKey->api_key); >+} >+ >+=head prepareAuthenticationHeaders >+@PARAM1 Koha::Borrower, to authenticate >+@PARAM2 DateTime, OPTIONAL, the timestamp of the HTTP request >+@PARAM3 HTTP verb, 'get', 'post', 'patch', 'put', ... >+@RETURNS HASHRef of authentication HTTP header names and their values. { >+ "X-Koha-Date" => "Mon, 26 Mar 2007 19:37:58 +0000", >+ "Authorization" => "Koha admin69:frJIUN8DYpKDtOLCwo//yllqDzg=", >+ } >+=cut >+ >+sub prepareAuthenticationHeaders { >+ my ($borrower, $dateTime, $method) = @_; >+ $borrower = Koha::Borrowers->cast($borrower); >+ >+ my $headerXKohaDate = DateTime::Format::HTTP->format_datetime( >+ ($dateTime || DateTime->now( time_zone => C4::Context->tz() )) >+ ); >+ my $headerAuthorization = "Koha ".$borrower->userid.":".makeSignature('get', $borrower->userid, $headerXKohaDate, $borrower->getApiKey('active')); >+ return {'X-Koha-Date' => $headerXKohaDate, >+ 'Authorization' => $headerAuthorization}; >+} >+ >+1; >diff --git a/Koha/Auth/Challenge/Version.pm b/Koha/Auth/Challenge/Version.pm >new file mode 100644 >index 0000000..94b2d7a >--- /dev/null >+++ b/Koha/Auth/Challenge/Version.pm >@@ -0,0 +1,56 @@ >+package Koha::Auth::Challenge::Version; >+ >+# Copyright 2015 Vaara-kirjastot >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use C4::Context; >+use Koha; >+ >+use base qw(Koha::Auth::Challenge); >+ >+use Koha::Exception::VersionMismatch; >+use Koha::Exception::BadSystemPreference; >+ >+=head challenge >+STATIC >+ >+ Koha::Auth::Challenge::Version::challenge(); >+ >+Checks if the DB version is valid. >+ >+@THROWS Koha::Exception::VersionMismatch, if versions do not match >+@THROWS Koha::Exception::BadSystemPreference, if "Version"-syspref is not set. >+ This probably means that Koha has not been installed yet. >+=cut >+ >+sub challenge { >+ my $versionSyspref = C4::Context->preference('Version'); >+ unless ( $versionSyspref ) { >+ Koha::Exception::BadSystemPreference->throw(error => "No Koha 'Version'-system preference defined. Koha needs to be installed."); >+ } >+ >+ my $kohaversion = Koha::version(); >+ # remove the 3 last . to have a Perl number >+ $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/; >+ if ( $versionSyspref < $kohaversion ) { >+ Koha::Exception::VersionMismatch->throw(error => "Database update needed. Database is 'v$versionSyspref' and Koha is 'v$kohaversion'"); >+ } >+} >+ >+1; >diff --git a/Koha/Auth/RequestNormalizer.pm b/Koha/Auth/RequestNormalizer.pm >new file mode 100644 >index 0000000..4d64ef3 >--- /dev/null >+++ b/Koha/Auth/RequestNormalizer.pm >@@ -0,0 +1,166 @@ >+package Koha::Auth::RequestNormalizer; >+ >+# Copyright 2015 Vaara-kirjastot >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Scalar::Util qw(blessed); >+ >+ >+=head normalizeCGI >+ >+Takes a CGI-object and finds the authentication markers from it. >+@PARAM1 CGI-object. >+@PARAM2 ARRAYRef, authentication headers that should be extracted for authentication >+@PARAM3 ARRAYRef, authentication POST parameters that should be extracted for authentication >+@PARAM4 ARRAYRef, authentication cookies that should be extracted for authentication >+@RETURNS List of : HASHRef of headers required for authentication, or undef >+ HASHRef of POST parameters required for authentication, or undef >+ HASHRef of the authenticaton cookie name => value, or undef >+=cut >+ >+sub normalizeCGI { >+ my ($controller, $authenticationHeaders, $authenticationPOSTparams, $authenticationCookies) = @_; >+ >+ my ($headers, $postParams, $cookies) = ({}, {}, {}); >+ foreach my $authHeader (@$authenticationHeaders) { >+ if (my $val = $controller->http($authHeader)) { >+ $headers->{$authHeader} = $val; >+ } >+ } >+ foreach my $authParam (@$authenticationPOSTparams) { >+ if (my $val = $controller->param($authParam)) { >+ $postParams->{$authParam} = $val; >+ } >+ } >+ foreach my $authCookie (@$authenticationCookies) { >+ if (my $val = $controller->cookie($authCookie)) { >+ $cookies->{$authCookie} = $val; >+ } >+ } >+ >+ my $method = $1 if ($ENV{SERVER_PROTOCOL} =~ /^(.+?)\//); >+ >+ my @originIps = ($ENV{'REMOTE_ADDR'}); >+ >+ my $requestAuthElements = { #Collect the authentication elements here. >+ headers => $headers, >+ postParams => $postParams, >+ cookies => $cookies, >+ originIps => \@originIps, >+ method => $method, >+ url => $ENV{REQUEST_URI}, >+ }; >+ return $requestAuthElements; >+} >+ >+=head normalizeMojolicious >+ >+Takes a Mojolicious::Controller-object and finds the authentication markers from it. >+@PARAM1 Mojolicious::Controller-object. >+@PARAM2-4 See normalizeCGI() >+@RETURNS HASHRef of the request's authentication elements marked for extraction, eg: >+ { >+ headers => { X-Koha-Signature => '32rFrFw3iojsev34AS', >+ X-Koha-Username => 'pavlov'}, >+ POSTparams => { password => '1234', >+ userid => 'pavlov'}, >+ cookies => { CGISESSID => '233FADFEV3as1asS' }, >+ method => 'https', >+ url => '/borrower/12/holds' >+ } >+=cut >+ >+sub normalizeMojolicious { >+ my ($controller, $authenticationHeaders, $authenticationPOSTparams, $authenticationCookies) = @_; >+ >+ my $request = $controller->req(); >+ my ($headers, $postParams, $cookies) = ({}, {}, {}); >+ my $headersHash = $request->headers()->to_hash(); >+ foreach my $authHeader (@$authenticationHeaders) { >+ if (my $val = $headersHash->{$authHeader}) { >+ $headers->{$authHeader} = $val; >+ } >+ } >+ foreach my $authParam (@$authenticationPOSTparams) { >+ if (my $val = $request->param($authParam)) { >+ $postParams->{$authParam} = $val; >+ } >+ } >+ >+ my $requestCookies = $request->cookies; >+ if (scalar(@$requestCookies)) { >+ foreach my $authCookieName (@$authenticationCookies) { >+ foreach my $requestCookie (@$requestCookies) { >+ if ($authCookieName eq $requestCookie->name) { >+ $cookies->{$authCookieName} = $requestCookie->value; >+ } >+ } >+ } >+ } >+ >+ my @originIps = ($controller->tx->original_remote_address()); >+ push @originIps, $request->headers()->header('X-Forwarded-For'); >+ >+ my $requestAuthElements = { #Collect the authentication elements here. >+ headers => $headers, >+ postParams => $postParams, >+ cookies => $cookies, >+ originIps => \@originIps, >+ method => $controller->req->method, >+ url => '/' . $controller->req->url->path_query, >+ }; >+ return $requestAuthElements; >+} >+ >+=head getSessionCookie >+ >+@PARAM1 CGI- or Mojolicious::Controller-object, this is used to identify which web framework to use. >+@PARAM2 CGI::Session. >+@RETURNS a Mojolicious cookie or a CGI::Cookie. >+=cut >+ >+sub getSessionCookie { >+ my ($controller, $session) = @_; >+ >+ my $cookie = { >+ name => 'CGISESSID', >+ value => $session->id, >+ }; >+ my $cookieOk; >+ >+ if (blessed($controller) && $controller->isa('CGI')) { >+ $cookie->{HttpOnly} = 1; >+ $cookieOk = $controller->cookie( $cookie ); >+ } >+ elsif (blessed($controller) && $controller->isa('Mojolicious::Controller')) { >+ $controller->res->cookies($cookie); >+ foreach my $c (@{$controller->res->cookies}) { >+ if ($c->name eq 'CGISESSID') { >+ $cookieOk = $c; >+ last; >+ } >+ } >+ } >+ unless ($cookieOk) { >+ Koha::Exception::UnknownProgramState->throw(error => __PACKAGE__."::getSessionCookie():> Unable to get a proper cookie?"); >+ } >+ return $cookieOk; >+} >+ >+1; >\ No newline at end of file >diff --git a/Koha/Auth/Route.pm b/Koha/Auth/Route.pm >new file mode 100644 >index 0000000..c2cf3b9 >--- /dev/null >+++ b/Koha/Auth/Route.pm >@@ -0,0 +1,75 @@ >+package Koha::Auth::Route; >+ >+use Modern::Perl; >+ >+# Copyright 2015 Vaara-kirjastot >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+=head >+ >+=NAME Koha::Auth::Route >+ >+=SYNOPSIS >+ >+This is an interface definition for Koha::Auth::Route::* -subclasses. >+This documentation explains how to subclass different routes. >+ >+=USAGE >+ >+ if ($userid && $password) { >+ $borrower = Koha::Auth::Route::<RouteName>::challenge($requestAuthElements, $permissionsRequired, $routeParams); >+ } >+ >+=head INPUT >+ >+Each Route gets three parameters: >+ $requestAuthElements, HASHRef of HASHRefs: >+ headers => HASHRef of HTTP Headers matching the @authenticationHeaders-package >+ variable in Koha::Auth, >+ Eg. { 'X-Koha-Signature' => "23in4ow2gas2opcnpa", ... } >+ postParams => HASHRef of HTTP POST parameters matching the >+ @authenticationPOSTparams-package variable in Koha::Auth, >+ Eg. { password => '1234', 'userid' => 'admin'} >+ cookies => HASHRef of HTTP Cookies matching the >+ @authenticationPOSTparams-package variable in Koha::Auth, >+ EG. { CGISESSID => '9821rj1kn3tr9ff2of2ln1' } >+ $permissionsRequired: >+ HASHRef of Koha permissions. >+ See Koha::Auth::PermissionManager for example. >+ $routeParams: HASHRef of special Route-related data >+ {inOPAC => 1, authnotrequired => 0, ...} >+ >+=head OUTPUT >+ >+Each route must return a Koha::Borrower-object representing the authenticated user. >+Even if the login succeeds with a superuser or similar virtual user, like >+anonymous login, a mock Borrower-object must be returned. >+If the login fails, each route must throw Koha::Exceptions to notify the cause >+of the failure. >+ >+=head ROUTE STRUCTURE >+ >+Each route consists of Koha::Auth::Challenge::*-objects to test for various >+authentication challenges. >+ >+See. Koha::Auth::Challenge for more information. >+ >+=cut >+ >+sub challenge {}; #@OVERLOAD this "interface" >+ >+1; >diff --git a/Koha/Auth/Route/Cookie.pm b/Koha/Auth/Route/Cookie.pm >new file mode 100644 >index 0000000..9637a5b >--- /dev/null >+++ b/Koha/Auth/Route/Cookie.pm >@@ -0,0 +1,44 @@ >+package Koha::Auth::Route::Cookie; >+ >+# Copyright 2015 Vaara-kirjastot >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Koha::Auth::Challenge::OPACMaintenance; >+use Koha::Auth::Challenge::Version; >+use Koha::Auth::Challenge::Cookie; >+use Koha::Auth::Challenge::Permission; >+ >+use base qw(Koha::Auth::Route); >+ >+=head challenge >+See Koha::Auth::Route, for usage documentation. >+@THROWS Koha::Exceptions from authentication components. >+=cut >+ >+sub challenge { >+ my ($rae, $permissionsRequired, $routeParams) = @_; >+ >+ Koha::Auth::Challenge::OPACMaintenance::challenge() if $routeParams->{inOPAC}; >+ Koha::Auth::Challenge::Version::challenge(); >+ my $borrower = Koha::Auth::Challenge::Cookie::challenge($rae->{cookies}->{CGISESSID}, $rae->{originIps}); >+ Koha::Auth::Challenge::Permission::challenge($borrower, $permissionsRequired) if $permissionsRequired; >+ return $borrower; >+} >+ >+1; >diff --git a/Koha/Auth/Route/Password.pm b/Koha/Auth/Route/Password.pm >new file mode 100644 >index 0000000..7620d09 >--- /dev/null >+++ b/Koha/Auth/Route/Password.pm >@@ -0,0 +1,46 @@ >+package Koha::Auth::Route::Password; >+ >+# Copyright 2015 Vaara-kirjastot >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Koha::Auth::Challenge::OPACMaintenance; >+use Koha::Auth::Challenge::Version; >+use Koha::Auth::Challenge::IndependentBranchesAutolocation; >+use Koha::Auth::Challenge::Password; >+use Koha::Auth::Challenge::Permission; >+ >+use base qw(Koha::Auth::Route); >+ >+=head challenge >+See Koha::Auth::Route, for usage documentation. >+@THROWS Koha::Exceptions from authentication components. >+=cut >+ >+sub challenge { >+ my ($rae, $permissionsRequired, $routeParams) = @_; >+ >+ Koha::Auth::Challenge::OPACMaintenance::challenge() if $routeParams->{inOPAC}; >+ Koha::Auth::Challenge::Version::challenge(); >+ Koha::Auth::Challenge::IndependentBranchesAutolocation::challenge($routeParams->{branch}); >+ my $borrower = Koha::Auth::Challenge::Password::challenge($rae->{postParams}->{userid}, $rae->{postParams}->{password}); >+ Koha::Auth::Challenge::Permission::challenge($borrower, $permissionsRequired) if $permissionsRequired; >+ return $borrower; >+} >+ >+1; >diff --git a/Koha/Auth/Route/RESTV1.pm b/Koha/Auth/Route/RESTV1.pm >new file mode 100644 >index 0000000..64ac84f >--- /dev/null >+++ b/Koha/Auth/Route/RESTV1.pm >@@ -0,0 +1,43 @@ >+package Koha::Auth::Route::RESTV1; >+ >+# Copyright 2015 Vaara-kirjastot >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Koha::Auth::Challenge::Version; >+use Koha::Auth::Challenge::RESTV1; >+use Koha::Auth::Challenge::Permission; >+ >+use base qw(Koha::Auth::Route); >+ >+=head challenge >+See Koha::Auth::Route, for usage documentation. >+@THROWS Koha::Exceptions from authentication components. >+=cut >+ >+sub challenge { >+ my ($rae, $permissionsRequired, $routeParams) = @_; >+ >+ #Koha::Auth::Challenge::RESTMaintenance::challenge() if $routeParams->{inREST}; #NOT IMPLEMENTED YET >+ Koha::Auth::Challenge::Version::challenge(); >+ my $borrower = Koha::Auth::Challenge::RESTV1::challenge($rae->{headers}, $rae->{method}, $rae->{url}); >+ Koha::Auth::Challenge::Permission::challenge($borrower, $permissionsRequired) if $permissionsRequired; >+ return $borrower; >+} >+ >+1; >diff --git a/Koha/AuthUtils.pm b/Koha/AuthUtils.pm >index 7d01e28..8845664 100644 >--- a/Koha/AuthUtils.pm >+++ b/Koha/AuthUtils.pm >@@ -24,6 +24,8 @@ use Fcntl qw/O_RDONLY/; # O_RDONLY is used in generate_salt > > use Koha::Borrower; > >+use Koha::Exception::LoginFailed; >+ > use base 'Exporter'; > > our $VERSION = '1.01'; >@@ -136,6 +138,66 @@ sub generate_salt { > return $string; > } > >+=head checkHash >+ >+ my $passwordOk = Koha::AuthUtils::checkHash($password1, $password2) >+ >+Checks if a clear-text String/password matches the given hash when >+MD5 or Bcrypt hashing algorith is applied to it. >+ >+Bcrypt is applied if @PARAM2 starts with '$2' >+MD5 otherwise >+ >+@PARAM1 String, clear text passsword or any other String >+@PARAM2 String, hashed text password or any other String. >+@RETURN Boolean, 1 if given parameters match >+ , 0 if not >+=cut >+ >+sub checkHash { >+ my ( $password, $stored_hash ) = @_; >+ >+ $password = Encode::encode( 'UTF-8', $password ) >+ if Encode::is_utf8($password); >+ >+ return if $stored_hash eq '!'; >+ >+ my $hash; >+ if ( substr( $stored_hash, 0, 2 ) eq '$2' ) { >+ $hash = hash_password( $password, $stored_hash ); >+ } else { >+ #@DEPRECATED Digest::MD5, don't use it or you will get hurt. >+ require Digest::MD5; >+ $hash = Digest::MD5::md5_base64($password); >+ } >+ return $hash eq $stored_hash; >+} >+ >+=head checkKohaSuperuser >+ >+ my $borrower = Koha::AuthUtils::checkKohaSuperuser($userid, $password); >+ >+Check if the userid and password match the ones in the $KOHA_CONF >+@PARAM1 String, user identifier, either the koha.borrowers.userid, or koha.borrowers.cardnumber >+@PARAM2 String, clear text password from the authenticating user >+@RETURNS Koha::Borrower branded as superuser with ->isSuperuser() >+ or undef if user logging in is not a superuser. >+@THROWS Koha::Exception::LoginFailed if user identifier matches, but password doesn't >+=cut >+ >+sub checkKohaSuperuser { >+ my ($userid, $password) = @_; >+ >+ if ( $userid && $userid eq C4::Context->config('user') ) { >+ if ( $password && $password eq C4::Context->config('pass') ) { >+ return _createTemporarySuperuser(); >+ } >+ else { >+ Koha::Exception::LoginFailed->throw(error => "Password authentication failed"); >+ } >+ } >+} >+ > =head checkKohaSuperuserFromUserid > See checkKohaSuperuser(), with only the "user identifier"-@PARAM. > @THROWS nothing. >@@ -153,13 +215,14 @@ sub checkKohaSuperuserFromUserid { > > Create a temporary superuser which should be instantiated only to the environment > and then discarded. So do not ->store() it! >-@RETURN Koha::Borrower >+@RETURN Koha::Borrower, stamped as superuser. > =cut > > sub _createTemporarySuperuser { > my $borrower = Koha::Borrower->new(); > > my $superuserName = C4::Context->config('user'); >+ $borrower->isSuperuser(1); > $borrower->set({borrowernumber => 0, > userid => $superuserName, > cardnumber => $superuserName, >diff --git a/Koha/Borrower.pm b/Koha/Borrower.pm >index 93426bf..4ceae11 100644 >--- a/Koha/Borrower.pm >+++ b/Koha/Borrower.pm >@@ -43,6 +43,32 @@ sub type { > return 'Borrower'; > } > >+=head isSuperuser >+ >+ $borrower->isSuperuser(1); #Set this borrower to be a superuser >+ if ($borrower->isSuperuser()) { >+ #All your base are belong to us >+ } >+ >+Should be used from the authentication modules to mark this $borrower-object to >+have unlimited access to all Koha-features. >+This $borrower-object is the Koha DB user. >+@PARAM1 Integer, 1 means this borrower is the super/DB user. >+ "0" disables the previously set superuserness. >+=cut >+ >+sub isSuperuser { >+ my ($self, $Iam) = @_; >+ >+ if (defined $Iam && $Iam == 1) { >+ $self->{superuser} = 1; >+ } >+ elsif (defined $Iam && $Iam eq "0") { #Dealing with zero is special in Perl >+ $self->{superuser} = undef; >+ } >+ return (exists($self->{superuser}) && $self->{superuser}) ? 1 : undef; >+} >+ > =head1 AUTHOR > > Kyle M Hall <kyle@bywatersolutions.com> >diff --git a/Koha/Schema/Result/BorrowerPermission.pm b/Koha/Schema/Result/BorrowerPermission.pm >new file mode 100644 >index 0000000..c05ce96 >--- /dev/null >+++ b/Koha/Schema/Result/BorrowerPermission.pm >@@ -0,0 +1,149 @@ >+use utf8; >+package Koha::Schema::Result::BorrowerPermission; >+ >+# Created by DBIx::Class::Schema::Loader >+# DO NOT MODIFY THE FIRST PART OF THIS FILE >+ >+=head1 NAME >+ >+Koha::Schema::Result::BorrowerPermission >+ >+=cut >+ >+use strict; >+use warnings; >+ >+use base 'DBIx::Class::Core'; >+ >+=head1 TABLE: C<borrower_permissions> >+ >+=cut >+ >+__PACKAGE__->table("borrower_permissions"); >+ >+=head1 ACCESSORS >+ >+=head2 borrower_permission_id >+ >+ data_type: 'integer' >+ is_auto_increment: 1 >+ is_nullable: 0 >+ >+=head2 borrowernumber >+ >+ data_type: 'integer' >+ is_foreign_key: 1 >+ is_nullable: 0 >+ >+=head2 permission_module_id >+ >+ data_type: 'integer' >+ is_foreign_key: 1 >+ is_nullable: 0 >+ >+=head2 permission_id >+ >+ data_type: 'integer' >+ is_foreign_key: 1 >+ is_nullable: 0 >+ >+=cut >+ >+__PACKAGE__->add_columns( >+ "borrower_permission_id", >+ { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, >+ "borrowernumber", >+ { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, >+ "permission_module_id", >+ { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, >+ "permission_id", >+ { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, >+); >+ >+=head1 PRIMARY KEY >+ >+=over 4 >+ >+=item * L</borrower_permission_id> >+ >+=back >+ >+=cut >+ >+__PACKAGE__->set_primary_key("borrower_permission_id"); >+ >+=head1 UNIQUE CONSTRAINTS >+ >+=head2 C<borrowernumber> >+ >+=over 4 >+ >+=item * L</borrowernumber> >+ >+=item * L</permission_module_id> >+ >+=item * L</permission_id> >+ >+=back >+ >+=cut >+ >+__PACKAGE__->add_unique_constraint( >+ "borrowernumber", >+ ["borrowernumber", "permission_module_id", "permission_id"], >+); >+ >+=head1 RELATIONS >+ >+=head2 borrowernumber >+ >+Type: belongs_to >+ >+Related object: L<Koha::Schema::Result::Borrower> >+ >+=cut >+ >+__PACKAGE__->belongs_to( >+ "borrowernumber", >+ "Koha::Schema::Result::Borrower", >+ { borrowernumber => "borrowernumber" }, >+ { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, >+); >+ >+=head2 permission >+ >+Type: belongs_to >+ >+Related object: L<Koha::Schema::Result::Permission> >+ >+=cut >+ >+__PACKAGE__->belongs_to( >+ "permission", >+ "Koha::Schema::Result::Permission", >+ { permission_id => "permission_id" }, >+ { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, >+); >+ >+=head2 permission_module >+ >+Type: belongs_to >+ >+Related object: L<Koha::Schema::Result::PermissionModule> >+ >+=cut >+ >+__PACKAGE__->belongs_to( >+ "permission_module", >+ "Koha::Schema::Result::PermissionModule", >+ { permission_module_id => "permission_module_id" }, >+ { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, >+); >+ >+ >+# Created by DBIx::Class::Schema::Loader v0.07039 @ 2015-07-17 12:21:37 >+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:WaapKkhLT6DkqDZGVFvbQg >+ >+ >+# You can replace this text with custom code or comments, and it will be preserved on regeneration >+1; >diff --git a/Koha/Schema/Result/PermissionModule.pm b/Koha/Schema/Result/PermissionModule.pm >new file mode 100644 >index 0000000..bdebd23 >--- /dev/null >+++ b/Koha/Schema/Result/PermissionModule.pm >@@ -0,0 +1,119 @@ >+use utf8; >+package Koha::Schema::Result::PermissionModule; >+ >+# Created by DBIx::Class::Schema::Loader >+# DO NOT MODIFY THE FIRST PART OF THIS FILE >+ >+=head1 NAME >+ >+Koha::Schema::Result::PermissionModule >+ >+=cut >+ >+use strict; >+use warnings; >+ >+use base 'DBIx::Class::Core'; >+ >+=head1 TABLE: C<permission_modules> >+ >+=cut >+ >+__PACKAGE__->table("permission_modules"); >+ >+=head1 ACCESSORS >+ >+=head2 permission_module_id >+ >+ data_type: 'integer' >+ is_auto_increment: 1 >+ is_nullable: 0 >+ >+=head2 module >+ >+ data_type: 'varchar' >+ is_nullable: 0 >+ size: 32 >+ >+=head2 description >+ >+ data_type: 'varchar' >+ is_nullable: 1 >+ size: 255 >+ >+=cut >+ >+__PACKAGE__->add_columns( >+ "permission_module_id", >+ { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, >+ "module", >+ { data_type => "varchar", is_nullable => 0, size => 32 }, >+ "description", >+ { data_type => "varchar", is_nullable => 1, size => 255 }, >+); >+ >+=head1 PRIMARY KEY >+ >+=over 4 >+ >+=item * L</permission_module_id> >+ >+=back >+ >+=cut >+ >+__PACKAGE__->set_primary_key("permission_module_id"); >+ >+=head1 UNIQUE CONSTRAINTS >+ >+=head2 C<module> >+ >+=over 4 >+ >+=item * L</module> >+ >+=back >+ >+=cut >+ >+__PACKAGE__->add_unique_constraint("module", ["module"]); >+ >+=head1 RELATIONS >+ >+=head2 borrower_permissions >+ >+Type: has_many >+ >+Related object: L<Koha::Schema::Result::BorrowerPermission> >+ >+=cut >+ >+__PACKAGE__->has_many( >+ "borrower_permissions", >+ "Koha::Schema::Result::BorrowerPermission", >+ { "foreign.permission_module_id" => "self.permission_module_id" }, >+ { cascade_copy => 0, cascade_delete => 0 }, >+); >+ >+=head2 permissions >+ >+Type: has_many >+ >+Related object: L<Koha::Schema::Result::Permission> >+ >+=cut >+ >+__PACKAGE__->has_many( >+ "permissions", >+ "Koha::Schema::Result::Permission", >+ { "foreign.module" => "self.module" }, >+ { cascade_copy => 0, cascade_delete => 0 }, >+); >+ >+ >+# Created by DBIx::Class::Schema::Loader v0.07039 @ 2015-07-17 12:21:37 >+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:qc8JEcG/PXIlFu44MB+ouQ >+ >+ >+# You can replace this text with custom code or comments, and it will be preserved on regeneration >+1; >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt >index 9ae147e..d3dc043 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt >@@ -43,7 +43,9 @@ > <form action="[% url %]" method="post" name="loginform" id="loginform"> > <input type="hidden" name="koha_login_context" value="intranet" /> > [% FOREACH INPUT IN INPUTS %] >- <input type="hidden" name="[% INPUT.name |html %]" value="[% INPUT.value |html %]" /> >+ [% UNLESS INPUT.name == 'logout.x' #No reason to send the logout-signal again %] >+ <input type="hidden" name="[% INPUT.name |html %]" value="[% INPUT.value |html %]" /> >+ [% END %] > [% END %] > <p><label for="userid">Username:</label> > <input type="text" name="userid" id="userid" class="input focus" value="[% userid %]" size="20" tabindex="1" /> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt >index c474791..81f8c10 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt >@@ -142,7 +142,9 @@ > <input type="hidden" name="koha_login_context" value="opac" /> > <fieldset class="brief"> > [% FOREACH INPUT IN INPUTS %] >- <input type="hidden" name="[% INPUT.name |html %]" value="[% INPUT.value |html %]" /> >+ [% UNLESS INPUT.name == 'logout.x' #No reason to send the logout-signal again %] >+ <input type="hidden" name="[% INPUT.name |html %]" value="[% INPUT.value |html %]" /> >+ [% END %] > [% END %] > <label for="userid">Login</label> > <input type="text" size="25" id="userid" name="userid" /> >diff --git a/opac/opac-search-history.pl b/opac/opac-search-history.pl >index 6878068..930f530 100755 >--- a/opac/opac-search-history.pl >+++ b/opac/opac-search-history.pl >@@ -41,7 +41,6 @@ my ($template, $loggedinuser, $cookie) = get_template_and_user( > query => $cgi, > type => "opac", > authnotrequired => 1, >- flagsrequired => {borrowers => 1}, > debug => 1, > } > ); >diff --git a/opac/opac-user.pl b/opac/opac-user.pl >index a653705..c59931d 100755 >--- a/opac/opac-user.pl >+++ b/opac/opac-user.pl >@@ -349,7 +349,7 @@ foreach my $res (@reserves) { > $template->param( WAITING => \@waiting ); > > # current alert subscriptions >-my $alerts = getalert($borrowernumber) if $borrowernumber; >+my $alerts = getalert($borrowernumber) if $borrowernumber; #Superuser has no borrowernumber > foreach ( @$alerts ) { > $_->{ $_->{type} } = 1; > $_->{relatedto} = findrelatedto( $_->{type}, $_->{externalid} ); >diff --git a/t/db_dependent/Koha/Borrower.t b/t/db_dependent/Koha/Borrower.t >new file mode 100644 >index 0000000..75f985a >--- /dev/null >+++ b/t/db_dependent/Koha/Borrower.t >@@ -0,0 +1,55 @@ >+#!/usr/bin/perl >+ >+# Copyright 2015 Open Source Freedom Fighters >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+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. >+ >+use Koha::Borrower; >+ >+ >+ >+testIsSuperuser(); >+ >+ >+ >+ >+ >+################################################################################ >+#### Define test subroutines here ############################################## >+################################################################################ >+ >+=head testIsSuperuser >+@UNIT_TEST >+Tests Koha::Borrower->isSuperuser() >+=cut >+ >+sub testIsSuperuser { >+ my $borrower = Koha::Borrower->new(); >+ ok((not(defined($borrower->isSuperuser()))), "isSuperuser(): By default user is not defined as superuser."); >+ ok(($borrower->isSuperuser(1) == 1), "isSuperuser(): Setting user as superuser returns 1."); >+ ok(($borrower->isSuperuser() == 1), "isSuperuser(): Getting superuser status from a superuser returns 1."); >+ ok((not(defined($borrower->isSuperuser(0)))), "isSuperuser(): Removing superuser status from a superuser OK and returns undef"); >+ ok((not(defined($borrower->isSuperuser()))), "isSuperuser(): Ex-superuser superuser status is undef"); >+} >+ >+ >+ >+ >+####################### >+done_testing(); #YAY!! >+####################### >\ No newline at end of file >-- >1.9.1
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 7174
:
40672
|
40678
|
40680
|
40723
|
40739
|
41117
|
41124
|
41159
|
41256
|
41311
|
41312
|
41413
|
41536
|
41537
|
63255