@@ -, +, @@ interface correctly displayed under the button, and they are active by default API keys". Click on it. --- Koha/ApiKey.pm | 46 ++++++++ Koha/ApiKeys.pm | 121 ++++++++++++++++++++ Koha/Borrower.pm | 32 ++++++ installer/data/mysql/kohastructure.sql | 19 ++++ installer/data/mysql/updatedatabase.pl | 27 +++++ .../prog/en/includes/members-toolbar.inc | 5 + .../prog/en/modules/members/apikeys.tt | 78 +++++++++++++ .../opac-tmpl/bootstrap/en/includes/usermenu.inc | 8 ++ .../opac-tmpl/bootstrap/en/modules/opac-apikeys.tt | 82 ++++++++++++++ members/apikeys.pl | 82 ++++++++++++++ opac/opac-apikeys.pl | 81 ++++++++++++++ t/db_dependent/Koha/ApiKeys.t | 124 +++++++++++++++++++++ 12 files changed, 705 insertions(+) create mode 100644 Koha/ApiKey.pm create mode 100644 Koha/ApiKeys.pm create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/members/apikeys.tt create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-apikeys.tt create mode 100755 members/apikeys.pl create mode 100755 opac/opac-apikeys.pl create mode 100644 t/db_dependent/Koha/ApiKeys.t --- a/Koha/ApiKey.pm +++ a/Koha/ApiKey.pm @@ -0,0 +1,46 @@ +package Koha::ApiKey; + +# Copyright BibLibre 2015 +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Carp; + +use Koha::Database; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::ApiKey - Koha API Key Object class + +=head1 API + +=head2 Class Methods + +=cut + +=head3 type + +=cut + +sub type { + return 'ApiKey'; +} + +1; --- a/Koha/ApiKeys.pm +++ a/Koha/ApiKeys.pm @@ -0,0 +1,121 @@ +package Koha::ApiKeys; + +# Copyright BibLibre 2015 +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; +use Scalar::Util qw(blessed); + +use Koha::Borrowers; +use Koha::ApiKey; + +use base qw(Koha::Objects); + +use Koha::Exception::BadParameter; +use Koha::Exception::UnknownObject; + +=head1 NAME + +Koha::ApiKeys - Koha API Keys Object class + +=head1 API + +=head2 Class Methods + +=cut + +=head3 type + +=cut + +sub type { + return 'ApiKey'; +} + +sub object_class { + return 'Koha::ApiKey'; +} + +sub _get_castable_unique_columns { + return ['api_key_id', 'api_key']; +} + +=head grant + + my $apiKey = Koha::ApiKey->grant({borrower => $borrower, + apiKey => $apiKey + }); + +Granting an ApiKey should be easy. This creates a new ApiKey for the given Borrower, +or sets the owner of an existing key. +$PARAM1 HASHRef of params, { + borrower => MANDATORY, a Koha::Borrower or something castable to one. + apiKey => OPTIONAL, an existing Koha::ApiKEy to give to somebody else. + not sure why anybody would want to do that, but + provided as a convenience for testing. +} +@THROWS Koha::Exception::BadParameter +=cut + +sub grant { + my ($self, $borrower, $apiKey) = @_; + $borrower = Koha::Borrowers->cast($borrower); + if ($apiKey) { + $apiKey = Koha::ApiKeys->cast($apiKey); + $apiKey->borrowernumber($borrower->borrowernumber); + } + else { + $apiKey = new Koha::ApiKey; + $apiKey->borrowernumber($borrower->borrowernumber); + $apiKey->api_key(String::Random->new->randregex('[a-zA-Z0-9]{32}')); + } + $apiKey->store; + return $apiKey; +} + +sub delete { + my ($self, $apiKey) = @_; + $apiKey = Koha::ApiKeys->cast($apiKey); + + if ($apiKey) { + $apiKey->delete; + } +} + +sub revoke { + my ($self, $apiKey) = @_; + $apiKey = Koha::ApiKeys->cast($apiKey); + + if ($apiKey) { + $apiKey->active(0); + $apiKey->store; + } + return $apiKey; +} + +sub activate { + my ($self, $apiKey) = @_; + $apiKey = Koha::ApiKeys->cast($apiKey); + + if ($apiKey) { + $apiKey->active(1); + $apiKey->store; + } + return $apiKey; +} + +1; --- a/Koha/Borrower.pm +++ a/Koha/Borrower.pm @@ -43,6 +43,38 @@ sub type { return 'Borrower'; } +=head getApiKeys + + my @apiKeys = $borrower->getApiKeys( $activeOnly ); + +=cut + +sub getApiKeys { + my ($self, $activeOnly) = @_; + + my @dbix_objects = $self->_result()->api_keys({active => 1}); + for (my $i=0 ; $i_new_from_dbic($dbix_objects[$i]); + } + + return \@dbix_objects; +} + +=head getApiKey + + my $apiKey = $borrower->getApiKeys( $activeOnly ); + +=cut + +sub getApiKey { + my ($self, $activeOnly) = @_; + + my $dbix_object = $self->_result()->api_keys({active => 1})->next(); + my $object = Koha::ApiKey->_new_from_dbic($dbix_object); + + return $object; +} + =head1 AUTHOR Kyle M Hall --- a/installer/data/mysql/kohastructure.sql +++ a/installer/data/mysql/kohastructure.sql @@ -16,6 +16,25 @@ /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- +-- Table structure for table api_keys +-- + +DROP TABLE IF EXISTS api_keys; +CREATE TABLE api_keys ( + api_key_id INT(11) NOT NULL auto_increment, + borrowernumber INT(11) NOT NULL, -- foreign key to the borrowers table + api_key VARCHAR(255) NOT NULL, -- API key used for API authentication + last_request_time INT(11) default 0, -- UNIX timestamp of when was the last transaction for this API-key? Used for request replay control. + active INT(1) DEFAULT 1, -- 0 means this API key is revoked + PRIMARY KEY (api_key_id), + UNIQUE KEY apk_bornumkey_idx (borrowernumber, api_key), + CONSTRAINT api_keys_fk_borrowernumber + FOREIGN KEY (borrowernumber) + REFERENCES borrowers (borrowernumber) + ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +-- -- Table structure for table `auth_header` -- --- a/installer/data/mysql/updatedatabase.pl +++ a/installer/data/mysql/updatedatabase.pl @@ -10776,6 +10776,33 @@ if ( CheckVersion($DBversion) ) { SetVersion ($DBversion); } +$DBversion = "XXX"; +if(CheckVersion($DBversion)) { + $dbh->do(q{ + CREATE TABLE api_keys ( + api_key_id INT(11) NOT NULL auto_increment, + borrowernumber INT(11) NOT NULL, -- foreign key to the borrowers table + api_key VARCHAR(255) NOT NULL, -- API key used for API authentication + last_request_time INT(11) default 0, -- UNIX timestamp of when was the last transaction for this API-key? Used for request replay control. + active INT(1) DEFAULT 1, -- 0 means this API key is revoked + PRIMARY KEY (api_key_id), + UNIQUE KEY apk_bornumkey_idx (borrowernumber, api_key), + CONSTRAINT api_keys_fk_borrowernumber + FOREIGN KEY (borrowernumber) + REFERENCES borrowers (borrowernumber) + ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + }); + + use Koha::Auth::PermissionManager; + my $pm = Koha::Auth::PermissionManager->new(); + $pm->addPermission({module => 'borrowers', code => 'manage_api_keys', description => "Manage Borrowers' REST API keys"}); + + print "Upgrade to $DBversion done (Bug 13920: Add API keys table)\n"; + SetVersion($DBversion); +} + + # DEVELOPER PROCESS, search for anything to execute in the db_update directory # SEE bug 13068 # if there is anything in the atomicupdate, read and execute it. --- a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc @@ -170,6 +170,11 @@ function searchToHold(){ [% ELSE %]
  • Set permissions
  • [% END %] + [% IF ( CAN_user_borrowers_manage_api_keys ) %] +
  • Manage API keys
  • + [% ELSE %] +
  • Manage API keys
  • + [% END %] [% IF ( CAN_user_borrowers ) %] [% IF ( NorwegianPatronDBEnable == 1 ) %]
  • Delete local
  • --- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/apikeys.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/members/apikeys.tt @@ -0,0 +1,78 @@ +[% USE Koha %] +[% INCLUDE 'doc-head-open.inc' %] +Koha › Patrons › API Keys +[% INCLUDE 'doc-head-close.inc' %] + + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'patron-search.inc' %] + + + +
    +
    +
    +
    + [% INCLUDE 'members-toolbar.inc' %] + +

    API keys for [% INCLUDE 'patron-title.inc' %]

    +
    +
    + + + +
    +
    + [% IF api_keys.size > 0 %] + + + + + + + + + + + [% FOREACH key IN api_keys %] + + + + + + + [% END %] + +
    KeyActiveLast transactionActions
    [% key.api_key %][% IF key.active %]Yes[% ELSE %]No[% END %][% key.last_request_time || '' %] +
    + + + + +
    +
    + + + [% IF key.active %] + + + [% ELSE %] + + + [% END %] +
    +
    + [% END %] +
    +
    +
    + [% INCLUDE 'circ-menu.inc' %] +
    +
    +[% INCLUDE 'intranet-bottom.inc' %] --- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc +++ a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc @@ -1,3 +1,4 @@ +[% USE Koha %] [% IF ( ( Koha.Preference( 'opacuserlogin' ) == 1 ) && loggedinusername ) %] [% END %] --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-apikeys.tt +++ a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-apikeys.tt @@ -0,0 +1,82 @@ +[% INCLUDE 'doc-head-open.inc' %] +[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog › Your library home › Your API keys +[% INCLUDE 'doc-head-close.inc' %] +[% BLOCK cssinclude %][% END %] + +[% INCLUDE 'bodytag.inc' bodyid='opac-user' bodyclass='scrollto' %] +[% INCLUDE 'masthead.inc' %] + +
    + + +
    +
    +
    + +
    +
    +
    +

    Your API keys

    +
    +
    + + +
    +
    + [% IF api_keys.size > 0 %] + + + + + + + + + + + [% FOREACH key IN api_keys %] + + + + + + + [% END %] + +
    KeyActiveLast transactionActions
    [% key.api_key %][% IF key.active %]Yes[% ELSE %]No[% END %][% key.last_request_time || '' %] +
    + + + +
    +
    + + [% IF key.active %] + + + [% ELSE %] + + + [% END %] +
    +
    + [% END %] +
    +
    +
    +
    +
    + +[% BLOCK jsinclude %][% END %] +[% INCLUDE 'opac-bottom.inc' %] --- a/members/apikeys.pl +++ a/members/apikeys.pl @@ -0,0 +1,82 @@ +#!/usr/bin/perl + +# Copyright 2015 BibLibre +# +# 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 2 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use CGI; +use String::Random; + +use C4::Auth; +use C4::Members; +use C4::Output; +use Koha::ApiKeys; +use Koha::ApiKey; + +my $cgi = new CGI; + +my ($template, $loggedinuser, $cookie) = get_template_and_user({ + template_name => 'members/apikeys.tt', + query => $cgi, + type => 'intranet', + authnotrequired => 0, + flagsrequired => {borrowers => 'manage_api_keys'}, +}); + +my $borrowernumber = $cgi->param('borrowernumber'); +my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber); +my $op = $cgi->param('op'); + +if ($op) { + if ($op eq 'generate') { + Koha::ApiKeys->grant($borrower); + print $cgi->redirect('/cgi-bin/koha/members/apikeys.pl?borrowernumber=' . $borrowernumber); + exit; + } + + if ($op eq 'delete') { + my $key = $cgi->param('key'); + Koha::ApiKeys->delete($key); + print $cgi->redirect('/cgi-bin/koha/members/apikeys.pl?borrowernumber=' . $borrowernumber); + exit; + } + + if ($op eq 'revoke') { + my $key = $cgi->param('key'); + Koha::ApiKeys->revoke($key); + print $cgi->redirect('/cgi-bin/koha/members/apikeys.pl?borrowernumber=' . $borrowernumber); + exit; + } + + if ($op eq 'activate') { + my $key = $cgi->param('key'); + Koha::ApiKeys->activate($key); + print $cgi->redirect('/cgi-bin/koha/members/apikeys.pl?borrowernumber=' . $borrowernumber); + exit; + } +} + +my @api_keys = Koha::ApiKeys->search({borrowernumber => $borrowernumber}); + +$template->param( + api_keys => \@api_keys, + borrower => $borrower, + borrowernumber => $borrowernumber, +); + +output_html_with_http_headers $cgi, $cookie, $template->output; --- a/opac/opac-apikeys.pl +++ a/opac/opac-apikeys.pl @@ -0,0 +1,81 @@ +#!/usr/bin/env perl + +# Copyright 2015 BibLibre +# +# 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 2 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use CGI; +use String::Random; + +use C4::Auth; +use C4::Members; +use C4::Output; +use Koha::ApiKeys; + +my $cgi = new CGI; + +my ($template, $loggedinuser, $cookie) = get_template_and_user({ + template_name => 'opac-apikeys.tt', + query => $cgi, + type => 'opac', + authnotrequired => 0, +}); + +my $borrowernumber = $loggedinuser; +my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber); +my $op = $cgi->param('op'); + +if ($op) { + if ($op eq 'generate') { + Koha::ApiKeys->grant($borrower); + print $cgi->redirect('/cgi-bin/koha/opac-apikeys.pl'); + exit; + } + + if ($op eq 'delete') { + my $key = $cgi->param('key'); + Koha::ApiKeys->delete($key); + print $cgi->redirect('/cgi-bin/koha/opac-apikeys.pl'); + exit; + } + + if ($op eq 'revoke') { + my $key = $cgi->param('key'); + Koha::ApiKeys->revoke($key); + print $cgi->redirect('/cgi-bin/koha/opac-apikeys.pl'); + exit; + } + + if ($op eq 'activate') { + my $key = $cgi->param('key'); + Koha::ApiKeys->activate($key); + print $cgi->redirect('/cgi-bin/koha/opac-apikeys.pl'); + exit; + } +} + +my @api_keys = Koha::ApiKeys->search({borrowernumber => $borrowernumber}); + +$template->param( + apikeysview => 1, + api_keys => \@api_keys, + borrower => $borrower, + borrowernumber => $borrowernumber, +); + +output_html_with_http_headers $cgi, $cookie, $template->output; --- a/t/db_dependent/Koha/ApiKeys.t +++ a/t/db_dependent/Koha/ApiKeys.t @@ -0,0 +1,124 @@ +#!/usr/bin/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 . + +use Modern::Perl; +use Test::More; +use Try::Tiny; +use Scalar::Util qw(blessed); + +use Koha::Auth::PermissionManager; +use Koha::ApiKeys; + +use t::lib::TestObjects::ObjectFactory; +use t::lib::TestObjects::BorrowerFactory; +use t::lib::Page::Members::Moremember; +use t::lib::Page::Opac::OpacMain; + +##Setting up the test context +my $testContext = {}; + +my $password = '1234'; +my $borrowerFactory = t::lib::TestObjects::BorrowerFactory->new(); +my $borrowers = $borrowerFactory->createTestGroup([ + {firstname => 'Olli-Antti', + surname => 'Kivi', + cardnumber => '1A01', + branchcode => 'CPL', + password => $password, + }, + {firstname => 'Alli-Ontti', + surname => 'Ivik', + cardnumber => '1A02', + branchcode => 'CPL', + password => $password, + }, + ], undef, $testContext); +my $borrowerKivi = $borrowers->{'1A01'}; +my $borrowerIvik = $borrowers->{'1A02'}; +my $permissionManager = Koha::Auth::PermissionManager->new(); +$permissionManager->grantPermission($borrowerKivi, 'borrowers', 'manage_api_keys'); + + +##Test context set, starting testing: +eval { #run in a eval-block so we don't die without tearing down the test context +subtest "ApiKeys API Unit tests" => sub { + my $borrowerKivi = $borrowers->{'1A01'}; + my $borrowerIvik = $borrowers->{'1A02'}; + + + my $apiKey = Koha::ApiKeys->grant($borrowerKivi); + is($apiKey->borrowernumber, $borrowerKivi->borrowernumber, "ApiKey granted"); + + Koha::ApiKeys->revoke($apiKey); + is($apiKey->active, 0, "ApiKey revoked"); + + Koha::ApiKeys->activate($apiKey); + is($apiKey->active, 1, "ApiKey activated"); + + Koha::ApiKeys->grant($borrowerIvik, $apiKey); + is($apiKey->borrowernumber, $borrowerIvik->borrowernumber, "ApiKey granted to another Borrower"); + + Koha::ApiKeys->delete($apiKey); + $apiKey = Koha::ApiKeys->find({api_key_id => $apiKey->api_key_id}); + ok(not($apiKey), "ApiKey deleted"); +} +}; +if ($@) { #Catch all leaking errors and gracefully terminate. + warn $@; + tearDown(); + exit 1; +} + +eval { +subtest "ApiKeys Intra Integration tests" => sub { + my $agent = t::lib::Page::Members::Moremember->new({borrowernumber => $borrowerKivi->borrowernumber}); + $agent->doPasswordLogin($borrowerKivi->userid, $password)->navigateManageApiKeys()->generateNewApiKey(); + my @apiKeys = Koha::ApiKeys->search({borrowernumber => $borrowerKivi->borrowernumber}); + $agent->revokeApiKey($apiKeys[0]->api_key)->deleteApiKey($apiKeys[0]->api_key) + ->quit(); +} +}; +if ($@) { #Catch all leaking errors and gracefully terminate. + warn $@; + tearDown(); + exit 1; +} + +eval { +subtest "ApiKeys OPAC Integration tests" => sub { + my $agent = t::lib::Page::Opac::OpacMain->new(); + $agent->doPasswordLogin($borrowerKivi->userid, $password)->navigateYourAPIKeys()->generateNewApiKey(); + my @apiKeys = Koha::ApiKeys->search({borrowernumber => $borrowerKivi->borrowernumber}); + $agent->revokeApiKey($apiKeys[0]->api_key)->deleteApiKey($apiKeys[0]->api_key) + ->quit(); +} +}; +if ($@) { #Catch all leaking errors and gracefully terminate. + warn $@; + tearDown(); + exit 1; +} + +##All tests done, tear down test context +tearDown(); +done_testing; + +sub tearDown { + t::lib::TestObjects::ObjectFactory->tearDownTestContext($testContext); +} --