@@ -, +, @@ elements to a separate data structure. authentication route to take. Route returns the authenticated Koha::Borrower or an Exception if login failed. framework in the format the framework needs. Eg. login failed, no permission, under maintenance. --- Koha/Auth.pm | 258 +++++++++++++++++++++ Koha/Auth/Challenge.pm | 74 ++++++ Koha/Auth/Challenge/Cookie.pm | 88 +++++++ .../Challenge/IndependentBranchesAutolocation.pm | 53 +++++ Koha/Auth/Challenge/OPACMaintenance.pm | 44 ++++ Koha/Auth/Challenge/Password.pm | 127 ++++++++++ Koha/Auth/Challenge/Permission.pm | 42 ++++ Koha/Auth/Challenge/RESTV1.pm | 179 ++++++++++++++ Koha/Auth/Challenge/Version.pm | 56 +++++ Koha/Auth/RequestNormalizer.pm | 178 ++++++++++++++ Koha/Auth/Route.pm | 75 ++++++ Koha/Auth/Route/Cookie.pm | 44 ++++ Koha/Auth/Route/Password.pm | 46 ++++ Koha/Auth/Route/RESTV1.pm | 43 ++++ 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 +++++ 19 files changed, 1369 insertions(+), 4 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 t/db_dependent/Koha/Borrower.t --- a/Koha/Auth.pm +++ a/Koha/Auth.pm @@ -0,0 +1,258 @@ +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 . + +#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 Koha::Libraries; + +#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', 'cardnumber', '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::Patron-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->{cookies}->{CGISESSID}, $authParams); + Koha::Exception::Logout->throw(error => "User logged out. Please redirect me!"); + } + #1. Check for password authentication, including LDAP. + if (not($borrower) && $rae->{postParams}->{koha_login_context} && ($rae->{postParams}->{userid} || $rae->{postParams}->{cardnumber}) && $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/) { + if (not($borrower) && $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. + if (not($borrower) && $rae->{cookies}->{CGISESSID}) { + $borrower = Koha::Auth::Route::Cookie::challenge($rae, $permissions, $authParams); + } + if (not($borrower)) { #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::Patron->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); + + if ($ENV{KOHA_REST_API_DEBUG} > 2) { + my @cc = caller(0); + print "\n".$cc[3]."\nSESSIONID ".$session->id().", FIRSTNAME ".$session->param('firstname')."\n"; + } + + 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} || '' ); + if ($rae->{postParams} && $rae->{postParams}->{koha_login_context} && $rae->{postParams}->{koha_login_context} eq 'REST' && + (not($session->param('koha_login_context')) || $session->param('koha_login_context') ne 'REST') #Make sure we dont create new Sessions for users who want to login many times in a row. + ) { + #We are logging in a user using the REST API, so we need to create a new session context outside of the usual CGISESSID-cookie + $session = C4::Auth::get_session(); + $session->param( 'koha_login_context', $rae->{postParams}->{koha_login_context} ); + } + + 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 ); + #originIps contain all the IP's this request has been proxied through. + #Get the last value. This is in line with how the CGI-layer deals with IP-based authentication. + $session->param( 'ip', $rae->{originIps}->[ -1 ] ); + $session->param( 'lasttime', time() ); + $session->flush(); #CGI::Session recommends to flush since auto-flush is not guaranteed. + + #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'; + } + unless ($branchname) { + my $library = Koha::Libraries->find($branchcode); + $branchname = $library->branchname if $library; + } + $session->param( 'branch', $branchcode ); + $session->param( 'branchname', ($branchname || 'NO_LIBRARY_SET')); +} + +=head clearUserEnvironment + +Removes the active authentication + +=cut + +sub clearUserEnvironment { + my ($sessionid, $authParams) = @_; + + my $session; + unless (blessed($sessionid)) { + $session = C4::Auth::get_session( $sessionid ); + } + else { + $session = $sessionid; + } + + if (C4::Context->userenv()) { + C4::Context::_unset_userenv( $session->id ); + } + $session->delete(); + $session->flush(); +} + +1; --- a/Koha/Auth/Challenge.pm +++ a/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 . + +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; --- a/Koha/Auth/Challenge/Cookie.pm +++ a/Koha/Auth/Challenge/Cookie.pm @@ -0,0 +1,88 @@ +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 . + +use Modern::Perl; + +use C4::Context; +use C4::Auth; +use Koha::AuthUtils; +use Koha::Patrons; + +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::Patron 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 (isSessionExpired($session)) { + Koha::Auth::clearUserEnvironment($session, {}); + 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) { + Koha::Auth::clearUserEnvironment($session, {}); + 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::Patrons->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; +} + +sub isSessionExpired { + my ($session) = @_; + + if ( ($session->param('lasttime') || 0) < (time()- C4::Auth::_timeout_syspref()) ) { + return 1; + } + return 0; +} + +1; --- a/Koha/Auth/Challenge/IndependentBranchesAutolocation.pm +++ a/Koha/Auth/Challenge/IndependentBranchesAutolocation.pm @@ -0,0 +1,53 @@ +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 . + +use Modern::Perl; + +use C4::Context; + +use Koha::Libraries; + +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 = Koha::Libraries->search->unblessed; + # 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; --- a/Koha/Auth/Challenge/OPACMaintenance.pm +++ a/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 . + +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; --- a/Koha/Auth/Challenge/Password.pm +++ a/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 . + +use Modern::Perl; + +use Koha::Patrons; +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::Patron-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::Patron, if login succeeded. + Sets Koha::Patron->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::Patrons->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::Patron, 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::Patrons->find({userid => $local_userid}); + return $borrower; + } + return undef; +} + +1; --- a/Koha/Auth/Challenge/Permission.pm +++ a/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 . + +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; --- a/Koha/Auth/Challenge/RESTV1.pm +++ a/Koha/Auth/Challenge/RESTV1.pm @@ -0,0 +1,179 @@ +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 . + +use Modern::Perl; +use DateTime::Format::HTTP; +use DateTime; + +use Koha::Patrons; + +use base qw(Koha::Auth::Challenge); + +use Koha::Exception::LoginFailed; +use Koha::Exception::BadParameter; +use Koha::Exception::Parse; + +=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::Patron 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 + }; + if (not($req_dt) || $@) { + Koha::Exception::BadParameter->throw(error => "X-Koha-Date HTTP-header [".$headers->{'X-Koha-Date'}."] 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 $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'"); + } + + my $borrower = Koha::Patrons->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."); + } + + #Checking for message replay abuses or change control using ETAG shouldn't be done here, since we need to make valid request more often than every second. + #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); + my $digest = Digest::SHA::hmac_sha256_hex($message, $apiKey->api_key); + + if ($ENV{KOHA_REST_API_DEBUG} > 2) { + my @cc = caller(1); + print "\n".$cc[3]."\nMAKESIGNATURE $method, $userid, $headerXKohaDate, ".$apiKey->api_key.", DIGEST $digest\n"; + } + + return $digest; +} + +=head prepareAuthenticationHeaders +@PARAM1 Koha::Patron, 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::Patrons->cast($borrower); + + my $headerXKohaDate = DateTime::Format::HTTP->format_datetime( + ($dateTime || DateTime->now( time_zone => C4::Context->tz() )) + ); + my $headerAuthorization = "Koha ".$borrower->userid.":".makeSignature($method, $borrower->userid, $headerXKohaDate, $borrower->getApiKey('active')); + return {'X-Koha-Date' => $headerXKohaDate, + 'Authorization' => $headerAuthorization}; +} + +1; --- a/Koha/Auth/Challenge/Version.pm +++ a/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 . + +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; --- a/Koha/Auth/RequestNormalizer.pm +++ a/Koha/Auth/RequestNormalizer.pm @@ -0,0 +1,178 @@ +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 . + +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') if $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)) { + if ($controller->isa('CGI')) { + $cookie->{HttpOnly} = 1; + $cookieOk = $controller->cookie( $cookie ); + } + elsif ($controller->isa('Mojolicious::Controller')) { + my $cooksreq = $controller->req->cookies; + my $cooksres = $controller->res->cookies; + foreach my $c (@{$controller->res->cookies}) { + + if ($c->name eq 'CGISESSID') { + $c->value($cookie->{value}); + $cookieOk = $c; + } + } + } + } + #No auth cookie, so we must make one :) + unless ($cookieOk) { + $controller->res->cookies($cookie); + my $cooks = $controller->res->cookies(); + foreach my $c (@$cooks) { + if ($c->name eq 'CGISESSID') { + $cookieOk = $c; + last; + } + } + } + return $cookieOk; +} + +1; --- a/Koha/Auth/Route.pm +++ a/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 . + +=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::::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::Patron-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; --- a/Koha/Auth/Route/Cookie.pm +++ a/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 . + +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; --- a/Koha/Auth/Route/Password.pm +++ a/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 . + +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}->{cardnumber}, $rae->{postParams}->{password}); + Koha::Auth::Challenge::Permission::challenge($borrower, $permissionsRequired) if $permissionsRequired; + return $borrower; +} + +1; --- a/Koha/Auth/Route/RESTV1.pm +++ a/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 . + +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; --- a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt @@ -45,7 +45,9 @@
[% FOREACH INPUT IN INPUTS %] - + [% UNLESS INPUT.name == 'logout.x' #No reason to send the logout-signal again %] + + [% END %] [% END %]

--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt +++ a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt @@ -154,7 +154,9 @@

[% FOREACH INPUT IN INPUTS %] - + [% UNLESS INPUT.name == 'logout.x' #No reason to send the logout-signal again %] + + [% END %] [% END %] --- a/opac/opac-search-history.pl +++ a/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, } ); --- a/opac/opac-user.pl +++ a/opac/opac-user.pl @@ -293,7 +293,7 @@ $template->param( ); # 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} ); --- a/t/db_dependent/Koha/Borrower.t +++ a/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 . + +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::Patron; + + + +testIsSuperuser(); + + + + + +################################################################################ +#### Define test subroutines here ############################################## +################################################################################ + +=head testIsSuperuser +@UNIT_TEST +Tests Koha::Borrower->isSuperuser() +=cut + +sub testIsSuperuser { + my $borrower = Koha::Patron->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!! +####################### --