From 5ff962c3de6cf5633c9871ad3c7a43ba1cf23599 Mon Sep 17 00:00:00 2001 From: Olli-Antti Kivilahti Date: Tue, 21 Jul 2015 06:54:53 +0000 Subject: [PATCH] Bug 7174 - Authentication Rewrite Depends heavily on Buugg 14540, which introduces many tests to spot regression caused by this feature. Introduces a new modular authentication system based on Exception signaling and reusable authentication components/challenges. This system is cross-framework -compatible, currently supporting Mojolicious and CGI, but adding support for any other framework is straightforward. Central idea is to provide a single authentication function to deal with all types of authentication scenarios, REST API, Cookie, Password, LDAP... Also it is important to make a system that is easy to extend and can deal with many future authentication scenarios. Currently only CGI password, LDAP and cookie login is tested. Legacy behaviour is used to deal with other types of authentication. Deprecates a lot of subroutines in C4::Auth. See the attached schematic in Bugzilla for a more architectural overview. The basic principle is: 1. We get an authentication request from any Web/GUI-framework, for ex CGI. 2. Request is normalized by extracting all necessary authentication data elements to a separate data structure. 3. Based on the found authentication data elements, system decides which authentication route to take. 4. Route implements all challenges needed to authenticate the request. Route returns the authenticated Koha::Borrower or an Exception if login failed. 5. The user environment/session is set/deleted based on the Route result. 6. a Koha::Borrower and the CGISESSID-cookie is returned to the calling framework in the format the framework needs. 7. Framework needs to catch possible exceptions and deal with them. Eg. login failed, no permission, under maintenance. 8. Authentication succeeds and session is set, or failure is reported to user. ----------------------------------- -::- TEST PLAN: -::- ----------------------------------- Use PageObject tests from Buugg 14540 and Buugg 14536 to test against regression. ::MANUAL TESTS:: OPAC: 1. Go to opac-main.pl 1.1. Try to access a restricted page like /cgi-bin/koha/opac-account.pl 2. Login with password. 3. Navigate to any OPAC page requiring login. 4. Observe that the login is still valid and correct username is displayed on top right of the browser window. 5. Logout 5.1. Try to access a restricted page. 6. Login again. 7. Browse a bit. 8. Logout. Anonymous search history (OPAC): 1. Make a search 2. Make another search 3. Login 4. Make yet another search 5. Go to my search history. 6. Observe that the last three searches are shown in your search history. 7. Logout STAFF CLIENT (Intra): 0. Try to access a restricted page, like /cgi-bin/koha/mainpage.pl 1. Login 2. Navigate a bit to confirm that the login remains active. 3. Logout. 4. Login again. 5. Navigate to a restricted page. 6. Logout. 6.1. Try to access a restricted page. --- C4/Auth.pm | 289 +++++++++++++------- C4/Auth_with_ldap.pm | 11 +- C4/Context.pm | 9 +- Koha/Auth.pm | 225 +++++++++++++++ Koha/Auth/Component.pm | 302 +++++++++++++++++++++ Koha/Auth/Component/Password.pm | 91 +++++++ Koha/Auth/RequestNormalizer.pm | 153 +++++++++++ Koha/Auth/Route.pm | 69 +++++ Koha/Auth/Route/Cookie.pm | 41 +++ Koha/Auth/Route/Password.pm | 42 +++ Koha/Auth/Route/RESTV1.pm | 41 +++ Koha/AuthUtils.pm | 65 ++++- Koha/Borrower.pm | 26 ++ koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt | 4 +- .../opac-tmpl/bootstrap/en/modules/opac-auth.tt | 4 +- opac/opac-user.pl | 2 +- t/db_dependent/Auth_with_ldap.t | 18 +- t/db_dependent/Koha/Borrower.t | 55 ++++ 18 files changed, 1336 insertions(+), 111 deletions(-) create mode 100644 Koha/Auth.pm create mode 100644 Koha/Auth/Component.pm create mode 100644 Koha/Auth/Component/Password.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 diff --git a/C4/Auth.pm b/C4/Auth.pm index 3743869..77cdfa9 100644 --- a/C4/Auth.pm +++ b/C4/Auth.pm @@ -19,7 +19,7 @@ package C4::Auth; use strict; use warnings; -use Digest::MD5 qw(md5_base64); +use Digest::MD5 qw(md5_base64); #@DEPRECATED Digest::MD5, don't use it or you will get hurt. use JSON qw/encode_json/; use URI::Escape; use CGI::Session; @@ -34,6 +34,8 @@ use C4::Branch; # GetBranches use C4::Search::History; use Koha; use Koha::AuthUtils qw(hash_password); +use Koha::Auth; +use Koha::Auth::Component; use POSIX qw/strftime/; use List::MoreUtils qw/ any /; use Encode qw( encode is_utf8); @@ -636,50 +638,64 @@ has authenticated. =cut +=head _version_check +#@DEPRECATED See Bug 7174 +use Koha::Auth::Component::* instead +=cut + sub _version_check { + #@DEPRECATED See Bug 7174 my $type = shift; my $query = shift; my $version; - # If version syspref is unavailable, it means Koha is being installed, - # and so we must redirect to OPAC maintenance page or to the WebInstaller - # also, if OpacMaintenance is ON, OPAC should redirect to maintenance - if ( C4::Context->preference('OpacMaintenance') && $type eq 'opac' ) { - warn "OPAC Install required, redirecting to maintenance"; - print $query->redirect("/cgi-bin/koha/maintenance.pl"); - safe_exit; - } - unless ( $version = C4::Context->preference('Version') ) { # assignment, not comparison - if ( $type ne 'opac' ) { - warn "Install required, redirecting to Installer"; - print $query->redirect("/cgi-bin/koha/installer/install.pl"); - } else { - warn "OPAC Install required, redirecting to maintenance"; - print $query->redirect("/cgi-bin/koha/maintenance.pl"); + try { + Koha::Auth::Component::checkOPACMaintenance() if ( $type eq 'opac' ); + Koha::Auth::Component::checkVersion(); + } catch { + if (blessed($_)) { + if ($_->isa('Koha::Exception::VersionMismatch')) { + # check that database and koha version are the same + # there is no DB version, it's a fresh install, + # go to web installer + # there is a DB version, compare it to the code version + my $warning = $_->error()." Redirecting to %s."; + if ( $type ne 'opac' ) { + warn sprintf( $warning, 'Installer' ); + print $query->redirect("/cgi-bin/koha/installer/install.pl?step=1&op=updatestructure"); + } else { + warn sprintf( "OPAC: " . $warning, 'maintenance' ); + print $query->redirect("/cgi-bin/koha/maintenance.pl"); + } + safe_exit; + } + elsif ($_->isa('Koha::Exception::ServiceTemporarilyUnavailable')) { + # also, if OpacMaintenance is ON, OPAC should redirect to maintenance + warn "OPAC Install required, redirecting to maintenance"; + print $query->redirect("/cgi-bin/koha/maintenance.pl"); + safe_exit; + } + elsif ($_->isa('Koha::Exception::BadSystemPreference')) { + # If version syspref is unavailable, it means Koha is being installed, + # and so we must redirect to OPAC maintenance page or to the WebInstaller + if ( $type ne 'opac' ) { + warn "Install required, redirecting to Installer"; + print $query->redirect("/cgi-bin/koha/installer/install.pl"); + } else { + warn "OPAC Install required, redirecting to maintenance"; + print $query->redirect("/cgi-bin/koha/maintenance.pl"); + } + safe_exit; + } + else { + warn "Unknown exception class ".ref($_)."\n"; + die $_->rethrow(); #Unhandled exception case + } } - safe_exit; - } - - # check that database and koha version are the same - # there is no DB version, it's a fresh install, - # go to web installer - # there is a DB version, compare it to the code version - my $kohaversion = Koha::version(); - - # remove the 3 last . to have a Perl number - $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/; - $debug and print STDERR "kohaversion : $kohaversion\n"; - if ( $version < $kohaversion ) { - my $warning = "Database update needed, redirecting to %s. Database is $version and Koha is $kohaversion"; - if ( $type ne 'opac' ) { - warn sprintf( $warning, 'Installer' ); - print $query->redirect("/cgi-bin/koha/installer/install.pl?step=1&op=updatestructure"); - } else { - warn sprintf( "OPAC: " . $warning, 'maintenance' ); - print $query->redirect("/cgi-bin/koha/maintenance.pl"); + else { + die $_; #Not a Koha::Exception-object, so rethrow it } - safe_exit; - } + }; } sub _session_log { @@ -699,7 +715,101 @@ sub _timeout_syspref { return $timeout; } +=head checkauth + +Compatibility layer for old Koha authentication system. +Tries to authenticate using Koha::Auth, but if no authentication mechanism is +identified, falls back to the deprecated legacy behaviour. +=cut + sub checkauth { + my @params = @_; #Clone params so we don't accidentally change them if we fallback to checkauth_legacy() + my $query = shift; + my $authnotrequired = shift; + my $flagsrequired = _changeAllPermissionsMarkerToAnyPermissionMarker(shift); #circulate => 1 is deprecated, only circulate => '*' is supported. + my $type = shift; + my $persona = shift; + + ##Revert to legacy authentication for more complex authentication mechanisms. + if ($persona && $cas && $shib) { + return checkauth_legacy(@params); + } + + my ($borrower, $cookie); + try { + ($borrower, $cookie) = Koha::Auth::authenticate($query, $flagsrequired, {authnotrequired => $authnotrequired}); + } catch { + if (blessed($_)) { + if ($_->isa('Koha::Exception::VersionMismatch')) { + + my $warning = $_->error()." Redirecting to %s."; + if ( $type ne 'opac' ) { + warn sprintf( $warning, 'Installer' ); + print $query->redirect("/cgi-bin/koha/installer/install.pl?step=1&op=updatestructure"); + } else { + warn sprintf( "OPAC: " . $warning, 'maintenance' ); + print $query->redirect("/cgi-bin/koha/maintenance.pl"); + } + safe_exit; + + } + elsif ($_->isa('Koha::Exception::BadSystemPreference')) { + + if ( $type ne 'opac' ) { + warn $_->error()." Redirecting to installer."; + print $query->redirect("/cgi-bin/koha/installer/install.pl"); + } else { + warn $_->error()." Redirecting to maintenance."; + print $query->redirect("/cgi-bin/koha/maintenance.pl"); + } + safe_exit; + + } + elsif ($_->isa('Koha::Exception::LoginFailed')) { + _showLoginPage($query, $type, {invalid_username_or_password => 1}, {}, undef); + } + elsif ($_->isa('Koha::Exception::NoPermission')) { + _showLoginPage($query, $type, {nopermission => 1}, {}, undef); + } + elsif ($_->isa('Koha::Exception::Logout')) { + _showLoginPage($query, $type, {}, {}, undef); + } + elsif ($_->isa('Koha::Exception::ServiceTemporarilyUnavailable')) { + warn $_->error(); + print $query->redirect("/cgi-bin/koha/maintenance.pl"); + safe_exit; + } + else { + warn "Unknown exception class ".ref($_)."\n"; + $_->rethrow(); #Unhandled exception case + } + } + else { + die $_; #Not a Koha::Exception-object + } + }; + + if ($borrower) { #We authenticated succesfully! Emulate the legacy interface for get_template_and_user(); + my $user = $borrower->userid; + my $sessionID = $cookie->{value}->[0]; + my $aa = $borrower->userid; + my $flags = getuserflags(undef, $borrower->userid, undef) if $borrower->userid; + return ( $user, $cookie, $sessionID, $flags ); + } +} + +=head checkauth_legacy +@DEPRECATED See Bug 7174 + +We are calling this because the given authentication mechanism is not yet supported +in Koha::Auth. + +See checkauth-documentation floating somewhere in this file for info about the +legacy authentication. +=cut + +sub checkauth_legacy { + #@DEPRECATED See Bug 7174 my $query = shift; $debug and warn "Checking Auth"; @@ -721,8 +831,6 @@ sub checkauth { my ( $userid, $cookie, $sessionID, $flags, $barshelves, $pubshelves ); my $logout = $query->param('logout.x'); - my $anon_search_history; - # This parameter is the name of the CAS server we want to authenticate against, # when using authentication against multiple CAS servers, as configured in Auth_cas_servers.yaml my $casparam = $query->param('cas'); @@ -783,7 +891,6 @@ sub checkauth { #if a user enters an id ne to the id in the current session, we need to log them in... #first we need to clear the anonymous session... $debug and warn "query id = $q_userid but session id = $s_userid"; - $anon_search_history = $session->param('search_history'); $session->delete(); $session->flush; C4::Context->_unset_userenv($sessionID); @@ -864,13 +971,6 @@ sub checkauth { #we initiate a session prior to checking for a username to allow for anonymous sessions... my $session = get_session("") or die "Auth ERROR: Cannot get_session()"; - # Save anonymous search history in new session so it can be retrieved - # by get_template_and_user to store it in user's search history after - # a successful login. - if ($anon_search_history) { - $session->param( 'search_history', $anon_search_history ); - } - my $sessionID = $session->id; C4::Context->_new_userenv($sessionID); $cookie = $query->cookie( @@ -989,12 +1089,12 @@ sub checkauth { $info{'nopermission'} = 1; C4::Context->_unset_userenv($sessionID); } - my ( $borrowernumber, $firstname, $surname, $userflags, + my ( $borrowernumber, $firstname, $surname, $branchcode, $branchname, $branchprinter, $emailaddress ); if ( $return == 1 ) { my $select = " - SELECT borrowernumber, firstname, surname, flags, borrowers.branchcode, + SELECT borrowernumber, firstname, surname, borrowers.branchcode, branches.branchname as branchname, branches.branchprinter as branchprinter, email @@ -1017,10 +1117,10 @@ sub checkauth { } } if ( $sth->rows ) { - ( $borrowernumber, $firstname, $surname, $userflags, + ( $borrowernumber, $firstname, $surname, $branchcode, $branchname, $branchprinter, $emailaddress ) = $sth->fetchrow; $debug and print STDERR "AUTH_3 results: " . - "$cardnumber,$borrowernumber,$userid,$firstname,$surname,$userflags,$branchcode,$emailaddress\n"; + "$cardnumber,$borrowernumber,$userid,$firstname,$surname,$branchcode,$emailaddress\n"; } else { print STDERR "AUTH_3: no results for userid='$userid', cardnumber='$cardnumber'.\n"; } @@ -1036,7 +1136,7 @@ sub checkauth { $branchname = GetBranchName($branchcode); } my $branches = GetBranches(); - if ( C4::Context->boolean_preference('IndependentBranches') && C4::Context->boolean_preference('Autolocation') ) { + if ( C4::Context->boolean_preference('IndependentBranches') && C4::Context->boolean_preference('Autolocation') ) { #Why Autolocation cannot work without IndependetBranches?? # we have to check they are coming from the right ip range my $domain = $branches->{$branchcode}->{'branchip'}; @@ -1066,7 +1166,6 @@ sub checkauth { $session->param( 'surname', $surname ); $session->param( 'branch', $branchcode ); $session->param( 'branchname', $branchname ); - $session->param( 'flags', $userflags ); $session->param( 'emailaddress', $emailaddress ); $session->param( 'ip', $session->remote_addr() ); $session->param( 'lasttime', time() ); @@ -1149,6 +1248,12 @@ sub checkauth { # # get the inputs from the incoming query + _showLoginPage($query, $type, $cookie, \%info, $casparam); +} + +sub _showLoginPage { + my ($query, $type, $cookie, $info, $casparam) = @_; + my @inputs = (); foreach my $name ( param $query) { (next) if ( $name eq 'userid' || $name eq 'password' || $name eq 'ticket' ); @@ -1200,7 +1305,7 @@ sub checkauth { IntranetUserJS => C4::Context->preference("IntranetUserJS"), IndependentBranches => C4::Context->preference("IndependentBranches"), AutoLocation => C4::Context->preference("AutoLocation"), - wrongip => $info{'wrongip'}, + wrongip => $info->{'wrongip'}, PatronSelfRegistration => C4::Context->preference("PatronSelfRegistration"), PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"), persona => C4::Context->preference("Persona"), @@ -1208,7 +1313,7 @@ sub checkauth { ); $template->param( OpacPublic => C4::Context->preference("OpacPublic") ); - $template->param( loginprompt => 1 ) unless $info{'nopermission'}; + $template->param( loginprompt => 1 ) unless $info->{'nopermission'}; if ( $type eq 'opac' ) { require C4::VirtualShelves; @@ -1238,7 +1343,7 @@ sub checkauth { } $template->param( - invalidCasLogin => $info{'invalidCasLogin'} + invalidCasLogin => $info->{'invalidCasLogin'} ); } @@ -1254,7 +1359,7 @@ sub checkauth { url => $self_url, LibraryName => C4::Context->preference("LibraryName"), ); - $template->param(%info); + $template->param(%$info); # $cookie = $query->cookie(CGISESSID => $session->id # ); @@ -1268,6 +1373,7 @@ sub checkauth { } =head2 check_api_auth +@DEPRECATED See Bug 7174 ($status, $cookie, $sessionId) = check_api_auth($query, $userflags); @@ -1302,6 +1408,7 @@ Possible return values in C<$status> are: =cut sub check_api_auth { + #@DEPRECATED See Bug 7174 my $query = shift; my $flagsrequired = shift; @@ -1433,17 +1540,17 @@ sub check_api_auth { if ( $return == 1 ) { my ( $borrowernumber, $firstname, $surname, - $userflags, $branchcode, $branchname, + $branchcode, $branchname, $branchprinter, $emailaddress ); my $sth = $dbh->prepare( -"select borrowernumber, firstname, surname, flags, borrowers.branchcode, branches.branchname as branchname,branches.branchprinter as branchprinter, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where userid=?" +"select borrowernumber, firstname, surname, borrowers.branchcode, branches.branchname as branchname,branches.branchprinter as branchprinter, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where userid=?" ); $sth->execute($userid); ( $borrowernumber, $firstname, $surname, - $userflags, $branchcode, $branchname, + $branchcode, $branchname, $branchprinter, $emailaddress ) = $sth->fetchrow if ( $sth->rows ); @@ -1454,14 +1561,14 @@ sub check_api_auth { $sth->execute($cardnumber); ( $borrowernumber, $firstname, $surname, - $userflags, $branchcode, $branchname, + $branchcode, $branchname, $branchprinter, $emailaddress ) = $sth->fetchrow if ( $sth->rows ); unless ( $sth->rows ) { $sth->execute($userid); ( - $borrowernumber, $firstname, $surname, $userflags, + $borrowernumber, $firstname, $surname, $branchcode, $branchname, $branchprinter, $emailaddress ) = $sth->fetchrow if ( $sth->rows ); } @@ -1495,7 +1602,6 @@ sub check_api_auth { $session->param( 'surname', $surname ); $session->param( 'branch', $branchcode ); $session->param( 'branchname', $branchname ); - $session->param( 'flags', $userflags ); $session->param( 'emailaddress', $emailaddress ); $session->param( 'ip', $session->remote_addr() ); $session->param( 'lasttime', time() ); @@ -1529,6 +1635,7 @@ sub check_api_auth { } =head2 check_cookie_auth +@DEPRECATED See Bug 7174 ($status, $sessionId) = check_api_auth($cookie, $userflags); @@ -1556,6 +1663,7 @@ Possible return values in C<$status> are: =cut sub check_cookie_auth { + #@DEPRECATED See Bug 7174 my $cookie = shift; my $flagsrequired = shift; @@ -1679,12 +1787,13 @@ sub get_session { return $session; } +#@DEPRECATED See Bug 7174 sub checkpw { my ( $dbh, $userid, $password, $query, $type ) = @_; $type = 'opac' unless $type; if ($ldap) { $debug and print STDERR "## checkpw - checking LDAP\n"; - my ( $retval, $retcard, $retuserid ) = checkpw_ldap(@_); # EXTERNAL AUTH + my ( $retval, $retcard, $retuserid ) = checkpw_ldap($userid, $password); # EXTERNAL AUTH return 0 if $retval == -1; # Incorrect password for LDAP login attempt ($retval) and return ( $retval, $retcard, $retuserid ); } @@ -1723,6 +1832,7 @@ sub checkpw { return checkpw_internal(@_) } +#@DEPRECATED See Bug 7174 sub checkpw_internal { my ( $dbh, $userid, $password ) = @_; @@ -1748,13 +1858,13 @@ sub checkpw_internal { $sth->execute($userid); if ( $sth->rows ) { my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname, - $surname, $branchcode, $branchname, $flags ) + $surname, $branchcode, $branchname ) = $sth->fetchrow; if ( checkpw_hash( $password, $stored_hash ) ) { C4::Context->set_userenv( "$borrowernumber", $userid, $cardnumber, - $firstname, $surname, $branchcode, $branchname, $flags ); + $firstname, $surname, $branchcode, $branchname ); return 1, $cardnumber, $userid; } } @@ -1775,6 +1885,8 @@ sub checkpw_internal { return 1, $cardnumber, $userid; } } + + #@DEPRECATED see Bug 7174. I think the demo-user should be represented with permissions instead of a hard-coded non-borrower anomaly. if ( $userid && $userid eq 'demo' && "$password" eq 'demo' && C4::Context->config('demo') ) @@ -1787,6 +1899,7 @@ sub checkpw_internal { return 0; } +#@DEPRECATED See Bug 7174 sub checkpw_hash { my ( $password, $stored_hash ) = @_; @@ -1797,6 +1910,7 @@ sub checkpw_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. $hash = md5_base64($password); } return $hash eq $stored_hash; @@ -1820,7 +1934,6 @@ sub getuserflags { my $flags = shift; my $userid = shift; my $dbh = @_ ? shift : C4::Context->dbh; - my $userflags; { # I don't want to do this, but if someone logs in as the database # user, it would be preferable not to spam them to death with @@ -1829,28 +1942,6 @@ sub getuserflags { $flags += 0; } return get_user_subpermissions($userid); - - #@DEPRECATED, USE THE Koha::Auth::PermissionManager - my $sth = $dbh->prepare("SELECT bit, flag, defaulton FROM userflags"); - $sth->execute; - - while ( my ( $bit, $flag, $defaulton ) = $sth->fetchrow ) { - if ( ( $flags & ( 2**$bit ) ) || $defaulton ) { - $userflags->{$flag} = 1; - } - else { - $userflags->{$flag} = 0; - } - } - - # get subpermissions and merge with top-level permissions - my $user_subperms = get_user_subpermissions($userid); - foreach my $module ( keys %$user_subperms ) { - next if $userflags->{$module} == 1; # user already has permission for everything in this module - $userflags->{$module} = $user_subperms->{$module}; - } - - return $userflags; } =head2 get_user_subpermissions @@ -1952,9 +2043,7 @@ sub haspermission { my $flags = getuserflags( undef, $userid ); #Sanitate 1 to * because we no longer have 1's from the koha.borrowers.flags. - foreach my $module (%$flagsrequired) { - $flagsrequired->{$module} = '*' if $flagsrequired->{$module} && $flagsrequired->{$module} eq '1'; - } + _changeAllPermissionsMarkerToAnyPermissionMarker($flagsrequired); if ( $userid eq C4::Context->config('user') ) { @@ -1989,7 +2078,21 @@ sub haspermission { #FIXME - This fcn should return the failed permission so a suitable error msg can be delivered. } +=head +@DEPRECATED +Sanitate 1 to * because we no longer have 1's from the koha.borrowers.flags. +=cut + +sub _changeAllPermissionsMarkerToAnyPermissionMarker { + #@DEPRECATED + my ($flagsrequired) = @_; + foreach my $module (%$flagsrequired) { + $flagsrequired->{$module} = '*' if $flagsrequired->{$module} && $flagsrequired->{$module} eq '1'; + } +} + sub getborrowernumber { + #@DEPRECATED See Bug 7174 my ($userid) = @_; my $userenv = C4::Context->userenv; if ( defined($userenv) && ref($userenv) eq 'HASH' && $userenv->{number} ) { diff --git a/C4/Auth_with_ldap.pm b/C4/Auth_with_ldap.pm index 58484a2..7f000c5 100644 --- a/C4/Auth_with_ldap.pm +++ b/C4/Auth_with_ldap.pm @@ -103,8 +103,17 @@ sub search_method { return $search; } +=head checkpw_ldap + +@RETURNS Integer, -1 if login failed + , 0 if connection to the LDAP server couldn't be reliably established. + or List of (-1|1|0, $cardnumber, $local_userid); + where $cardnumber is koha.borrowers.cardnumber + $local_userid is the koha.borrowers.userid +=cut + sub checkpw_ldap { - my ($dbh, $userid, $password) = @_; + my ($userid, $password) = @_; my @hosts = split(',', $prefhost); my $db = Net::LDAP->new(\@hosts); unless ( $db ) { diff --git a/C4/Context.pm b/C4/Context.pm index 660a489..05f20b4 100644 --- a/C4/Context.pm +++ b/C4/Context.pm @@ -1064,7 +1064,7 @@ sub userenv { C4::Context->set_userenv($usernum, $userid, $usercnum, $userfirstname, $usersurname, - $userbranch, $branchname, $userflags, + $userbranch, $branchname, $superlibrarian, $emailaddress, $branchprinter, $persona); Establish a hash of user environment variables. @@ -1076,7 +1076,7 @@ set_userenv is called in Auth.pm #' sub set_userenv { shift @_; - my ($usernum, $userid, $usercnum, $userfirstname, $usersurname, $userbranch, $branchname, $userflags, $emailaddress, $branchprinter, $persona, $shibboleth)= + my ($usernum, $userid, $usercnum, $userfirstname, $usersurname, $userbranch, $branchname, $superlibrarian, $emailaddress, $branchprinter, $persona, $shibboleth)= map { Encode::is_utf8( $_ ) ? $_ : Encode::decode('UTF-8', $_) } # CGI::Session doesn't handle utf-8, so we decode it here @_; my $var=$context->{"activeuser"} || ''; @@ -1089,7 +1089,7 @@ sub set_userenv { #possibly a law problem "branch" => $userbranch, "branchname" => $branchname, - "flags" => $userflags, + "flags" => $superlibrarian, #@DEPRECATED, use Koha::Borrower->isSuperlibrarian() instead of flags "emailaddress" => $emailaddress, "branchprinter" => $branchprinter, "persona" => $persona, @@ -1204,6 +1204,7 @@ sub tz { =head2 IsSuperLibrarian +@DEPRECATED, use Koha::Borrower->isSuperlibrarian() C4::Context->IsSuperLibrarian(); @@ -1220,7 +1221,7 @@ sub IsSuperLibrarian { return 1; } - return ($userenv->{flags}//0) % 2; + return ($userenv->{flags}//0) % 2; #If flags == 1, this is true. } =head2 interface diff --git a/Koha/Auth.pm b/Koha/Auth.pm new file mode 100644 index 0000000..83c99a5 --- /dev/null +++ b/Koha/Auth.pm @@ -0,0 +1,225 @@ +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-Signature', 'ETag', 'X-Koha-Username'); +our @authenticationPOSTparams = ('password', 'userid', 'PT', 'branch', 'logout.x'); +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::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}->{userid} && $rae->{postParams}->{password}) { + $borrower = Koha::Auth::Route::Password::challenge($rae, $permissions, $authParams); + } + #2. Check for REST's signature-based authentication. + elsif ($rae->{headers}->{'X-Koha-Signature'}) { + $borrower = Koha::Auth::Route::RESTV1::check($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 => "No 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 { + $_->rethrow(); #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')) { + ##Branch is already set so no reason to change it. + return; + } + 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))); +} + +=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; + C4::Context->_unset_userenv( $rae->{cookies}->{CGISESSID} ); +} +1; \ No newline at end of file diff --git a/Koha/Auth/Component.pm b/Koha/Auth/Component.pm new file mode 100644 index 0000000..a8ca56d --- /dev/null +++ b/Koha/Auth/Component.pm @@ -0,0 +1,302 @@ +package Koha::Auth::Component; + +# 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 Digest::SHA qw(hmac_sha256_hex); + +use Koha qw(version); +use Koha::Borrowers; +use Koha::AuthUtils; +use Koha::Auth::Component::Password; +use Koha::Auth::PermissionManager; + +use Koha::Exception::VersionMismatch; +use Koha::Exception::BadSystemPreference; +use Koha::Exception::ServiceTemporarilyUnavailable; +use Koha::Exception::LoginFailed; + +=head1 NAME Koha::Auth::Component + +=head2 SYNOPSIS + +In this package we define the authentication steps we can use to define +authentication path behaviour. + +=head2 USAGE + + use Scalar::Util qw(blessed); + try { + ... + Koha::Auth::Component::checkVersion(); + Koha::Auth::Component::checkOPACMaintenance(); + ... + } 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 + +=head checkVersion +STATIC + + Koha::Auth::Component::checkVersion(); + +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 checkVersion { + 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'"); + } +} + +=head checkOPACMaintenance +STATIC + + Koha::Auth::Component::checkOPACMaintenance(); + +Checks if OPAC is under maintenance. + +@THROWS Koha::Exception::ServiceTemporarilyUnavailable +=cut + +sub checkOPACMaintenance { + if ( C4::Context->preference('OpacMaintenance') ) { + Koha::Exception::ServiceTemporarilyUnavailable->throw(error => 'OPAC is under maintenance'); + } +} + +=head checkPassword +STATIC + + Koha::Auth::Component::checkPassword(); + +@RETURN Koha::Borrower-object if check succeedes, otherwise throws exceptions. +@THROWS Koha::Exception::LoginFailed from Koha::AuthUtils password checks. +=cut + +sub checkPassword { + my ($userid, $password) = @_; + + my $borrower; + if (C4::Context->config('useldapserver')) { + $borrower = Koha::Auth::Component::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::Component::Password::checkKohaPassword($userid, $password); +} + +=head checkPermissions +STATIC + + Koha::Auth::Component::checkPermissions($borrower, $permissionsRequired); + +@THROWS Koha::Exception::NoPermission with the missing permission if permissions + are inadequate +=cut + +sub checkPermissions { + my ($borrower, $permissionsRequired) = @_; + + my $permissionManager = Koha::Auth::PermissionManager->new(); + $permissionManager->hasPermissions($borrower, $permissionsRequired); +} + +=head checkCookie +STATIC + + Koha::Auth::Component::checkCookie($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 checkCookie { + 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}) unless $borrower; + Koha::Exception::LoginFailed->throw(error => "Cookie authentication succeeded, but no borrower found with userid '$userid'.") + unless $borrower; + + $session->param( 'lasttime', time() ); + return $borrower; +} + +=head checkIndependentBranchesAutolocation + +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 checkIndependentBranchesAutolocation { + my ($currentBranchcode) = @_; + + if ( $currentBranchcode && C4::Context->boolean_preference('IndependentBranches') && C4::Context->boolean_preference('Autolocation') ) { + my $ip = $ENV{'REMOTE_ADDR'}; + + my $branches = 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."); + } + } +} + +=head checkRESTV1 + + my $borrower = Koha::Auth::Component::checkRESTV1(); + +For authentication to succeed, the client have to send 3 custom HTTP +headers: + - X-Koha-Username: userid of borrower + - X-Koha-Timestamp: timestamp of the request + - X-Koha-Signature: signature of the request + +The signature is a HMAC-SHA256 hash of several elements of the request, +separated by spaces: + - HTTP method (uppercase) + - URL path and query string + - username + - timestamp of the request + +The server then tries to rebuild the signature with each user's API key. +If one matches the received X-Koha-Signature, then authentication is +almost OK. + +To avoid requests to be replayed, the last request's timestamp is stored +in database and the authentication succeeds only if the stored timestamp +is lesser than X-Koha-Timestamp. + +@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 no user found with such name +=cut + +sub checkRESTV1 { + my ($headers, $method, $uri) = @_; + my $borrower = Koha::Borrowers->find({userid => $headers->{'X-Koha-Username'}}); + Koha::Exception::LoginFailed->throw(error => "No borrower from header X-Koha-Username, with value '".$headers->{'X-Koha-Username'}."'") + unless $borrower; + + my $req_username = $headers->{'X-Koha-Username'}; + my $req_timestamp = $headers->{'X-Koha-Timestamp'}; + my $req_signature = $headers->{'X-Koha-Signature'}; + + my @apikeys = Koha::ApiKeys->search({ + borrowernumber => $borrower->borrowernumber, + active => 1, + }); + Koha::Exception::LoginFailed->throw(error => "User has no API keys") unless @apikeys; + + my $message = "$method $uri $req_username $req_timestamp"; + my $signature = ''; + foreach my $apikey (@apikeys) { + $signature = Digest::SHA::hmac_sha256_hex($message, $apikey->api_key); + + last if $signature eq $req_signature; + } + + unless ($signature eq $req_signature) { + Koha::Exception::LoginFailed->throw(error => "API key authentication failed"); + } + + my $api_timestamp = Koha::ApiTimestamps->find($borrower->borrowernumber); + my $timestamp = $api_timestamp ? $api_timestamp->timestamp : 0; + unless ($timestamp < $req_timestamp) { + Koha::Exception::LoginFailed->throw(error => "Bad X-Koha-Timestamp, expected '$timestamp'"); + } + + unless ($api_timestamp) { + $api_timestamp = new Koha::ApiTimestamp; + $api_timestamp->borrowernumber($borrower->borrowernumber); + } + $api_timestamp->timestamp($req_timestamp); + $api_timestamp->store; + + return $borrower; +} + +1; \ No newline at end of file diff --git a/Koha/Auth/Component/Password.pm b/Koha/Auth/Component/Password.pm new file mode 100644 index 0000000..9ffbaa1 --- /dev/null +++ b/Koha/Auth/Component/Password.pm @@ -0,0 +1,91 @@ +package Koha::Auth::Component::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::AuthUtils; + +our @usernameAliasColumns = ('userid', 'cardnumber'); #Possible columns to treat as the username when authenticating. Must be UNIQUE in DB. + +=head NAME Koha::Auth::Component::Password + +=head SYNOPSIS + +This module implements the more specific behaviour of the password authentication component. + +=head checkKohaPassword + + my $borrower = Koha::AuthUtils::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::Borrower->find({userid => $local_userid}); + return $borrower; + } + return undef; +} \ No newline at end of file diff --git a/Koha/Auth/RequestNormalizer.pm b/Koha/Auth/RequestNormalizer.pm new file mode 100644 index 0000000..baa2bb6 --- /dev/null +++ b/Koha/Auth/RequestNormalizer.pm @@ -0,0 +1,153 @@ +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) = ({}, {}, {}); + if (blessed($controller) && $controller->isa('CGI')) { + my $headers = $request->headers(); + foreach my $authHeader (@$authenticationHeaders) { + if (my $val = $request->headers()->$authHeader()) { + $headers->{$authHeader} = $val; + } + } + foreach my $authParam (@$authenticationPOSTparams) { + if (my $val = $request->param($authParam)) { + $postParams->{$authParam} = $val; + } + } + foreach my $authCookie (@$authenticationCookies) { + if (my $val = $request->cookies($authCookie)) { + $cookies->{$authCookie} = $val; + } + } + } + + 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; \ No newline at end of file diff --git a/Koha/Auth/Route.pm b/Koha/Auth/Route.pm new file mode 100644 index 0000000..297dca1 --- /dev/null +++ b/Koha/Auth/Route.pm @@ -0,0 +1,69 @@ +package Koha::Auth::Route; + +# 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 C4::Auth::haspermission() 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::Component-subroutine calls to test for various +authentication challenges. + +=cut + +1; \ No newline at end of file diff --git a/Koha/Auth/Route/Cookie.pm b/Koha/Auth/Route/Cookie.pm new file mode 100644 index 0000000..3ced599 --- /dev/null +++ b/Koha/Auth/Route/Cookie.pm @@ -0,0 +1,41 @@ +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::Component; + +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::Component::checkOPACMaintenance() if $routeParams->{inOPAC}; + Koha::Auth::Component::checkVersion(); + my $borrower = Koha::Auth::Component::checkCookie($rae->{cookies}->{CGISESSID}); + Koha::Auth::Component::checkPermissions($borrower, $permissionsRequired) if $permissionsRequired; + return $borrower; +} + +1; \ No newline at end of file diff --git a/Koha/Auth/Route/Password.pm b/Koha/Auth/Route/Password.pm new file mode 100644 index 0000000..44cc1fa --- /dev/null +++ b/Koha/Auth/Route/Password.pm @@ -0,0 +1,42 @@ +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::Component; + +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::Component::checkOPACMaintenance() if $routeParams->{inOPAC}; + Koha::Auth::Component::checkVersion(); + Koha::Auth::Component::checkIndependentBranchesAutolocation($routeParams->{branch}); + my $borrower = Koha::Auth::Component::checkPassword($rae->{postParams}->{userid}, $rae->{postParams}->{password}); + Koha::Auth::Component::checkPermissions($borrower, $permissionsRequired) if $permissionsRequired; + return $borrower; +} + +1; \ No newline at end of file diff --git a/Koha/Auth/Route/RESTV1.pm b/Koha/Auth/Route/RESTV1.pm new file mode 100644 index 0000000..fd945e1 --- /dev/null +++ b/Koha/Auth/Route/RESTV1.pm @@ -0,0 +1,41 @@ +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::Component; + +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::Component::checkRESTMaintenance() if $routeParams->{inREST}; #NOT IMPLEMENTED YET + Koha::Auth::Component::checkVersion(); + my $borrower = Koha::Auth::Component::checkRESTV1($rae->{headers}); + Koha::Auth::Component::checkPermissions($borrower, $permissionsRequired) if $permissionsRequired; + return $borrower; +} + +1; \ No newline at end of file diff --git a/Koha/AuthUtils.pm b/Koha/AuthUtils.pm index 7d01e28..8845664 100644 --- a/Koha/AuthUtils.pm +++ b/Koha/AuthUtils.pm @@ -24,6 +24,8 @@ use Fcntl qw/O_RDONLY/; # O_RDONLY is used in generate_salt use Koha::Borrower; +use Koha::Exception::LoginFailed; + use base 'Exporter'; our $VERSION = '1.01'; @@ -136,6 +138,66 @@ sub generate_salt { return $string; } +=head checkHash + + my $passwordOk = Koha::AuthUtils::checkHash($password1, $password2) + +Checks if a clear-text String/password matches the given hash when +MD5 or Bcrypt hashing algorith is applied to it. + +Bcrypt is applied if @PARAM2 starts with '$2' +MD5 otherwise + +@PARAM1 String, clear text passsword or any other String +@PARAM2 String, hashed text password or any other String. +@RETURN Boolean, 1 if given parameters match + , 0 if not +=cut + +sub checkHash { + my ( $password, $stored_hash ) = @_; + + $password = Encode::encode( 'UTF-8', $password ) + if Encode::is_utf8($password); + + return if $stored_hash eq '!'; + + my $hash; + if ( substr( $stored_hash, 0, 2 ) eq '$2' ) { + $hash = hash_password( $password, $stored_hash ); + } else { + #@DEPRECATED Digest::MD5, don't use it or you will get hurt. + require Digest::MD5; + $hash = Digest::MD5::md5_base64($password); + } + return $hash eq $stored_hash; +} + +=head checkKohaSuperuser + + my $borrower = Koha::AuthUtils::checkKohaSuperuser($userid, $password); + +Check if the userid and password match the ones in the $KOHA_CONF +@PARAM1 String, user identifier, either the koha.borrowers.userid, or koha.borrowers.cardnumber +@PARAM2 String, clear text password from the authenticating user +@RETURNS Koha::Borrower branded as superuser with ->isSuperuser() + or undef if user logging in is not a superuser. +@THROWS Koha::Exception::LoginFailed if user identifier matches, but password doesn't +=cut + +sub checkKohaSuperuser { + my ($userid, $password) = @_; + + if ( $userid && $userid eq C4::Context->config('user') ) { + if ( $password && $password eq C4::Context->config('pass') ) { + return _createTemporarySuperuser(); + } + else { + Koha::Exception::LoginFailed->throw(error => "Password authentication failed"); + } + } +} + =head checkKohaSuperuserFromUserid See checkKohaSuperuser(), with only the "user identifier"-@PARAM. @THROWS nothing. @@ -153,13 +215,14 @@ sub checkKohaSuperuserFromUserid { Create a temporary superuser which should be instantiated only to the environment and then discarded. So do not ->store() it! -@RETURN Koha::Borrower +@RETURN Koha::Borrower, stamped as superuser. =cut sub _createTemporarySuperuser { my $borrower = Koha::Borrower->new(); my $superuserName = C4::Context->config('user'); + $borrower->isSuperuser(1); $borrower->set({borrowernumber => 0, userid => $superuserName, cardnumber => $superuserName, diff --git a/Koha/Borrower.pm b/Koha/Borrower.pm index 93426bf..4ceae11 100644 --- a/Koha/Borrower.pm +++ b/Koha/Borrower.pm @@ -43,6 +43,32 @@ sub type { return 'Borrower'; } +=head isSuperuser + + $borrower->isSuperuser(1); #Set this borrower to be a superuser + if ($borrower->isSuperuser()) { + #All your base are belong to us + } + +Should be used from the authentication modules to mark this $borrower-object to +have unlimited access to all Koha-features. +This $borrower-object is the Koha DB user. +@PARAM1 Integer, 1 means this borrower is the super/DB user. + "0" disables the previously set superuserness. +=cut + +sub isSuperuser { + my ($self, $Iam) = @_; + + if (defined $Iam && $Iam == 1) { + $self->{superuser} = 1; + } + elsif (defined $Iam && $Iam eq "0") { #Dealing with zero is special in Perl + $self->{superuser} = undef; + } + return (exists($self->{superuser}) && $self->{superuser}) ? 1 : undef; +} + =head1 AUTHOR Kyle M Hall diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt index 9ae147e..d3dc043 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt @@ -43,7 +43,9 @@
[% FOREACH INPUT IN INPUTS %] - + [% UNLESS INPUT.name == 'logout.x' #No reason to send the logout-signal again %] + + [% END %] [% END %]

diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt index c474791..81f8c10 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt @@ -142,7 +142,9 @@

[% FOREACH INPUT IN INPUTS %] - + [% UNLESS INPUT.name == 'logout.x' #No reason to send the logout-signal again %] + + [% END %] [% END %] diff --git a/opac/opac-user.pl b/opac/opac-user.pl index d4fbbe3..c59931d 100755 --- a/opac/opac-user.pl +++ b/opac/opac-user.pl @@ -349,7 +349,7 @@ foreach my $res (@reserves) { $template->param( WAITING => \@waiting ); # current alert subscriptions -my $alerts = getalert($borrowernumber); +my $alerts = getalert($borrowernumber) if $borrowernumber; #Superuser has no borrowernumber foreach ( @$alerts ) { $_->{ $_->{type} } = 1; $_->{relatedto} = findrelatedto( $_->{type}, $_->{externalid} ); diff --git a/t/db_dependent/Auth_with_ldap.t b/t/db_dependent/Auth_with_ldap.t index 4982248..5c648f5 100755 --- a/t/db_dependent/Auth_with_ldap.t +++ b/t/db_dependent/Auth_with_ldap.t @@ -74,7 +74,7 @@ subtest "checkpw_ldap tests" => sub { ## Connection fail tests $desired_connection_result = 'error'; - warning_is { $ret = C4::Auth_with_ldap::checkpw_ldap( $dbh, 'hola', password => 'hey' ) } + warning_is { $ret = C4::Auth_with_ldap::checkpw_ldap( 'hola', password => 'hey' ) } "LDAP connexion failed", "checkpw_ldap prints correct warning if LDAP conexion fails"; is( $ret, 0, "checkpw_ldap returns 0 if LDAP conexion fails"); @@ -96,7 +96,7 @@ subtest "checkpw_ldap tests" => sub { warning_like { $ret = C4::Auth_with_ldap::checkpw_ldap( - $dbh, 'hola', password => 'hey' ) } + 'hola', password => 'hey' ) } qr/Anonymous LDAP bind failed: LDAP error #1: error_name/, "checkpw_ldap prints correct warning if LDAP anonymous bind fails"; is( $ret, 0, "checkpw_ldap returns 0 if LDAP anonymous bind fails"); @@ -108,14 +108,14 @@ subtest "checkpw_ldap tests" => sub { $desired_count_result = 0; # user auth problem $non_anonymous_bind_result = 'success'; reload_ldap_module(); - is ( C4::Auth_with_ldap::checkpw_ldap( $dbh, 'hola', password => 'hey' ), + is ( C4::Auth_with_ldap::checkpw_ldap( 'hola', password => 'hey' ), 0, "checkpw_ldap returns 0 if user lookup returns 0"); $non_anonymous_bind_result = 'error'; reload_ldap_module(); warning_like { $ret = C4::Auth_with_ldap::checkpw_ldap( - $dbh, 'hola', password => 'hey' ) } + 'hola', password => 'hey' ) } qr/LDAP bind failed as kohauser hola: LDAP error #1: error_name/, "checkpw_ldap prints correct warning if LDAP bind fails"; is ( $ret, -1, "checkpw_ldap returns -1 LDAP bind fails for user (Bug 8148)"); @@ -130,7 +130,7 @@ subtest "checkpw_ldap tests" => sub { reload_ldap_module(); warning_like { $ret = C4::Auth_with_ldap::checkpw_ldap( - $dbh, 'hola', password => 'hey' ) } + 'hola', password => 'hey' ) } qr/LDAP bind failed as kohauser hola: LDAP error #1: error_name/, "checkpw_ldap prints correct warning if LDAP bind fails"; is ( $ret, 0, "checkpw_ldap returns 0 LDAP bind fails for user (Bug 12831)"); @@ -150,7 +150,7 @@ subtest "checkpw_ldap tests" => sub { reload_ldap_module(); warning_like { $ret = C4::Auth_with_ldap::checkpw_ldap( - $dbh, 'hola', password => 'hey' ) } + 'hola', password => 'hey' ) } qr/LDAP bind failed as ldapuser cn=Manager,dc=metavore,dc=com: LDAP error #1: error_name/, "checkpw_ldap prints correct warning if LDAP bind fails"; is ( $ret, 0, "checkpw_ldap returns 0 if bind fails"); @@ -162,7 +162,7 @@ subtest "checkpw_ldap tests" => sub { reload_ldap_module(); warning_like { $ret = C4::Auth_with_ldap::checkpw_ldap( - $dbh, 'hola', password => 'hey' ) } + 'hola', password => 'hey' ) } qr/LDAP Auth rejected : invalid password for user 'hola'. LDAP error #1: error_name/, "checkpw_ldap prints correct warning if LDAP bind fails"; is ( $ret, -1, "checkpw_ldap returns -1 if bind fails (Bug 8148)"); @@ -175,7 +175,7 @@ subtest "checkpw_ldap tests" => sub { reload_ldap_module(); warning_like { $ret = C4::Auth_with_ldap::checkpw_ldap( - $dbh, 'hola', password => 'hey' ) } + 'hola', password => 'hey' ) } qr/LDAP bind failed as ldapuser cn=Manager,dc=metavore,dc=com: LDAP error #1: error_name/, "checkpw_ldap prints correct warning if LDAP bind fails"; is ( $ret, 0, "checkpw_ldap returns 0 if bind fails"); @@ -187,7 +187,7 @@ subtest "checkpw_ldap tests" => sub { reload_ldap_module(); warning_like { $ret = C4::Auth_with_ldap::checkpw_ldap( - $dbh, 'hola', password => 'hey' ) } + 'hola', password => 'hey' ) } qr/LDAP Auth rejected : invalid password for user 'hola'. LDAP error #1: error_name/, "checkpw_ldap prints correct warning if LDAP bind fails"; is ( $ret, -1, "checkpw_ldap returns -1 if bind fails (Bug 8148)"); diff --git a/t/db_dependent/Koha/Borrower.t b/t/db_dependent/Koha/Borrower.t new file mode 100644 index 0000000..75f985a --- /dev/null +++ b/t/db_dependent/Koha/Borrower.t @@ -0,0 +1,55 @@ +#!/usr/bin/perl + +# Copyright 2015 Open Source Freedom Fighters +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Test::More; #Please don't set the test count here. It is nothing but trouble when rebasing against master and is of dubious help. + +use Koha::Borrower; + + + +testIsSuperuser(); + + + + + +################################################################################ +#### Define test subroutines here ############################################## +################################################################################ + +=head testIsSuperuser +@UNIT_TEST +Tests Koha::Borrower->isSuperuser() +=cut + +sub testIsSuperuser { + my $borrower = Koha::Borrower->new(); + ok((not(defined($borrower->isSuperuser()))), "isSuperuser(): By default user is not defined as superuser."); + ok(($borrower->isSuperuser(1) == 1), "isSuperuser(): Setting user as superuser returns 1."); + ok(($borrower->isSuperuser() == 1), "isSuperuser(): Getting superuser status from a superuser returns 1."); + ok((not(defined($borrower->isSuperuser(0)))), "isSuperuser(): Removing superuser status from a superuser OK and returns undef"); + ok((not(defined($borrower->isSuperuser()))), "isSuperuser(): Ex-superuser superuser status is undef"); +} + + + + +####################### +done_testing(); #YAY!! +####################### \ No newline at end of file -- 1.9.1