Bug 20568: API keys management in interface
authorJulian Maurice <julian.maurice@biblibre.com>
Mon, 23 Mar 2015 19:14:23 +0000 (20:14 +0100)
committerJonathan Druart <jonathan.druart@bugs.koha-community.org>
Wed, 9 May 2018 15:55:58 +0000 (12:55 -0300)
This introduces the concept of API keys for use in the new REST API.
A key is a string of 32 alphanumerical characters (32 is purely
arbitrary, it can be changed easily).
A user can have multiple keys (unlimited at the moment)
Keys can be generated automatically, and then we have the possibility to
delete or revoke each one individually.

Test plan:
1/ Go to staff interface
2/ Go to a borrower page
3/ In toolbar, click on More -> Manage API keys
4/ Click on "Generate new key" multiple times, check that they are
   correctly displayed under the button, and they are active by default
5/ Revoke some keys, check that they are not active anymore
6/ Delete some keys, check that they disappear from table
7/ Go to opac interface, log in
8/ In your user account pages, you now have a new tab to the left "your
   API keys". Click on it.
9/ Repeat steps 4-6

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Julian Maurice <julian.maurice@biblibre.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>

Koha/ApiKey.pm [new file with mode: 0644]
Koha/ApiKeys.pm [new file with mode: 0644]
Koha/Schema/Result/ApiKey.pm [new file with mode: 0644]
installer/data/mysql/kohastructure.sql
koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc
koha-tmpl/intranet-tmpl/prog/en/modules/members/apikeys.tt [new file with mode: 0644]
koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc
koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-apikeys.tt [new file with mode: 0644]
members/apikeys.pl [new file with mode: 0755]
opac/opac-apikeys.pl [new file with mode: 0755]

