Z39.50 stored credentials
authorBill Erickson <berick@esilibrary.com>
Wed, 27 Feb 2013 18:08:07 +0000 (13:08 -0500)
committerMike Rylander <mrylander@gmail.com>
Wed, 13 Mar 2013 18:43:26 +0000 (14:43 -0400)
* New non-IDL-accessible DB table for storing credentials
* API for applying credentials
* Additions to the Z39.50 configuration UI for applying and clearing
  credentials.
* At Z39.50 search time, if no creds are provided by the caller, but
  creds are configured in the database, creds from the DB are used to
  make the Z39 search call.
* Release notes included

Signed-off-by: Bill Erickson <berick@esilibrary.com>
Signed-off-by: Mike Rylander <mrylander@gmail.com>
Open-ILS/examples/opensrf_core.xml.example
Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Z3950.pm
Open-ILS/src/sql/Pg/002.schema.config.sql
Open-ILS/src/sql/Pg/800.fkeys.sql
Open-ILS/src/sql/Pg/999.functions.global.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.z3950_credentials.sql [new file with mode: 0644]
Open-ILS/src/templates/conify/global/config/z3950_source.tt2
Open-ILS/web/js/ui/default/conify/global/config/z3950_source.js
docs/RELEASE_NOTES_NEXT/z39_source_credentials.txt [new file with mode: 0644]

index 39ddbf8..6e0d675 100644 (file)
@@ -187,6 +187,7 @@ Example OpenSRF bootstrap configuration file for Evergreen
       <match_string>open-ils.cstore.direct.actor.user.create</match_string>
       <match_string>open-ils.cstore.direct.actor.user.update</match_string>
       <match_string>open-ils.cstore.direct.actor.user.delete</match_string>
+      <match_string>open-ils.search.z3950.apply_credentials</match_string>
     </log_protect>
   </shared>
 </config>
index 978b106..1f17a0c 100644 (file)
@@ -27,6 +27,44 @@ my $sclient;
 my %services;
 my $default_service;
 
