PerlChildInitHandler OpenILS::WWW::AutoSuggest::child_init
PerlChildInitHandler OpenILS::WWW::PhoneList::child_init
PerlChildInitHandler OpenILS::WWW::EGWeb::child_init
+PerlChildInitHandler OpenILS::WWW::RemoteAuth::child_init
# ----------------------------------------------------------------------------------
# Set some defaults for our working directories
use OpenILS::WWW::IDL2js ('@sysconfdir@/opensrf_core.xml');
use OpenILS::WWW::FlatFielder;
use OpenILS::WWW::PhoneList ('@sysconfdir@/opensrf_core.xml');
+use OpenILS::WWW::RemoteAuth ('@sysconfdir@/opensrf_core.xml');
# Pass second argument of '1' to enable template caching.
use OpenILS::WWW::PrintTemplate ('/openils/conf/opensrf_core.xml', 0);
#OpenILS::WWW::Redirect->parse_ips_file('@sysconfdir@/lib_ips.txt');
-
1;
</permacrud>
</class>
+ <class id="cra" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::remoteauth_profile" oils_persist:tablename="config.remoteauth_profile" reporter:label="Remote Patron Authentication Configuration Profile">
+ <fields oils_persist:primary="name">
+ <field name="name" reporter:datatype="text"/>
+ <field name="description" reporter:datatype="text"/>
+ <field name="context_org" reporter:datatype="org_unit"/>
+ <field name="enabled" reporter:datatype="bool"/>
+ <field name="perm" reporter:datatype="link"/>
+ <field name="restrict_to_org" reporter:datatype="bool"/>
+ <field name="allow_inactive" reporter:datatype="bool"/>
+ <field name="allow_expired" reporter:datatype="bool"/>
+ <field name="block_list" reporter:datatype="text"/>
+ </fields>
+ <links>
+ <link field="context_org" reltype="has_a" key="id" map="" class="aou"/>
+ <link field="perm" reltype="has_a" key="id" map="" class="ppl"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="ADMIN_REMOTEAUTH" context_field="context_org"/>
+ <retrieve permission="STAFF_LOGIN" context_field="context_org"/>
+ <update permission="ADMIN_REMOTEAUTH" context_field="context_org"/>
+ <delete permission="ADMIN_REMOTEAUTH" context_field="context_org"/>
+ </actions>
+ </permacrud>
+ </class>
+
<!-- ********************************************************************************************************************* -->
</IDL>
--- /dev/null
+# Copyright (C) 2019 BC Libraries Cooperative
+#
+# This program 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.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+# ======================================================================
+# - base class for configurable HTTP API for patron auth/retrieval
+# - provides generic methods shared by all handler subclasses
+# - handlers take care of endpoint-specific implementation details
+# ======================================================================
+
+package OpenILS::WWW::RemoteAuth;
+use strict; use warnings;
+use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN AUTH_REQUIRED HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
+use DateTime::Format::ISO8601;
+
+use OpenSRF::EX qw(:try);
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenSRF::System;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Application::AppUtils;
+our $U = "OpenILS::Application::AppUtils";
+
+my $bootstrap_config;
+my @handlers_to_preinit = ();
+
+sub editor {
+ my ($self, $editor) = @_;
+ $self->{editor} = $editor if $editor;
+ return $self->{editor};
+}
+
+sub config {
+ my ($self, $config) = @_;
+ $self->{config} = $config if $config;
+ return $self->{config};
+}
+
+sub import {
+ my ($self, $bootstrap_config, $handlers) = @_;
+ @handlers_to_preinit = split /\s+/, $handlers, -1 if defined($handlers);
+}
+
+sub child_init {
+ OpenSRF::System->bootstrap_client(config_file => $bootstrap_config);
+ my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
+ Fieldmapper->import(IDL => $idl);
+ OpenILS::Utils::CStoreEditor->init;
+ foreach my $module (@handlers_to_preinit) {
+ eval {
+ $module->use;
+ };
+ }
+ return Apache2::Const::OK;
+}
+
+sub handler {
+ my $r = shift;
+
+ my $stat = Apache2::Const::AUTH_REQUIRED;
+
+ # load the appropriate module and process our request
+ try {
+ my $module = $r->dir_config('OILSRemoteAuthHandler');
+ $module->use;
+ my $handler = $module->new;
+ $stat = $handler->process($r);
+ } catch Error with {
+ $logger->error("processing RemoteAuth handler failed: @_");
+ $stat = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+ };
+
+ return $stat;
+}
+
+sub load_config {
+ my ($self, $e, $r) = @_;
+
+ # name to use for config lookup
+ my $name = $r->dir_config('OILSRemoteAuthProfile');
+ return undef unless $name;
+
+ # load config
+ my $config = $e->retrieve_config_remoteauth_profile($name);
+ if ($config and $U->is_true($config->enabled)) {
+ return $config;
+ }
+ $logger->info("RemoteAuth: config profile $name not found (or not enabled)");
+ return undef;
+}
+
+sub do_client_auth {
+ my ($self, $client_username, $client_password) = @_;
+ my $login_resp = $U->simplereq(
+ 'open-ils.auth',
+ 'open-ils.auth.login', {
+ username => $client_username,
+ password => $client_password,
+ type => 'staff'
+ }
+ );
+ if ($login_resp->{textcode} eq 'SUCCESS') {
+ return $login_resp->{payload}->{authtoken};
+ }
+ $logger->info("RemoteAuth: failed to authenticate client $client_username");
+ return undef;
+}
+
+sub do_patron_auth {
+ my ($self, $e, $config, $id, $password) = @_;
+ my $org_unit = $config->context_org;
+
+ return $self->backend_error unless $e->checkauth;
+
+ # XXX
+ my $args = {
+ type => 'opac',
+ org => $org_unit,
+ identifier => $id,
+ password => $password,
+ agent => 'remoteauth'
+ };
+
+ my $response = $U->simplereq(
+ 'open-ils.auth',
+ 'open-ils.auth.login', $args);
+ if($U->event_code($response)) {
+ $logger->info("RemoteAuth: failed to authenticate user $id at org unit $org_unit");
+ return $self->patron_not_authenticated;
+ }
+
+ # get basic patron info via user authtoken
+ my $authtoken = $response->{payload}->{authtoken};
+ my $user = $U->simplereq(
+ 'open-ils.auth',
+ 'open-ils.auth.session.retrieve', $authtoken);
+ if (!$user or $U->event_code($user)) {
+ $logger->error("RemoteAuth: failed to retrieve user for session $authtoken");
+ return $self->backend_error;
+ }
+ my $userid = $user->id;
+ my $home_ou = $user->home_ou;
+
+ unless ($e->allowed('VIEW_USER', $home_ou)) {
+ $logger->info("RemoteAuth: client does not have permission to view user $userid");
+ return $self->client_not_authorized;
+ }
+
+ # do basic validation (and skip the permit test where applicable)
+ if ($U->is_true($user->deleted)) {
+ $logger->info("RemoteAuth: user $userid is deleted");
+ return $self->patron_not_found;
+ }
+
+ if ($U->is_true($user->barred)) {
+ $logger->info("RemoteAuth: user $userid is barred");
+ return $self->patron_is_blocked;
+ }
+
+ # check if remoteauth is permitted for this user
+ my $permit_test = $e->json_query(
+ {from => ['actor.permit_remoteauth', $config->name, $userid]}
+ )->[0]{'actor.permit_remoteauth'};;
+
+ if ($permit_test eq 'success') {
+ return $self->success($user);
+ } elsif ($permit_test eq 'not_found') {
+ return $self->patron_not_found;
+ } elsif ($permit_test eq 'expired') {
+ return $self->patron_is_expired;
+ } else {
+ return $self->patron_is_blocked;
+ }
+}
+
+# Dummy methods for responding to the client based on
+# different error (or success) conditions.
+# The handler will normally want to override these methods
+# with its own version of them.
+
+# patron auth succeeded
+sub success {
+ return Apache2::Const::OK;
+}
+
+# generic backend error
+sub backend_error {
+ return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+}
+
+# client error (e.g. missing params)
+sub client_error {
+ return Apache2::Const::HTTP_BAD_REQUEST;
+}
+
+# client auth failed
+sub client_not_authorized {
+ return Apache2::Const::AUTH_REQUIRED;
+}
+
+# patron auth failed (bad password etc)
+sub patron_not_authenticated {
+ return Apache2::Const::FORBIDDEN;
+}
+
+# patron does not exist or is inactive/deleted
+sub patron_not_found {
+ return Apache2::Const::DECLINED;
+}
+
+# patron is barred or has blocking penalties
+sub patron_is_blocked {
+ return Apache2::Const::FORBIDDEN;
+}
+
+# patron is expired
+sub patron_is_expired {
+ return Apache2::Const::DECLINED;
+}
+
+1;
+
--- /dev/null
+BEGIN;
+
+CREATE TABLE config.remoteauth_profile (
+ name TEXT PRIMARY KEY,
+ description TEXT,
+ context_org INT NOT NULL REFERENCES actor.org_unit(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ enabled BOOLEAN NOT NULL DEFAULT FALSE,
+ perm INT NOT NULL REFERENCES permission.perm_list(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
+ restrict_to_org BOOLEAN NOT NULL DEFAULT TRUE,
+ allow_inactive BOOL NOT NULL DEFAULT FALSE,
+ allow_expired BOOL NOT NULL DEFAULT FALSE,
+ block_list TEXT
+);
+
+CREATE OR REPLACE FUNCTION actor.permit_remoteauth (profile_name TEXT, userid BIGINT) RETURNS TEXT AS $func$
+DECLARE
+ usr actor.usr%ROWTYPE;
+ profile config.remoteauth_profile%ROWTYPE;
+ perm TEXT;
+ context_org_list INT[];
+ home_prox INT;
+ block TEXT;
+ penalty_count INT;
+BEGIN
+
+ SELECT INTO usr * FROM actor.usr WHERE id = userid AND NOT deleted;
+ IF usr IS NULL THEN
+ RETURN 'not_found';
+ END IF;
+
+ IF usr.barred IS TRUE THEN
+ RETURN 'blocked';
+ END IF;
+
+ SELECT INTO profile * FROM config.remoteauth_profile WHERE name = profile_name;
+ SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( profile.context_org );
+
+ -- user's home library must be within the context org
+ IF profile.restrict_to_org IS TRUE AND usr.home_ou NOT IN (SELECT * FROM UNNEST(context_org_list)) THEN
+ RETURN 'not_found';
+ END IF;
+
+ SELECT INTO perm code FROM permission.perm_list WHERE id = profile.perm;
+ IF permission.usr_has_perm(usr.id, perm, profile.context_org) IS FALSE THEN
+ RETURN 'not_found';
+ END IF;
+
+ IF usr.expire_date < NOW() AND profile.allow_expired IS FALSE THEN
+ RETURN 'expired';
+ END IF;
+
+ IF usr.active IS FALSE AND profile.allow_inactive IS FALSE THEN
+ RETURN 'blocked';
+ END IF;
+
+ -- Proximity of user's home_ou to context_org to see if penalties should be ignored.
+ SELECT INTO home_prox prox FROM actor.org_unit_proximity WHERE from_org = usr.home_ou AND to_org = profile.context_org;
+
+ -- Loop through the block list to see if the user has any matching penalties.
+ IF profile.block_list IS NOT NULL THEN
+ FOR block IN SELECT UNNEST(STRING_TO_ARRAY(profile.block_list, '|')) LOOP
+ SELECT INTO penalty_count COUNT(DISTINCT csp.*)
+ FROM actor.usr_standing_penalty usp
+ JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
+ WHERE usp.usr = usr.id
+ AND usp.org_unit IN ( SELECT * FROM UNNEST(context_org_list) )
+ AND ( usp.stop_date IS NULL or usp.stop_date > NOW() )
+ AND ( csp.ignore_proximity IS NULL OR csp.ignore_proximity < home_prox )
+ AND csp.block_list ~ block;
+ IF penalty_count > 0 THEN
+ -- User has penalties that match this block, so auth is not permitted.
+ -- Don't bother testing the rest of the block list.
+ RETURN 'blocked';
+ END IF;
+ END LOOP;
+ END IF;
+
+ -- User has passed all tests.
+ RETURN 'success';
+
+END;
+$func$ LANGUAGE plpgsql;
+
+COMMIT;
+
( 613, 'ADMIN_CAROUSEL', oils_i18n_gettext(613,
'Allow a user to manage carousels', 'ppl', 'description')),
( 614, 'REFRESH_CAROUSEL', oils_i18n_gettext(614,
- 'Allow a user to refresh carousels', 'ppl', 'description'))
+ 'Allow a user to refresh carousels', 'ppl', 'description')),
+ ( 615, 'ADMIN_REMOTEAUTH', oils_i18n_gettext( 615,
+ 'Administer remote patron authentication', 'ppl', 'description' ))
;
100.circ_matrix.sql
110.hold_matrix.sql
120.floating_groups.sql
+150.remoteauth.sql
210.schema.serials.sql
200.schema.acq.sql
--- /dev/null
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('XXXX');
+
+INSERT INTO permission.perm_list ( id, code, description ) VALUES
+ ( 615, 'ADMIN_REMOTEAUTH', oils_i18n_gettext( 615,
+ 'Administer remote patron authentication', 'ppl', 'description' ));
+
+CREATE TABLE config.remoteauth_profile (
+ name TEXT PRIMARY KEY,
+ description TEXT,
+ context_org INT NOT NULL REFERENCES actor.org_unit(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ enabled BOOLEAN NOT NULL DEFAULT FALSE,
+ perm INT NOT NULL REFERENCES permission.perm_list(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
+ restrict_to_org BOOLEAN NOT NULL DEFAULT TRUE,
+ allow_inactive BOOL NOT NULL DEFAULT FALSE,
+ allow_expired BOOL NOT NULL DEFAULT FALSE,
+ block_list TEXT
+);
+
+CREATE OR REPLACE FUNCTION actor.permit_remoteauth (profile_name TEXT, userid BIGINT) RETURNS TEXT AS $func$
+DECLARE
+ usr actor.usr%ROWTYPE;
+ profile config.remoteauth_profile%ROWTYPE;
+ perm TEXT;
+ context_org_list INT[];
+ home_prox INT;
+ block TEXT;
+ penalty_count INT;
+BEGIN
+
+ SELECT INTO usr * FROM actor.usr WHERE id = userid AND NOT deleted;
+ IF usr IS NULL THEN
+ RETURN 'not_found';
+ END IF;
+
+ IF usr.barred IS TRUE THEN
+ RETURN 'blocked';
+ END IF;
+
+ SELECT INTO profile * FROM config.remoteauth_profile WHERE name = profile_name;
+ SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( profile.context_org );
+
+ -- user's home library must be within the context org
+ IF profile.restrict_to_org IS TRUE AND usr.home_ou NOT IN (SELECT * FROM UNNEST(context_org_list)) THEN
+ RETURN 'not_found';
+ END IF;
+
+ SELECT INTO perm code FROM permission.perm_list WHERE id = profile.perm;
+ IF permission.usr_has_perm(usr.id, perm, profile.context_org) IS FALSE THEN
+ RETURN 'not_found';
+ END IF;
+
+ IF usr.expire_date < NOW() AND profile.allow_expired IS FALSE THEN
+ RETURN 'expired';
+ END IF;
+
+ IF usr.active IS FALSE AND profile.allow_inactive IS FALSE THEN
+ RETURN 'blocked';
+ END IF;
+
+ -- Proximity of user's home_ou to context_org to see if penalties should be ignored.
+ SELECT INTO home_prox prox FROM actor.org_unit_proximity WHERE from_org = usr.home_ou AND to_org = profile.context_org;
+
+ -- Loop through the block list to see if the user has any matching penalties.
+ IF profile.block_list IS NOT NULL THEN
+ FOR block IN SELECT UNNEST(STRING_TO_ARRAY(profile.block_list, '|')) LOOP
+ SELECT INTO penalty_count COUNT(DISTINCT csp.*)
+ FROM actor.usr_standing_penalty usp
+ JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
+ WHERE usp.usr = usr.id
+ AND usp.org_unit IN ( SELECT * FROM UNNEST(context_org_list) )
+ AND ( usp.stop_date IS NULL or usp.stop_date > NOW() )
+ AND ( csp.ignore_proximity IS NULL OR csp.ignore_proximity < home_prox )
+ AND csp.block_list ~ block;
+ IF penalty_count > 0 THEN
+ -- User has penalties that match this block, so auth is not permitted.
+ -- Don't bother testing the rest of the block list.
+ RETURN 'blocked';
+ END IF;
+ END LOOP;
+ END IF;
+
+ -- User has passed all tests.
+ RETURN 'success';
+
+END;
+$func$ LANGUAGE plpgsql;
+
+COMMIT;
+