diff --git a/Koha/ApiKey.pm b/Koha/ApiKey.pm
new file mode 100644 (file)
index 0000000..be4b76e
--- /dev/null
@@ -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;
diff --git a/Koha/ApiKeys.pm b/Koha/ApiKeys.pm
new file mode 100644 (file)
index 0000000..8b25820
--- /dev/null
@@ -0,0 +1,52 @@
+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 Carp;
+
+use Koha::Database;
+
+use Koha::Borrower;
+
+use base qw(Koha::Objects);
+
+=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';
+}
+
+1;
diff --git a/Koha/Schema/Result/ApiKey.pm b/Koha/Schema/Result/ApiKey.pm
new file mode 100644 (file)
index 0000000..6767a56
--- /dev/null
@@ -0,0 +1,92 @@
+use utf8;
+package Koha::Schema::Result::ApiKey;
+
+# Created by DBIx::Class::Schema::Loader
+# DO NOT MODIFY THE FIRST PART OF THIS FILE
+
+=head1 NAME
+
+Koha::Schema::Result::ApiKey
+
+=cut
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+=head1 TABLE: C<api_keys>
+
+=cut
+
+__PACKAGE__->table("api_keys");
+
+=head1 ACCESSORS
+
+=head2 borrowernumber
+
+  data_type: 'integer'
+  is_foreign_key: 1
+  is_nullable: 0
+
+=head2 api_key
+
+  data_type: 'varchar'
+  is_nullable: 0
+  size: 255
+
+=head2 active
+
+  data_type: 'integer'
+  default_value: 1
+  is_nullable: 1
+
+=cut
+
+__PACKAGE__->add_columns(
+  "borrowernumber",
+  { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
+  "api_key",
+  { data_type => "varchar", is_nullable => 0, size => 255 },
+  "active",
+  { data_type => "integer", default_value => 1, is_nullable => 1 },
+);
+
+=head1 PRIMARY KEY
+
+=over 4
+
+=item * L</borrowernumber>
+
+=item * L</api_key>
+
+=back
+
+=cut
+
+__PACKAGE__->set_primary_key("borrowernumber", "api_key");
+
+=head1 RELATIONS
+
+=head2 borrowernumber
+
+Type: belongs_to
+
+Related object: L<Koha::Schema::Result::Borrower>
+
+=cut
+
+__PACKAGE__->belongs_to(
+  "borrowernumber",
+  "Koha::Schema::Result::Borrower",
+  { borrowernumber => "borrowernumber" },
+  { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
+);
+
+
+# Created by DBIx::Class::Schema::Loader v0.07025 @ 2015-03-24 07:35:30
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:dvujXVM5Vfu3SA2UfiVPtw
+
+
+# You can replace this text with custom code or comments, and it will be preserved on regeneration
+1;
index ea59d03..7dad2e7 100644 (file)
 /*!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 (
+    borrowernumber int(11) NOT NULL, -- foreign key to the borrowers table
+    api_key VARCHAR(255) NOT NULL, -- API key used for API authentication
+    active int(1) DEFAULT 1, -- 0 means this API key is revoked
+    PRIMARY KEY (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`
 --
 
index 467997f..138ef92 100644 (file)
                 [% ELSE %]
                     <li class="disabled"><a data-toggle="tooltip" data-placement="left" title="You are not authorized to set permissions" id="patronflags" href="#">Set permissions</a></li>
                 [% END %]
+
                 [% IF CAN_user_borrowers_edit_borrowers && useDischarge %]
                     <li><a href="/cgi-bin/koha/members/discharge.pl?borrowernumber=[% patron.borrowernumber %]">Discharge</a></li>
                 [% END %]
                 [% IF CAN_user_borrowers_edit_borrowers %]
+
+                [% IF ( CAN_user_borrowers ) %]
+                    <li><a id="apikeys" href="/cgi-bin/koha/members/apikeys.pl?borrowernumber=[% borrowernumber %]">Manage API keys</a></li>
+                [% ELSE %]
+                    <li class="disabled"><a data-toggle="tooltip" data-placement="left" title="You are not authorized to manage API keys" id="apikeys" href="#">Manage API keys</a></li>
+                [% END %]
+
+                [% IF ( CAN_user_borrowers ) %]
                     [% IF ( NorwegianPatronDBEnable == 1 ) %]
                         <li><a id="deletepatronlocal" href="#">Delete local</a></li>
                         <li><a id="deletepatronremote" href="#">Delete remote</a></li>
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/apikeys.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/apikeys.tt
new file mode 100644 (file)
index 0000000..e5131fc
--- /dev/null
@@ -0,0 +1,76 @@
+[% USE Koha %]
+[% INCLUDE 'doc-head-open.inc' %]
+<title>Koha &rsaquo; Patrons [% IF ( searching ) %]&rsaquo; API Keys[% END %]</title>
+[% INCLUDE 'doc-head-close.inc' %]
+</head>
+<body id="pat_apikeys" class="pat">
+[% INCLUDE 'header.inc' %]
+[% INCLUDE 'patron-search.inc' %]
+
+<div id="breadcrumbs">
+  <a href="/cgi-bin/koha/mainpage.pl">Home</a>
+  &rsaquo;
+  <a href="/cgi-bin/koha/members/members-home.pl">Patrons</a>
+  &rsaquo;
+  API Keys for [% INCLUDE 'patron-title.inc' %]
+</div>
+
+<div id="doc3" class="yui-t2">
+  <div id="bd">
+    <div id="yui-main">
+      <div class="yui-b">
+        [% INCLUDE 'members-toolbar.inc' %]
+
+        <h1>API keys for [% INCLUDE 'patron-title.inc' %]</h1>
+        <div>
+          <form action="/cgi-bin/koha/members/apikeys.pl" method="post">
+            <input type="hidden" name="borrowernumber" value="[% borrowernumber %]">
+            <input type="hidden" name="op" value="generate">
+            <input type="submit" value="Generate new key">
+          </form>
+        </div>
+        [% IF api_keys.size > 0 %]
+          <table>
+            <thead>
+              <tr>
+                <th>Key</th>
+                <th>Active</th>
+                <th>Actions</th>
+              </tr>
+            </thead>
+            <tbody>
+              [% FOREACH key IN api_keys %]
+                <tr>
+                  <td>[% key.api_key %]</td>
+                  <td>[% IF key.active %]Yes[% ELSE %]No[% END %]</td>
+                  <td>
+                    <form action="/cgi-bin/koha/members/apikeys.pl" method="post">
+                      <input type="hidden" name="borrowernumber" value="[% borrowernumber %]">
+                      <input type="hidden" name="key" value="[% key.api_key %]">
+                      <input type="hidden" name="op" value="delete">
+                      <input type="submit" value="Delete">
+                    </form>
+                    <form action="/cgi-bin/koha/members/apikeys.pl" method="post">
+                      <input type="hidden" name="borrowernumber" value="[% borrowernumber %]">
+                      <input type="hidden" name="key" value="[% key.api_key %]">
+                      [% IF key.active %]
+                        <input type="hidden" name="op" value="revoke">
+                        <input type="submit" value="Revoke">
+                      [% ELSE %]
+                        <input type="hidden" name="op" value="activate">
+                        <input type="submit" value="Activate">
+                      [% END %]
+                    </form>
+                  </td>
+                </tr>
+              [% END %]
+            </tbody>
+          </table>
+        [% END %]
+      </div>
+    </div>
+    <div class="yui-b">
+      [% INCLUDE 'circ-menu.inc' %]
+    </div>
+  </div>
+[% INCLUDE 'intranet-bottom.inc' %]
index 0783438..2b78450 100644 (file)
@@ -1,3 +1,4 @@
+[% USE Koha %]
 [% IF ( ( Koha.Preference( 'opacuserlogin' ) == 1 ) && loggedinusername ) %]
     <div id="menu">
         <h4><a href="#" class="menu-collapse-toggle">Your account menu</a></h4>
                 [% END %]
                 <a href="/cgi-bin/koha/opac-illrequests.pl">your interlibrary loan requests</a></li>
             [% END %]
+
+            [% IF apikeysview %]
+              <li class="active">
+            [% ELSE %]
+              <li>
+            [% END %]
+              <a href="/cgi-bin/koha/opac-apikeys.pl">your API keys</a>
+            </li>
         </ul>
     </div>
 [% END %]
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-apikeys.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-apikeys.tt
new file mode 100644 (file)
index 0000000..c2235b4
--- /dev/null
@@ -0,0 +1,80 @@
+[% INCLUDE 'doc-head-open.inc' %]
+[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog &rsaquo; Your library home
+[% INCLUDE 'doc-head-close.inc' %]
+[% BLOCK cssinclude %][% END %]
+</head>
+[% INCLUDE 'bodytag.inc' bodyid='opac-user' bodyclass='scrollto' %]
+[% INCLUDE 'masthead.inc' %]
+
+<div class="main">
+    <ul class="breadcrumb">
+        <li><a href="/cgi-bin/koha/opac-main.pl">Home</a> <span class="divider">&rsaquo;</span></li>
+        <li>
+          <a href="/cgi-bin/koha/opac-user.pl">
+            [% INCLUDE 'patron-title.inc' category_type = borrower.category_type firstname = borrower.firstname surname = borrower.surname othernames = borrower.othernames %]
+          </a>
+          <span class="divider">&rsaquo;</span>
+        </li>
+        <li><a href="/cgi-bin/koha/opac-apikeys.pl">Your API keys</a></li>
+    </ul>
+
+    <div class="container-fluid">
+        <div class="row-fluid">
+            <div class="span2">
+                <div id="navigation">
+                    [% INCLUDE 'navigation.inc' IsPatronPage = 1 %]
+                </div>
+            </div>
+            <div class="span10">
+                <div id="apikeys" class="maincontent">
+                  <h1>Your API keys</h1>
+                  <div>
+                    <form action="/cgi-bin/koha/opac-apikeys.pl" method="post">
+                      <input type="hidden" name="op" value="generate">
+                      <input type="submit" value="Generate new key">
+                    </form>
+                  </div>
+                  [% IF api_keys.size > 0 %]
+                    <table class="table table-bordered table-striped">
+                      <thead>
+                        <tr>
+                          <th>Key</th>
+                          <th>Active</th>
+                          <th>Actions</th>
+                        </tr>
+                      </thead>
+                      <tbody>
+                        [% FOREACH key IN api_keys %]
+                          <tr>
+                            <td>[% key.api_key %]</td>
+                            <td>[% IF key.active %]Yes[% ELSE %]No[% END %]</td>
+                            <td>
+                              <form action="/cgi-bin/koha/opac-apikeys.pl" method="post" class="form-inline">
+                                <input type="hidden" name="key" value="[% key.api_key %]">
+                                <input type="hidden" name="op" value="delete">
+                                <input type="submit" value="Delete">
+                              </form>
+                              <form action="/cgi-bin/koha/opac-apikeys.pl" method="post" class="form-inline">
+                                <input type="hidden" name="key" value="[% key.api_key %]">
+                                [% IF key.active %]
+                                  <input type="hidden" name="op" value="revoke">
+                                  <input type="submit" value="Revoke">
+                                [% ELSE %]
+                                  <input type="hidden" name="op" value="activate">
+                                  <input type="submit" value="Activate">
+                                [% END %]
+                              </form>
+                            </td>
+                          </tr>
+                        [% END %]
+                      </tbody>
+                    </table>
+                  [% END %]
+                </div> <!-- /#apikeys -->
+            </div> <!-- /.span10 -->
+        </div> <!-- /.row-fluid -->
+    </div> <!-- /.container-fluid -->
+</div> <!-- /#main -->
+
+[% BLOCK jsinclude %][% END %]
+[% INCLUDE 'opac-bottom.inc' %]
diff --git a/members/apikeys.pl b/members/apikeys.pl
new file mode 100755 (executable)
index 0000000..7893ad3
--- /dev/null
@@ -0,0 +1,96 @@
+#!/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;
+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 => 1},
+});
+
+my $borrowernumber = $cgi->param('borrowernumber');
+my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
+my $op = $cgi->param('op');
+
+if ($op) {
+    if ($op eq 'generate') {
+        my $apikey = new Koha::ApiKey;
+        $apikey->borrowernumber($borrowernumber);
+        $apikey->api_key(String::Random->new->randregex('[a-zA-Z0-9]{32}'));
+        $apikey->store;
+        print $cgi->redirect('/cgi-bin/koha/members/apikeys.pl?borrowernumber=' . $borrowernumber);
+        exit;
+    }
+
+    if ($op eq 'delete') {
+        my $key = $cgi->param('key');
+        my $api_key = Koha::ApiKeys->find({borrowernumber => $borrowernumber, api_key => $key});
+        if ($api_key) {
+            $api_key->delete;
+        }
+        print $cgi->redirect('/cgi-bin/koha/members/apikeys.pl?borrowernumber=' . $borrowernumber);
+        exit;
+    }
+
+    if ($op eq 'revoke') {
+        my $key = $cgi->param('key');
+        my $api_key = Koha::ApiKeys->find({borrowernumber => $borrowernumber, api_key => $key});
+        if ($api_key) {
+            $api_key->active(0);
+            $api_key->store;
+        }
+        print $cgi->redirect('/cgi-bin/koha/members/apikeys.pl?borrowernumber=' . $borrowernumber);
+        exit;
+    }
+
+    if ($op eq 'activate') {
+        my $key = $cgi->param('key');
+        my $api_key = Koha::ApiKeys->find({borrowernumber => $borrowernumber, api_key => $key});
+        if ($api_key) {
+            $api_key->active(1);
+            $api_key->store;
+        }
+        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;
diff --git a/opac/opac-apikeys.pl b/opac/opac-apikeys.pl
new file mode 100755 (executable)
index 0000000..a2008a9
--- /dev/null
@@ -0,0 +1,97 @@
+#!/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;
+use Koha::ApiKey;
+
+my $cgi = new CGI;
+
+my ($template, $loggedinuser, $cookie) = get_template_and_user({
+    template_name => 'opac-apikeys.tt',
+    query => $cgi,
+    type => 'opac',
+    authnotrequired => 0,
+    flagsrequired => {borrow => 1},
+});
+
+my $borrowernumber = $loggedinuser;
+my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
+my $op = $cgi->param('op');
+
+if ($op) {
+    if ($op eq 'generate') {
+        my $apikey = new Koha::ApiKey;
+        $apikey->borrowernumber($borrowernumber);
+        $apikey->api_key(String::Random->new->randregex('[a-zA-Z0-9]{32}'));
+        $apikey->store;
+        print $cgi->redirect('/cgi-bin/koha/opac-apikeys.pl');
+        exit;
+    }
+
+    if ($op eq 'delete') {
+        my $key = $cgi->param('key');
+        my $api_key = Koha::ApiKeys->find({borrowernumber => $borrowernumber, api_key => $key});
+        if ($api_key) {
+            $api_key->delete;
+        }
+        print $cgi->redirect('/cgi-bin/koha/opac-apikeys.pl');
+        exit;
+    }
+
+    if ($op eq 'revoke') {
+        my $key = $cgi->param('key');
+        my $api_key = Koha::ApiKeys->find({borrowernumber => $borrowernumber, api_key => $key});
+        if ($api_key) {
+            $api_key->active(0);
+            $api_key->store;
+        }
+        print $cgi->redirect('/cgi-bin/koha/opac-apikeys.pl');
+        exit;
+    }
+
+    if ($op eq 'activate') {
+        my $key = $cgi->param('key');
+        my $api_key = Koha::ApiKeys->find({borrowernumber => $borrowernumber, api_key => $key});
+        if ($api_key) {
+            $api_key->active(1);
+            $api_key->store;
+        }
+        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;