+__PACKAGE__->register_method(
+    method    => 'apply_credentials',
+    api_name  => 'open-ils.search.z3950.apply_credentials',
+    signature => {
+        desc   => "Apply credentials for a Z39.50 server",
+        params => [
+            {desc => 'Authtoken', type => 'string'},
+            {desc => 'Z39.50 Source (server) name', type => 'string'},
+            {desc => 'Context org unit', type => 'number'},
+            {desc => 'Username', type => 'string'},
+            {desc => 'Password', type => 'string'}
+        ],
+        return => {
+            desc => 'Event; SUCCESS on success, other event type on error'
+        }
+    }
+);
+
+sub apply_credentials {
+    my ($self, $client, $auth, $source, $ctx_ou, $username, $password) = @_;
+
+    my $e = new_editor(authtoken => $auth, xact => 1);
+
+    return $e->die_event unless 
+        $e->checkauth and 
+        $e->allowed('ADMIN_Z3950_SOURCE', $ctx_ou);
+
+    $e->json_query({from => [
+        'config.z3950_source_credentials_apply',
+        $source, $ctx_ou, $username, $password
+    ]}) or return $e->die_event;
+
+    $e->commit;
+
+    return OpenILS::Event->new('SUCCESS');
+}
+
 
 __PACKAGE__->register_method(
     method    => 'do_class_search',
@@ -314,14 +352,22 @@ sub do_search {
     my $limit = $$args{limit} || 10;
     my $offset = $$args{offset} || 0;
 
-    my $username = $$args{username} || "";
-    my $password = $$args{password} || "";
+    my $editor = new_editor(authtoken => $auth);
+    return $editor->event unless 
+        $editor->checkauth and
+        $editor->allowed('REMOTE_Z3950_QUERY', $editor->requestor->ws_ou);
+
+    my $creds = $editor->json_query({from => [
+        'config.z3950_source_credentials_lookup',
+        $$args{service}, $editor->requestor->ws_ou
+    ]})->[0] || {};
 
-    my $tformat = $services{$args->{service}}->{transmission_format} || $output;
+    # use the caller-provided username/password if offered.
+    # otherwise, use the stored credentials.
+    my $username = $$args{username} || $creds->{username} || "";
+    my $password = $$args{password} || $creds->{password} || "";
 
-    my $editor = new_editor(authtoken => $auth);
-    return $editor->event unless $editor->checkauth;
-    return $editor->event unless $editor->allowed('REMOTE_Z3950_QUERY');
+    my $tformat = $services{$args->{service}}->{transmission_format} || $output;
 
     $logger->info("z3950: connecting to server $host:$port:$db as $username");
 
index 38416d9..6f63a17 100644 (file)
@@ -514,6 +514,15 @@ CREATE TABLE config.z3950_attr (
     CONSTRAINT z_code_format_once_per_source UNIQUE (code,format,source)
 );
 
+CREATE TABLE config.z3950_source_credentials (
+    id SERIAL PRIMARY KEY,
+    owner INTEGER NOT NULL, -- REFERENCES actor.org_unit(id),
+    source TEXT NOT NULL REFERENCES config.z3950_source(name),
+    username TEXT,
+    password TEXT,
+    CONSTRAINT czsc_source_once_per_lib UNIQUE (source, owner)
+);
+
 CREATE TABLE config.i18n_locale (
     code        TEXT    PRIMARY KEY,
     marc_code   TEXT    NOT NULL, -- should exist in config.coded_value_map WHERE ctype = 'item_lang'
index 5314b0e..cf55a30 100644 (file)
@@ -132,6 +132,8 @@ CREATE INDEX by_heading ON authority.record_entry (authority.simple_normalize_he
 
 ALTER TABLE config.z3950_source ADD CONSTRAINT use_perm_fkey FOREIGN KEY (use_perm) REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED;
 
+ALTER TABLE config.z3950_source_credentials ADD CONSTRAINT z3950_source_creds_owner_fkey FOREIGN KEY (owner) REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+
 ALTER TABLE config.org_unit_setting_type_log ADD CONSTRAINT config_org_unit_setting_type_log_fkey FOREIGN KEY (org) REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
 
 ALTER TABLE config.filter_dialog_filter_set
index 7659219..c3f1533 100644 (file)
@@ -2188,3 +2188,51 @@ return $retval;
 $BODY$ LANGUAGE plperlu IMMUTABLE STRICT COST 100;
 
 -- user activity functions --
+
+
+-- find the most relevant set of credentials for the Z source and org
+CREATE OR REPLACE FUNCTION config.z3950_source_credentials_lookup
+        (source TEXT, owner INTEGER) 
+        RETURNS config.z3950_source_credentials AS $$
+
+    SELECT creds.* 
+    FROM config.z3950_source_credentials creds
+        JOIN actor.org_unit aou ON (aou.id = creds.owner)
+        JOIN actor.org_unit_type aout ON (aout.id = aou.ou_type)
+    WHERE creds.source = $1 AND creds.owner IN ( 
+        SELECT id FROM actor.org_unit_ancestors($2) 
+    )
+    ORDER BY aout.depth DESC LIMIT 1;
+
+$$ LANGUAGE SQL STABLE;
+
+-- since we are not exposing config.z3950_source_credentials
+-- via the IDL, providing a stored proc gives us a way to
+-- set values in the table via cstore
+CREATE OR REPLACE FUNCTION config.z3950_source_credentials_apply
+        (src TEXT, org INTEGER, uname TEXT, passwd TEXT) 
+        RETURNS VOID AS $$
+BEGIN
+    PERFORM 1 FROM config.z3950_source_credentials
+        WHERE owner = org AND source = src;
+
+    IF FOUND THEN
+        IF COALESCE(uname, '') = '' AND COALESCE(passwd, '') = '' THEN
+            DELETE FROM config.z3950_source_credentials 
+                WHERE owner = org AND source = src;
+        ELSE 
+            UPDATE config.z3950_source_credentials 
+                SET username = uname, password = passwd
+                WHERE owner = org AND source = src;
+        END IF;
+    ELSE
+        IF COALESCE(uname, '') <> '' OR COALESCE(passwd, '') <> '' THEN
+            INSERT INTO config.z3950_source_credentials
+                (source, owner, username, password) 
+                VALUES (src, org, uname, passwd);
+        END IF;
+    END IF;
+END;
+$$ LANGUAGE PLPGSQL;
+
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.z3950_credentials.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.z3950_credentials.sql
new file mode 100644 (file)
index 0000000..ca6d364
--- /dev/null
@@ -0,0 +1,60 @@
+
+BEGIN;
+
+CREATE TABLE config.z3950_source_credentials (
+    id SERIAL PRIMARY KEY,
+    owner INTEGER NOT NULL REFERENCES actor.org_unit(id),
+    source TEXT NOT NULL REFERENCES config.z3950_source(name),
+    -- do some Z servers require a username but no password or vice versa?
+    username TEXT,
+    password TEXT,
+    CONSTRAINT czsc_source_once_per_lib UNIQUE (source, owner)
+);
+
+-- find the most relevant set of credentials for the Z source and org
+CREATE OR REPLACE FUNCTION config.z3950_source_credentials_lookup
+        (source TEXT, owner INTEGER) 
+        RETURNS config.z3950_source_credentials AS $$
+
+    SELECT creds.* 
+    FROM config.z3950_source_credentials creds
+        JOIN actor.org_unit aou ON (aou.id = creds.owner)
+        JOIN actor.org_unit_type aout ON (aout.id = aou.ou_type)
+    WHERE creds.source = $1 AND creds.owner IN ( 
+        SELECT id FROM actor.org_unit_ancestors($2) 
+    )
+    ORDER BY aout.depth DESC LIMIT 1;
+
+$$ LANGUAGE SQL STABLE;
+
+-- since we are not exposing config.z3950_source_credentials
+-- via the IDL, providing a stored proc gives us a way to
+-- set values in the table via cstore
+CREATE OR REPLACE FUNCTION config.z3950_source_credentials_apply
+        (src TEXT, org INTEGER, uname TEXT, passwd TEXT) 
+        RETURNS VOID AS $$
+BEGIN
+    PERFORM 1 FROM config.z3950_source_credentials
+        WHERE owner = org AND source = src;
+
+    IF FOUND THEN
+        IF COALESCE(uname, '') = '' AND COALESCE(passwd, '') = '' THEN
+            DELETE FROM config.z3950_source_credentials 
+                WHERE owner = org AND source = src;
+        ELSE 
+            UPDATE config.z3950_source_credentials 
+                SET username = uname, password = passwd
+                WHERE owner = org AND source = src;
+        END IF;
+    ELSE
+        IF COALESCE(uname, '') <> '' OR COALESCE(passwd, '') <> '' THEN
+            INSERT INTO config.z3950_source_credentials
+                (source, owner, username, password) 
+                VALUES (src, org, uname, passwd);
+        END IF;
+    END IF;
+END;
+$$ LANGUAGE PLPGSQL;
+
+
+COMMIT;
index abe02e4..2589ca2 100644 (file)
         </div>
     </div>
     <div>
-        <button onClick="location.href = location.href.replace(/\/[^\/]+$/, '')">[% l('Return to Sources') %]</button>
+        <button onClick="location.href = location.href.replace(/\/[^\/]+$/, '')">
+            [% l('Return to Sources') %]
+        </button>
     </div>
     <br/>
+    
+    <div>
+        [% l('Credentials for ') %]
+        <select dojoType="openils.widget.OrgUnitFilteringSelect" 
+            jsId='z39ContextSelector'
+            searchAttr='shortname' 
+            labelAttr='shortname'> 
+        </select> 
+        [% l('Username') %] 
+        <input type='text' id='z39-creds-username'/>
+        [% l('Password') %] 
+        <input type='password' id='z39-creds-password'/>
+        <button id='z39-creds-button' onclick='applyCreds()'>
+            [% l('Apply Credentials') %]
+        </button>
+        <button id='z39-creds-clear' onclick='applyCreds(1)'>
+            [% l('Clear Stored Credentials') %]
+        </button>
+    </div>
+
+    <br/>
 
     <table
         id="zaGrid"
index 23998e5..c73bc9c 100644 (file)
@@ -21,12 +21,39 @@ function buildZSGrid() {
             readOnly : true
         };
 
+        // draw the credentials context org unit selector
+        new openils.User().buildPermOrgSelector(
+            'ADMIN_Z3950_SOURCE', z39ContextSelector);
+
     } else {
 
         zsGrid.loadAll({order_by:{czs : 'name'}});
     }
 }
 
+function applyCreds(clear) {
+    dojo.byId('z39-creds-button').disabled = true;
+    dojo.byId('z39-creds-clear').disabled = true;
+    fieldmapper.standardRequest(
+        ['open-ils.search', 'open-ils.search.z3950.apply_credentials'],
+        {   async : true,
+            params : [
+                openils.User.authtoken,
+                sourceCode,
+                z39ContextSelector.attr('value'),
+                clear ? '' : dojo.byId('z39-creds-username').value,
+                clear ? '' : dojo.byId('z39-creds-password').value
+            ],
+            oncomplete : function(r) {
+                dojo.byId('z39-creds-password').value = '';
+                dojo.byId('z39-creds-button').disabled = false;
+                dojo.byId('z39-creds-clear').disabled = false;
+                openils.Util.readResponse(r);
+            }
+        }
+    );
+}
+
 function formatSourceName(val) {
     return '<a href="' + location.href + '/' + escape(val) + '">' + val + '</a>';
 }
diff --git a/docs/RELEASE_NOTES_NEXT/z39_source_credentials.txt b/docs/RELEASE_NOTES_NEXT/z39_source_credentials.txt
new file mode 100644 (file)
index 0000000..eb74bcc
--- /dev/null
@@ -0,0 +1,13 @@
+Storing Z39.50 Server Credentials
+=================================
+
+In the Z39.50 configuration interface, staff now have the option to apply
+z39.50 login credentials to each Z39.50 server at different levels of the 
+org unit hierarchy (similar to org unit settings).  When credentials are set 
+for a Z39 server, searches against the z39 server will used the stored 
+credentials, unless other credentials are provided by the caller, in which 
+case the caller credentials are used.
+
+For security purposes, passwords may not be retrieved or reported on by staff.  
+Staff can only apply new values for credentials or clear existing ones.
+