@@ -, +, @@ 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 | 227 +++++++++++++++++++++ Koha/Auth/Challenge.pm | 74 +++++++ Koha/Auth/Challenge/Cookie.pm | 78 +++++++ .../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 | 157 ++++++++++++++ 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, 1654 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 --- a/Koha/Auth.pm +++ a/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 . + +#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; --- 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,78 @@ +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::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) = @_; + + 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') && $session->param('ip') ne $ENV{'REMOTE_ADDR'} ) { + $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; --- a/Koha/Auth/Challenge/IndependentBranchesAutolocation.pm +++ a/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 . + +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; --- 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::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; --- 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,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 . + +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; --- 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,157 @@ +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 $requestAuthElements = { #Collect the authentication elements here. + headers => $headers, + postParams => $postParams, + cookies => $cookies, + 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 $requestAuthElements = { #Collect the authentication elements here. + headers => $headers, + postParams => $postParams, + cookies => $cookies, + 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; --- 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::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; --- 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}); + 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}->{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/AuthUtils.pm +++ a/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, --- a/Koha/Borrower.pm +++ a/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 --- a/Koha/Schema/Result/BorrowerPermission.pm +++ a/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 + +=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 + +=back + +=cut + +__PACKAGE__->set_primary_key("borrower_permission_id"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=item * L + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint( + "borrowernumber", + ["borrowernumber", "permission_module_id", "permission_id"], +); + +=head1 RELATIONS + +=head2 borrowernumber + +Type: belongs_to + +Related object: L + +=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 + +=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 + +=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; --- a/Koha/Schema/Result/PermissionModule.pm +++ a/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 + +=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 + +=back + +=cut + +__PACKAGE__->set_primary_key("permission_module_id"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("module", ["module"]); + +=head1 RELATIONS + +=head2 borrower_permissions + +Type: has_many + +Related object: L + +=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 + +=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; --- a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt @@ -43,7 +43,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 @@ -142,7 +142,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 @@ -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} ); --- 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::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!! +####################### --