[open-ils-commits] [GIT] Evergreen ILS branch master updated. 46c8e3a876bdf34222429dc9d98954ff84ef20c1

Evergreen Git git at git.evergreen-ils.org
Fri Sep 6 17:17:42 EDT 2019


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  46c8e3a876bdf34222429dc9d98954ff84ef20c1 (commit)
       via  e8507394878a873920710cb1c979dcf8dd36661f (commit)
       via  56bab3e9d877ad3db06e5bce5fed9e45ae5a003f (commit)
       via  59f6637ae1228019744e911df65983f1cdb70ac8 (commit)
       via  7ae4f7692ce7ed647af985a02436338c8a164369 (commit)
       via  a830b6ab23fef58ec94da5bc2096fe9da99cd246 (commit)
       via  ab6e3d356892c1d2d376f75b56a9fd31888cb5a5 (commit)
       via  ccbcd4773e363da7e1ffa8c5d92a44db04cd43de (commit)
       via  dfefabd1649a644ed9972a0227be877ca98908ec (commit)
       via  6e74775849baa2e16673b1ae94d750d8119df577 (commit)
       via  69a23a05570d4e1d5980c7af852b3abe3f3069a8 (commit)
       via  8589d20554a27ec6ba27d2fb43d5b72bbc75d6dd (commit)
      from  b577e78fa1fd0fd38e218f135c9afa044d3ff591 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 46c8e3a876bdf34222429dc9d98954ff84ef20c1
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Fri Sep 6 17:17:14 2019 -0400

    LP#1817645: add release notes
    
    These are taken from the introduction of the technical reference
    docs written by Jeff Davis.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/docs/RELEASE_NOTES_NEXT/Architecture/remoteauth.adoc b/docs/RELEASE_NOTES_NEXT/Architecture/remoteauth.adoc
new file mode 100644
index 0000000000..d86cba3f95
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Architecture/remoteauth.adoc
@@ -0,0 +1,18 @@
+Configurable APIs for Patron Authentication and Retrieval
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Many external services need to authenticate patrons and retrieve information
+about their accounts from Evergreen.  Most of these services support some form
+of HTTP-based authentication, but every service has its own requirements and
+none of them support native Evergreen authentication.  Meanwhile, libraries
+often need to restrict access to these external services based on patron type,
+current status, standing penalties, and so on.
+
+To meet these needs, Evergreen now has support for separate, configurable HTTP
+API endpoints for remote patron authentication and retrieval.  Each RemoteAuth
+endpoint handles a different external service or authentication method.  You
+set up the endpoints you want in your Apache config; each one uses a generic
+mod_perl handler to manage incoming requests, and specifies a Perl module that
+can actually talk to the external service, as well as an authentication profile
+that determines which patrons can be authenticated at this endpoint.  Support
+for https://tools.ietf.org/html/rfc7617["Basic" HTTP Authentication] is
+provided as a reference implementation.

commit e8507394878a873920710cb1c979dcf8dd36661f
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Fri Sep 6 17:13:30 2019 -0400

    LP#1817645: stamp schema update
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 2f61d63c1d..977003f998 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -92,7 +92,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1179', :eg_version); -- dpearl/Dycrona/gmcharlt
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1180', :eg_version); -- jeffdavis/gmcharlt
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.remoteauth.sql b/Open-ILS/src/sql/Pg/upgrade/1180.schema.remoteauth.sql
similarity index 98%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.remoteauth.sql
rename to Open-ILS/src/sql/Pg/upgrade/1180.schema.remoteauth.sql
index 0b6df554fa..4b86706626 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.remoteauth.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1180.schema.remoteauth.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('1180', :eg_version);
+
 INSERT INTO permission.perm_list ( id, code, description ) VALUES
  ( 615, 'ADMIN_REMOTEAUTH', oils_i18n_gettext( 615,
     'Administer remote patron authentication', 'ppl', 'description' ));

commit 56bab3e9d877ad3db06e5bce5fed9e45ae5a003f
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Fri Sep 6 17:11:13 2019 -0400

    LP#1817645: (follow-up) sync schema update script
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.remoteauth.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.remoteauth.sql
index 6936d7e3bf..0b6df554fa 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.remoteauth.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.remoteauth.sql
@@ -1,7 +1,5 @@
 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' ));
@@ -15,7 +13,8 @@ CREATE TABLE config.remoteauth_profile (
     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
+    block_list TEXT,
+    usr_activity_type INT REFERENCES config.usr_activity_type(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED
 );
 
 CREATE OR REPLACE FUNCTION actor.permit_remoteauth (profile_name TEXT, userid BIGINT) RETURNS TEXT AS $func$

commit 59f6637ae1228019744e911df65983f1cdb70ac8
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Mon Jun 17 14:12:21 2019 -0700

    LP#1817645: use label as reporter:selector for user activity type
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 20905ca075..1a8598848b 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -3737,7 +3737,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 	</class>
 	<class id="cuat" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::usr_activity_type" oils_persist:tablename="config.usr_activity_type" reporter:label="User Activity Type">
 		<fields oils_persist:primary="id" oils_persist:sequence="config.usr_activity_type_id_seq">
-			<field name="id" reporter:label="ID" reporter:datatype="id" />
+			<field name="id" reporter:label="ID" reporter:datatype="id" reporter:selector="label"/>
 			<field name="ewho" reporter:label="Event Caller" reporter:datatype="text"/>
 			<field name="ewhat" reporter:label="Event Type" reporter:datatype="text"/>
 			<field name="ehow" reporter:label="Event Mechanism" reporter:datatype="text"/>

commit 7ae4f7692ce7ed647af985a02436338c8a164369
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Wed Jun 12 10:46:30 2019 -0700

    LP#1817645: add RemoteAuth profiles to Angular server admin
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index f358b652c3..20905ca075 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -12942,16 +12942,16 @@ SELECT  usr,
 
 	<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"/>
-			<field name="usr_activity_type" reporter:datatype="link"/>
+			<field name="name"              reporter:datatype="text"     reporter:label="Name" oils_obj:required="true"/>
+			<field name="description"       reporter:datatype="text"     reporter:label="Description"/>
+			<field name="context_org"       reporter:datatype="org_unit" reporter:label="Context Org" oils_obj:required="true"/>
+			<field name="enabled"           reporter:datatype="bool"     reporter:label="Enabled"/>
+			<field name="perm"              reporter:datatype="link"     reporter:label="Permission Required by User" oils_obj:required="true"/>
+			<field name="restrict_to_org"   reporter:datatype="bool"     reporter:label="Restrict by Home Library"/>
+			<field name="allow_inactive"    reporter:datatype="bool"     reporter:label="Allow Inactive Users"/>
+			<field name="allow_expired"     reporter:datatype="bool"     reporter:label="Allow Expired Users"/>
+			<field name="block_list"        reporter:datatype="text"     reporter:label="Block List"/>
+			<field name="usr_activity_type" reporter:datatype="link"     reporter:label="User Activity Type"/>
 		</fields>
 		<links>
 			<link field="context_org" reltype="has_a" key="id" map="" class="aou"/>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html
index ab8ed5b09e..a1b6db1c44 100644
--- a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html
@@ -88,6 +88,8 @@
       routerLink="/staff/admin/server/config/print_template"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Remote Accounts"  
       routerLink="/staff/admin/server/config/remote_account"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Remote Authentication Profiles"  
+      routerLink="/staff/admin/server/config/remoteauth_profile"></eg-link-table-link>
     <eg-link-table-link i18n-label label="SMS Carriers"  
       routerLink="/staff/admin/server/config/sms_carrier"></eg-link-table-link>
     <eg-link-table-link i18n-label label="User Activity Types"  

commit a830b6ab23fef58ec94da5bc2096fe9da99cd246
Author: Jeff Davis <jeff.davis at bc.libraries.coop>
Date:   Wed May 1 11:42:11 2019 -0700

    LP#1817645: track RemoteAuth user activity
    
    Signed-off-by: Jeff Davis <jeff.davis at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index dc236aeb55..f358b652c3 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -12951,10 +12951,12 @@ SELECT  usr,
 			<field name="allow_inactive" reporter:datatype="bool"/>
 			<field name="allow_expired" reporter:datatype="bool"/>
 			<field name="block_list" reporter:datatype="text"/>
+			<field name="usr_activity_type" reporter:datatype="link"/>
 		</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"/>
+			<link field="usr_activity_type" reltype="has_a" key="id" map="" class="cuat"/>
 		</links>
 		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
 			<actions>
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth.pm
index 7726429c34..82163160f1 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth.pm
@@ -77,7 +77,8 @@ sub handler {
         my $handler = $module->new;
         $stat = $handler->process($r);
     } catch Error with {
-        $logger->error("processing RemoteAuth handler failed: @_");
+        my $err = shift;
+        $logger->error("processing RemoteAuth handler failed: $err");
         $stat = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
     };
 
@@ -123,15 +124,18 @@ sub do_patron_auth {
 
     return $self->backend_error unless $e->checkauth;
 
-    # XXX
     my $args = {
-        type => 'opac',
+        type => 'opac', # XXX
         org => $org_unit,
         identifier => $id,
-        password => $password,
-        agent => 'remoteauth'
+        password => $password
     };
 
+    my $cuat = $e->retrieve_config_usr_activity_type($config->usr_activity_type);
+    if ($cuat) {
+        $args->{agent} = $cuat->ewho;
+    }
+
     my $response = $U->simplereq(
         'open-ils.auth',
         'open-ils.auth.login', $args);
diff --git a/Open-ILS/src/perlmods/live_t/29-lp1817645-remoteauth-patron-api.t b/Open-ILS/src/perlmods/live_t/29-lp1817645-remoteauth-patron-api.t
index 13a4a0e8f6..6732d6c15b 100644
--- a/Open-ILS/src/perlmods/live_t/29-lp1817645-remoteauth-patron-api.t
+++ b/Open-ILS/src/perlmods/live_t/29-lp1817645-remoteauth-patron-api.t
@@ -1,6 +1,6 @@
 #!perl
 
-use Test::More tests => 9; # XXX
+use Test::More tests => 10; # XXX
 
 diag("Tests RemoteAuth patron auth/retrieval");
 
@@ -37,6 +37,7 @@ my $staff_login = $U->simplereq(
 );
 is($staff_login->{textcode}, 'SUCCESS', 'Staff login OK');
 my $e = new_editor( authtoken => $staff_login->{payload}->{authtoken} );
+$e->init;
 
 my $client = LWP::UserAgent->new;
 $client->ssl_opts( verify_hostname => 0 );
@@ -135,6 +136,8 @@ is( $basic_external, '403', 'Basic request for external user correctly returned
 # - response: "+VALID" if auth succeeds
 
 
-# TODO: verify user activity based on the above tests
-
+# verify user activity based on the above tests
+my $user = $U->fetch_user_by_barcode( $valid->{barcode} );
+my $basic_activity = $e->search_actor_usr_activity([{usr => $user->id, etype => 1001}]);
+ok(scalar(@$basic_activity) > 0, 'Basic request for valid patron is recorded in user activity');
 
diff --git a/Open-ILS/src/sql/Pg/150.remoteauth.sql b/Open-ILS/src/sql/Pg/150.remoteauth.sql
index 0e7f823cb1..0b36c4991c 100644
--- a/Open-ILS/src/sql/Pg/150.remoteauth.sql
+++ b/Open-ILS/src/sql/Pg/150.remoteauth.sql
@@ -9,7 +9,8 @@ CREATE TABLE config.remoteauth_profile (
     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
+    block_list TEXT,
+    usr_activity_type INT REFERENCES config.usr_activity_type(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED
 );
 
 CREATE OR REPLACE FUNCTION actor.permit_remoteauth (profile_name TEXT, userid BIGINT) RETURNS TEXT AS $func$
diff --git a/Open-ILS/tests/datasets/sql/remoteauth.sql b/Open-ILS/tests/datasets/sql/remoteauth.sql
index be0c7998c5..f967efb090 100644
--- a/Open-ILS/tests/datasets/sql/remoteauth.sql
+++ b/Open-ILS/tests/datasets/sql/remoteauth.sql
@@ -1,7 +1,11 @@
+INSERT INTO config.usr_activity_type (id, ewho, ewhat, ehow, egroup, label) VALUES
+ ( 1001, 'basicauth', 'login', 'apache', 'authen',
+    oils_i18n_gettext(1001, 'RemoteAuth Login: HTTP Basic Authentication', 'cuat', 'label'));
+
 -- config for Basic HTTP Authentication (SYS1)
 INSERT INTO config.remoteauth_profile
     (name, description, context_org, enabled, perm,
-        restrict_to_org, allow_inactive, allow_expired, block_list)
+        restrict_to_org, allow_inactive, allow_expired, block_list, usr_activity_type)
     VALUES ('Basic', 'Basic HTTP Authentication for SYS1', 2, TRUE, 1,
-        TRUE, FALSE, FALSE, NULL);
+        TRUE, FALSE, FALSE, NULL, 1001);
 

commit ab6e3d356892c1d2d376f75b56a9fd31888cb5a5
Author: Jeff Davis <jeff.davis at bc.libraries.coop>
Date:   Thu Apr 4 15:47:14 2019 -0700

    LP#1817645: RemoteAuth documentation
    
    Signed-off-by: Jeff Davis <jeff.davis at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/docs/TechRef/remoteauth.adoc b/docs/TechRef/remoteauth.adoc
new file mode 100644
index 0000000000..ceb61d6b69
--- /dev/null
+++ b/docs/TechRef/remoteauth.adoc
@@ -0,0 +1,88 @@
+Configurable APIs for Patron Authentication and Retrieval
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Summary
++++++++
+
+Many external services need to authenticate patrons and retrieve information about their accounts from Evergreen.  Most of these services support some form of HTTP-based authentication, but every service has its own requirements and none of them support native Evergreen authentication.  Meanwhile, libraries often need to restrict access to these external services based on patron type, current status, standing penalties, and so on.
+
+To meet these needs, Evergreen now has support for separate, configurable HTTP API endpoints for remote patron authentication and retrieval.  Each RemoteAuth endpoint handles a different external service or authentication method.  You set up the endpoints you want in your Apache config; each one uses a generic mod_perl handler to manage incoming requests, and specifies a Perl module that can actually talk to the external service, as well as an authentication profile that determines which patrons can be authenticated at this endpoint.  Support for https://tools.ietf.org/html/rfc7617["Basic" HTTP Authentication] is provided as a reference implementation.
+
+
+
+How it works
+++++++++++++
+
+. Client submits a request to a RemoteAuth endpoint containing user credentials and any additional requirements.
+. RemoteAuth loads the handler module for this endpoint.
+. Handler processes the request and authorizes the client.
+. Handler loads this endpoint's configuration from the database.
+. Handler authenticates the user using the credentials provided, and tests whether auth is permitted for this user at this endpoint.
+. Handler returns an appropriate response to RemoteAuth, which passes the response to the client.
+
+
+Apache configuration
+++++++++++++++++++++
+
+To define a new RemoteAuth endpoint, add a new Location directive in your `eg_vhost.conf` file.  The default configuration for Basic auth looks like this:
+
+....
+<Location /api/basicauth>
+    SetHandler perl-script
+    PerlHandler OpenILS::WWW::RemoteAuth
+    Options +ExecCGI
+
+    # access restricted to localhost by default; since this module provides no
+    # client authentiation, restricting access by IP or other means is stongly
+    # recommended
+    Require local
+
+    # remoteauth profile name
+    PerlSetVar OILSRemoteAuthProfile "Basic"
+    # Perl module for processing requests
+    PerlSetVar OILSRemoteAuthHandler "OpenILS::WWW::RemoteAuth::Basic"
+
+    # staff username/password for profile lookup and patron retrieval
+    PerlSetVar OILSRemoteAuthClientUsername "admin"
+    PerlSetVar OILSRemoteAuthClientPassword "demo123"
+</Location>
+....
+
+Here, the URL path `/api/basicauth` is our endpoint.  External clients send appropriately-constructed requests to this URL and get a response indicating whether auth succeeded (and containing patron account information, depending on how the endpoint is configured).
+
+Since different external services have different requirements for patron auth, each RemoteAuth endpoint handles requests differently.  All endpoints use `OpenILS::WWW::RemoteAuth` as the main mod_perl handler, but specific implementation details are handled by the module specified by the OILSRemoteAuthHandler variable -- in this case, `OpenILS::WWW::RemoteAuth::Basic` for Basic HTTP Authentication endpoints.
+
+The OILSRemoteAuthProfile variable specifies the name of a profile in the config.remoteauth_profile database table (see "Database configuration" below).
+
+OILSRemoteAuthClientUsername and OILSRemoteAuthClientPassword are the username and password of an Evergreen user account that has the permissions necessary to (1) view users at this location and (2) retrieve config entries from the config.remoteauth_profile table.  Ideally, these credentials would be provided in the actual request.  Where that's not possible, as in the present case, we include the client credentials in our Apache configuration.
+
+When an endpoint doesn't use client-provided authorization credentials, we may wish to restrict access to the endpoint by IP address or other means.  By default, Basic auth only allows connections originating from the same server ("Require local").  To make an endpoint publicly accessible, use "Require all granted".  To restrict access by IP, use "Require ip <ip-address>".
+
+
+Database configuration
+++++++++++++++++++++++
+
+In each endpoint's Apache configuration, the OILSRemoteAuthProfile variable specifies the name of an entry in the config.remoteauth_profile table (see "Apache configuration" above).  The profile tells us the context org unit for requests at this endpoint and defines rules for permitting auth requests.  Supported rules include:
+
+* *perm:* a permission which the user must have in order to be authenticated via RemoteAuth
+* *restrict_to_org:* only allow users belonging to this location to be authenticated, even if the client can retrieve users at other locations
+* *allow_inactive:* allow authentication for inactive users
+* *allow_expired:* allow authentication for expired users
+* *block_list:* authentication is not permitted if the user has a standing penalty at this location with one of these blocks
+
+RemoteAuth uses the actor.permit_remoteauth database function to apply these rules during an authentication request.  For example, if allow_expired is false and the user is expired, the database function will respond with "expired" and RemoteAuth will deny the authentication request.
+
+You can use the same configuration for multiple endpoints.  For example, suppose you need separate endpoints for EZProxy and Basic HTTP Authentication, but you want both endpoints to use the same auth rules.  In that case, each endpoint gets a separate entry in `eg_vhost.conf`, but the profile name specified by the OILSRemoteAuthProfile variable will be the same for both endpoints.
+
+
+Test plan
++++++++++
+
+. Install the branch on a test server and load concerto data.  Basic auth will be enabled by default for localhost access only.
+. Generate base64-encoded credentials for your test user: `echo -n "<username>:<password>" | base64`
+. Query the basic auth endpoint: `curl -k -s -o /dev/null -I -w "%{http_code}\n" https://localhost/api/basicauth -H "Authorization: Basic <base64-encoded-credentials>"`
+
+This will return 200 if auth is successful, and 403 if auth fails or is not permitted.
+
+There's also a Perl live test.  However, the live test may fail in some environments due to an https://github.com/libwww-perl/libwww-perl/issues/83#issuecomment-405233929[upstream bug] in `LWP::Protocol::https` that prevents us from skipping certificate verification.  The packaged version of that module in Ubuntu 16.04 is affected; installing `LWP::Protocol::https` version >=6.07 from CPAN resolves the problem.
+

commit ccbcd4773e363da7e1ffa8c5d92a44db04cd43de
Author: Jeff Davis <jeff.davis at bc.libraries.coop>
Date:   Mon Mar 4 16:34:58 2019 -0800

    LP#1817645: RemoteAuth Perl live test for basic HTTP authentication (RFC 7617)
    
    Signed-off-by: Jeff Davis <jeff.davis at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/perlmods/live_t/29-lp1817645-remoteauth-patron-api.t b/Open-ILS/src/perlmods/live_t/29-lp1817645-remoteauth-patron-api.t
new file mode 100644
index 0000000000..13a4a0e8f6
--- /dev/null
+++ b/Open-ILS/src/perlmods/live_t/29-lp1817645-remoteauth-patron-api.t
@@ -0,0 +1,140 @@
+#!perl
+
+use Test::More tests => 9; # XXX
+
+diag("Tests RemoteAuth patron auth/retrieval");
+
+use strict; use warnings;
+use OpenILS::Utils::TestUtils;
+use MIME::Base64;
+use HTTP::Request;
+use LWP::UserAgent;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Application::AppUtils;
+our $U = "OpenILS::Application::AppUtils";
+
+OpenILS::Utils::TestUtils->new->bootstrap;
+
+my $not_found = { barcode => '99999393000', password => 'nonexistentbarcode' };
+my $expired = { barcode => '99999393001', password => 'marges1234' };
+my $deleted = { barcode => '99999393002', password => 'homers1234' };
+my $barred = { barcode => '99999393003', password => 'barts1234' };
+my $valid = { barcode => '99999393004', password => 'lisas1234' };
+my $inactive = { barcode => '99999393005', password => 'maggies1234' };
+my $external = { barcode => '99999393100', password => 'shelbyvillem1234' };
+
+# context org is SYS1, test user's home OU is BR1;
+# use BR3 (under SYS2) to test external users
+my $external_org = 6; # BR3
+
+my $staff_login = $U->simplereq(
+    'open-ils.auth',
+    'open-ils.auth.login', {
+        username => 'admin',
+        password => 'demo123',
+        type => 'staff'
+    }
+);
+is($staff_login->{textcode}, 'SUCCESS', 'Staff login OK');
+my $e = new_editor( authtoken => $staff_login->{payload}->{authtoken} );
+
+my $client = LWP::UserAgent->new;
+$client->ssl_opts( verify_hostname => 0 );
+
+# my $res = $client->request( $method, $uri, $headers, $content, $request_timeout );
+
+
+######################################################################
+
+# requests:
+# - validate barcode only?
+# - validate barcode + PIN
+# - retrieve user and check for required fields in response
+
+# test cases:
+# - valid user with username or barcode (opac.barcode_regex)
+# - valid user with barcode prefix
+# - valid user with barcode
+# - invalid password
+# - barcode not found
+# - user is deleted
+# - user is expired
+# - user is barred
+# - user has non-blocking penalties, auth/retrieval succeeds
+# - user has blocking penalties
+# - user exists, but home OU is not in scope
+
+# not currently supported:
+# - AuthProxy: services should use the remote auth server directly
+# - opted-in patrons: these are external patrons and thus not auth'd
+
+######################################################################
+
+#---------------------------------------------------------------------
+# Basic access authentication (RFC 7617)
+#---------------------------------------------------------------------
+# - endpoint: /api/basicauth
+# - client auth: none
+# - request: includes "Authorization: Basic <credentials>" header,
+#   credentials = Base64-encoded "id:password" string
+# - response: HTTP 200 on success, 401 with WWW-Authenticate header 
+#   field on failure
+# - does not return patron info
+#---------------------------------------------------------------------
+sub basic_request {
+    my ($u, $password) = @_;
+    my $barcode = $u->{barcode};
+    $password ||= $u->{password};
+    my $resp = $client->get(
+        "https://localhost/api/basicauth",
+        'Authorization' => 'Basic ' . encode_base64("$barcode:$password")
+    );
+    return $resp->code;
+}
+
+my $basic_not_found = basic_request($not_found);
+is ( $basic_not_found, '403', 'Basic request for nonexistent barcode correctly returned 403' );
+
+my $basic_success = basic_request($valid);
+is( $basic_success, '200', 'Basic request for valid patron OK' );
+
+# invalid password
+my $basic_invalid_pw = basic_request($valid, 'badpassword');
+is( $basic_invalid_pw, '403', 'Basic request with invalid password correctly returned 403' );
+
+# user is deleted
+my $basic_deleted = basic_request($deleted);
+is( $basic_deleted, '403', 'Basic request for deleted user correctly returned 403' );
+
+# user is expired
+my $basic_expired = basic_request($expired);
+is( $basic_expired, '403', 'Basic request for expired user correctly returned 403' );
+
+# user is inactive
+my $basic_inactive = basic_request($inactive);
+is( $basic_inactive, '403', 'Basic request for inactive user correctly returned 403' );
+
+# user is barred
+my $basic_barred = basic_request($barred);
+is( $basic_barred, '403', 'Basic request for barred user correctly returned 403' );
+
+# home OU is not in scope
+my $basic_external = basic_request($external);
+is( $basic_external, '403', 'Basic request for external user correctly returned 403' );
+
+# TODO: user has blocking penalties
+
+# TODO: user has non-blocking penalties, auth/retrieval succeeds
+
+
+
+# TODO: EZProxy external script authentication:
+# - endpoint: /remoteauth/ezproxy/<shortname>/<id>/<password>
+# - client auth: none
+# - request: GET with user and pass params, as above
+# - response: "+VALID" if auth succeeds
+
+
+# TODO: verify user activity based on the above tests
+
+

commit dfefabd1649a644ed9972a0227be877ca98908ec
Author: Jeff Davis <jeff.davis at bc.libraries.coop>
Date:   Mon Mar 4 16:12:45 2019 -0800

    LP#1817645: RemoteAuth pgTAP test
    
    Signed-off-by: Jeff Davis <jeff.davis at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/sql/Pg/live_t/lp1817645-remoteauth.pg b/Open-ILS/src/sql/Pg/live_t/lp1817645-remoteauth.pg
new file mode 100644
index 0000000000..2982d0a3db
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/live_t/lp1817645-remoteauth.pg
@@ -0,0 +1,63 @@
+BEGIN;
+
+SELECT plan(8);
+
+-- test accounts:
+-- 99999393000: not found (user does not exist)
+-- 99999393001: expired
+-- 99999393002: deleted
+-- 99999393003: barred
+-- 99999393004: valid
+-- 99999393005: inactive
+-- 99999393100: external
+
+SELECT isnt_empty(
+  'SELECT * FROM config.remoteauth_profile WHERE enabled IS TRUE AND name = ''Basic''',
+  'Basic HTTP authentication is enabled'
+);
+
+SELECT is(
+  (SELECT * FROM actor.permit_remoteauth('Basic', (SELECT id FROM actor.usr WHERE usrname = '99999393000')),
+  'not_found',
+  'Confirm remoteauth test for nonexistent patron'
+);
+
+SELECT is(
+  (SELECT * FROM actor.permit_remoteauth('Basic', (SELECT id FROM actor.usr WHERE usrname = '99999393001')),
+  'expired',
+  'Confirm remoteauth test for expired patron'
+);
+
+SELECT is(
+  (SELECT * FROM actor.permit_remoteauth('Basic', (SELECT id FROM actor.usr WHERE usrname = '99999393002')),
+  'not_found',
+  'Confirm remoteauth test for deleted patron'
+);
+
+SELECT is(
+  (SELECT * FROM actor.permit_remoteauth('Basic', (SELECT id FROM actor.usr WHERE usrname = '99999393003')),
+  'blocked',
+  'Confirm remoteauth test for barred patron'
+);
+
+SELECT is(
+  (SELECT * FROM actor.permit_remoteauth('Basic', (SELECT id FROM actor.usr WHERE usrname = '99999393004')),
+  'success',
+  'Confirm remoteauth test for valid patron'
+);
+
+SELECT is(
+  (SELECT * FROM actor.permit_remoteauth('Basic', (SELECT id FROM actor.usr WHERE usrname = '99999393005')),
+  'blocked',
+  'Confirm remoteauth test for inactive patron'
+);
+
+SELECT is(
+  (SELECT * FROM actor.permit_remoteauth('Basic', (SELECT id FROM actor.usr WHERE usrname = '99999393100')),
+  'not_found',
+  'Confirm remoteauth test for external patron'
+);
+
+-- Finish the tests and clean up.
+SELECT * FROM finish();
+ROLLBACK;

commit 6e74775849baa2e16673b1ae94d750d8119df577
Author: Jeff Davis <jeff.davis at bc.libraries.coop>
Date:   Mon Mar 4 14:53:43 2019 -0800

    LP#1817645: sample data for testing remote patron auth
    
    Signed-off-by: Jeff Davis <jeff.davis at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/tests/datasets/sql/load_all.sql b/Open-ILS/tests/datasets/sql/load_all.sql
index f1a20fc3a6..128372e202 100644
--- a/Open-ILS/tests/datasets/sql/load_all.sql
+++ b/Open-ILS/tests/datasets/sql/load_all.sql
@@ -99,6 +99,9 @@ INSERT INTO biblio.record_entry (marc, last_xact_id)
 -- load survey data
 \i surveys.sql
 
+-- load remoteauth data
+\i remoteauth.sql
+
 -- clean up the env
 \i env_destroy.sql
 
diff --git a/Open-ILS/tests/datasets/sql/remoteauth.sql b/Open-ILS/tests/datasets/sql/remoteauth.sql
new file mode 100644
index 0000000000..be0c7998c5
--- /dev/null
+++ b/Open-ILS/tests/datasets/sql/remoteauth.sql
@@ -0,0 +1,7 @@
+-- config for Basic HTTP Authentication (SYS1)
+INSERT INTO config.remoteauth_profile
+    (name, description, context_org, enabled, perm,
+        restrict_to_org, allow_inactive, allow_expired, block_list)
+    VALUES ('Basic', 'Basic HTTP Authentication for SYS1', 2, TRUE, 1,
+        TRUE, FALSE, FALSE, NULL);
+
diff --git a/Open-ILS/tests/datasets/sql/users_patrons_100.sql b/Open-ILS/tests/datasets/sql/users_patrons_100.sql
index 5923e87f98..e3ff2581dd 100644
--- a/Open-ILS/tests/datasets/sql/users_patrons_100.sql
+++ b/Open-ILS/tests/datasets/sql/users_patrons_100.sql
@@ -2146,3 +2146,141 @@ UPDATE actor.usr SET
     credit_forward_balance = '0',
     mailing_address = CURRVAL('actor.usr_address_id_seq')
     WHERE id=CURRVAL('actor.usr_id_seq');
+
+
+-- users for auth testing
+-- barcode pattern: 99999393XXX
+
+-- expired
+INSERT INTO actor.usr
+    (profile, ident_type, usrname, home_ou, family_name, passwd, first_given_name, second_given_name, expire_date, dob, suffix)
+    VALUES (2, 3, '99999393001', 4, 'Simpson', 'marges1234',
+        'Marge', '', NOW() + '3 years'::INTERVAL, NULL, '');
+
+INSERT INTO actor.usr_address
+    (country, within_city_limits, post_code, street1, valid, state, city, street2, county, usr)
+    VALUES ('USA', 't', '20521', '742 Evergreen Terrace', 't',
+        'NT', 'Springfield', '', '', CURRVAL('actor.usr_id_seq'));
+
+INSERT INTO actor.card (barcode, usr)
+    VALUES ('99999393001', CURRVAL('actor.usr_id_seq'));
+
+UPDATE actor.usr SET
+    card = CURRVAL('actor.card_id_seq'),
+    billing_address = CURRVAL('actor.usr_address_id_seq'),
+    credit_forward_balance = '0',
+    mailing_address = CURRVAL('actor.usr_address_id_seq')
+    WHERE id=CURRVAL('actor.usr_id_seq');
+
+UPDATE actor.usr SET expire_date = '2010-01-01' WHERE id=CURRVAL('actor.usr_id_seq');
+
+-- deleted
+INSERT INTO actor.usr
+    (profile, ident_type, usrname, home_ou, family_name, passwd, first_given_name, second_given_name, expire_date, dob, suffix)
+    VALUES (2, 3, '99999393002', 4, 'Simpson', 'homers1234',
+        'Homer', '', NOW() + '3 years'::INTERVAL, NULL, '');
+
+INSERT INTO actor.usr_address
+    (country, within_city_limits, post_code, street1, valid, state, city, street2, county, usr)
+    VALUES ('USA', 't', '20521', '742 Evergreen Terrace', 't',
+        'NT', 'Springfield', '', '', CURRVAL('actor.usr_id_seq'));
+
+INSERT INTO actor.card (barcode, usr)
+    VALUES ('99999393002', CURRVAL('actor.usr_id_seq'));
+
+UPDATE actor.usr SET
+    card = CURRVAL('actor.card_id_seq'),
+    billing_address = CURRVAL('actor.usr_address_id_seq'),
+    credit_forward_balance = '0',
+    mailing_address = CURRVAL('actor.usr_address_id_seq')
+    WHERE id=CURRVAL('actor.usr_id_seq');
+
+UPDATE actor.usr SET deleted = TRUE WHERE id=CURRVAL('actor.usr_id_seq');
+
+-- barred
+INSERT INTO actor.usr
+    (profile, ident_type, usrname, home_ou, family_name, passwd, first_given_name, second_given_name, expire_date, dob, suffix)
+    VALUES (2, 3, '99999393003', 4, 'Simpson', 'barts1234',
+        'Bart', '', NOW() + '3 years'::INTERVAL, NULL, '');
+
+INSERT INTO actor.usr_address
+    (country, within_city_limits, post_code, street1, valid, state, city, street2, county, usr)
+    VALUES ('USA', 't', '20521', '742 Evergreen Terrace', 't',
+        'NT', 'Springfield', '', '', CURRVAL('actor.usr_id_seq'));
+
+INSERT INTO actor.card (barcode, usr)
+    VALUES ('99999393003', CURRVAL('actor.usr_id_seq'));
+
+UPDATE actor.usr SET
+    card = CURRVAL('actor.card_id_seq'),
+    billing_address = CURRVAL('actor.usr_address_id_seq'),
+    credit_forward_balance = '0',
+    mailing_address = CURRVAL('actor.usr_address_id_seq')
+    WHERE id=CURRVAL('actor.usr_id_seq');
+
+UPDATE actor.usr SET barred = TRUE WHERE id=CURRVAL('actor.usr_id_seq');
+
+-- valid
+INSERT INTO actor.usr
+    (profile, ident_type, usrname, home_ou, family_name, passwd, first_given_name, second_given_name, expire_date, dob, suffix)
+    VALUES (2, 3, '99999393004', 4, 'Simpson', 'lisas1234',
+        'Lisa', '', NOW() + '3 years'::INTERVAL, NULL, '');
+
+INSERT INTO actor.usr_address
+    (country, within_city_limits, post_code, street1, valid, state, city, street2, county, usr)
+    VALUES ('USA', 't', '20521', '742 Evergreen Terrace', 't',
+        'NT', 'Springfield', '', '', CURRVAL('actor.usr_id_seq'));
+
+INSERT INTO actor.card (barcode, usr)
+    VALUES ('99999393004', CURRVAL('actor.usr_id_seq'));
+
+UPDATE actor.usr SET
+    card = CURRVAL('actor.card_id_seq'),
+    billing_address = CURRVAL('actor.usr_address_id_seq'),
+    credit_forward_balance = '0',
+    mailing_address = CURRVAL('actor.usr_address_id_seq')
+    WHERE id=CURRVAL('actor.usr_id_seq');
+
+-- inactive
+INSERT INTO actor.usr
+    (profile, ident_type, usrname, home_ou, family_name, passwd, first_given_name, second_given_name, expire_date, dob, suffix)
+    VALUES (2, 3, '99999393005', 4, 'Simpson', 'maggies1234',
+        'Maggie', '', NOW() + '3 years'::INTERVAL, NULL, '');
+
+INSERT INTO actor.usr_address
+    (country, within_city_limits, post_code, street1, valid, state, city, street2, county, usr)
+    VALUES ('USA', 't', '20521', '742 Evergreen Terrace', 't',
+        'NT', 'Springfield', '', '', CURRVAL('actor.usr_id_seq'));
+
+INSERT INTO actor.card (barcode, usr)
+    VALUES ('99999393005', CURRVAL('actor.usr_id_seq'));
+
+UPDATE actor.usr SET
+    card = CURRVAL('actor.card_id_seq'),
+    billing_address = CURRVAL('actor.usr_address_id_seq'),
+    credit_forward_balance = '0',
+    mailing_address = CURRVAL('actor.usr_address_id_seq')
+    WHERE id=CURRVAL('actor.usr_id_seq');
+
+UPDATE actor.usr SET active = FALSE WHERE id=CURRVAL('actor.usr_id_seq');
+
+-- external
+INSERT INTO actor.usr
+    (profile, ident_type, usrname, home_ou, family_name, passwd, first_given_name, second_given_name, expire_date, dob, suffix)
+    VALUES (2, 3, '99999393100', 6, 'Manhattan', 'shelbyvillem1234',
+        'Shelbyville', '', NOW() + '3 years'::INTERVAL, NULL, '');
+
+INSERT INTO actor.usr_address
+    (country, within_city_limits, post_code, street1, valid, state, city, street2, county, usr)
+    VALUES ('USA', 't', '20521', '1 Shelbyville Way', 't',
+        'NT', 'Shelbyville', '', '', CURRVAL('actor.usr_id_seq'));
+
+INSERT INTO actor.card (barcode, usr)
+    VALUES ('99999393100', CURRVAL('actor.usr_id_seq'));
+
+UPDATE actor.usr SET
+    card = CURRVAL('actor.card_id_seq'),
+    billing_address = CURRVAL('actor.usr_address_id_seq'),
+    credit_forward_balance = '0',
+    mailing_address = CURRVAL('actor.usr_address_id_seq')
+    WHERE id=CURRVAL('actor.usr_id_seq');

commit 69a23a05570d4e1d5980c7af852b3abe3f3069a8
Author: Jeff Davis <jeff.davis at bc.libraries.coop>
Date:   Mon Mar 4 16:48:23 2019 -0800

    LP#1817645: RemoteAuth handler for basic HTTP authentication (RFC 7617)
    
    Signed-off-by: Jeff Davis <jeff.davis at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/examples/apache_24/eg_startup.in b/Open-ILS/examples/apache_24/eg_startup.in
index f805c60f42..27b1abf23b 100755
--- a/Open-ILS/examples/apache_24/eg_startup.in
+++ b/Open-ILS/examples/apache_24/eg_startup.in
@@ -14,7 +14,7 @@ use OpenILS::WWW::EGWeb ('@sysconfdir@/opensrf_core.xml', 'OpenILS::WWW::EGCatLo
 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');
+use OpenILS::WWW::RemoteAuth ('@sysconfdir@/opensrf_core.xml', 'OpenILS::WWW::RemoteAuth::Basic');
 
 # Pass second argument of '1' to enable template caching.
 use OpenILS::WWW::PrintTemplate ('/openils/conf/opensrf_core.xml', 0);
diff --git a/Open-ILS/examples/apache_24/eg_vhost.conf.in b/Open-ILS/examples/apache_24/eg_vhost.conf.in
index 43e17704ee..153735a389 100644
--- a/Open-ILS/examples/apache_24/eg_vhost.conf.in
+++ b/Open-ILS/examples/apache_24/eg_vhost.conf.in
@@ -836,6 +836,26 @@ RewriteRule ^/openurl$ ${openurl:%1} [NE,PT]
     </IfModule>
 </Location>
 
+<Location /api/basicauth>
+    SetHandler perl-script
+    PerlHandler OpenILS::WWW::RemoteAuth
+    Options +ExecCGI
+
+    # access restricted to localhost by default; since this module provides no
+    # client authentiation, restricting access by IP or other means is stongly
+    # recommended
+    Require local
+
+    # remoteauth profile name
+    PerlSetVar OILSRemoteAuthProfile "Basic"
+    # Perl module for processing requests
+    PerlSetVar OILSRemoteAuthHandler "OpenILS::WWW::RemoteAuth::Basic"
+
+    # staff username/password for config lookup and patron retrieval
+    PerlSetVar OILSRemoteAuthClientUsername "admin"
+    PerlSetVar OILSRemoteAuthClientPassword "demo123"
+</Location>
+
 
 # Uncomment the following to force SSL for everything. Note that this defeats caching
 # and you will suffer a performance hit.
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth/Basic.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth/Basic.pm
new file mode 100644
index 0000000000..cae24f9762
--- /dev/null
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth/Basic.pm
@@ -0,0 +1,144 @@
+# 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.
+
+# ====================================================================== 
+# - RemoteAuth handler for HTTP basic access authorization (RFC 7617)
+# - patron credentials are Bas64-encoded in Authorization header
+# - no client authorization - restricting access by IP or other methods
+#   is strongly recommended!
+# ====================================================================== 
+
+package OpenILS::WWW::RemoteAuth::Basic;
+use strict; use warnings;
+use OpenILS::WWW::RemoteAuth;
+use base "OpenILS::WWW::RemoteAuth";
+
+use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN AUTH_REQUIRED HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
+use MIME::Base64;
+use OpenSRF::EX qw(:try);
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenSRF::Utils::JSON;
+
+sub new {
+    my( $class, $args ) = @_;
+    $args ||= {};
+    $class = ref $class || $class;
+    return bless($args, $class);
+}
+
+# here's our main method; it controls the various steps of the auth flow,
+# prepares the response content, and returns an HTTP status code
+sub process {
+    my ($self, $r) = @_;
+    my ($authtoken, $editor, $config);
+
+    # authorize client
+    try {
+        my $client_user = $r->dir_config('OILSRemoteAuthClientUsername');
+        my $client_pw = $r->dir_config('OILSRemoteAuthClientPassword');
+        $authtoken = $self->do_client_auth($client_user, $client_pw);
+    } catch Error with {
+        $logger->error("RemoteAuth Basic failed on client auth: @_");
+        return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+    };
+    return $self->client_not_authorized unless $authtoken;
+
+    # load config
+    try {
+        $editor = new_editor( authtoken => $authtoken );
+        $config = $self->load_config($editor, $r);
+    } catch Error with {
+        $logger->error("RemoteAuth Basic failed on load config: @_");
+        return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+    };
+    return $self->backend_error unless $config;
+
+    # extract patron id/password from Authorization request header
+    my $auth_header = $r->headers_in->get('Authorization');
+    unless (defined $auth_header && $auth_header =~ /^Basic /) {
+        # include WWW-Authenticate header on 401 responses, per RFC 7617
+        my $name = $config->name;
+        $r->err_headers_out->add('WWW-Authenticate' => "Basic realm=\"$name\"");
+        return Apache2::Const::AUTH_REQUIRED;
+    }
+    $auth_header =~ s/^Basic //;
+    my ($id, $password) = split(/:/, decode_base64($auth_header), 2);
+
+    # authenticate patron
+    my $stat = $self->do_patron_auth($editor, $config, $id, $password);
+    return $stat unless $stat == Apache2::Const::OK;
+
+    # XXX RFC 7617 doesn't require any particular content in the body of the
+    # response.  The response content could be made configurable, but for now,
+    # let's respond with a simple JSON message containing the username/barcode
+    # used to authenticate the user: it's a predictable response, it doesn't
+    # require us to retrieve any additional patron information, and it's
+    # compatible with the Apereo CAS server's requirements for remote REST
+    # authentication, as documented here:
+    # https://apereo.github.io/cas/5.0.x/installation/Rest-Authentication.html
+
+    my $response_content = { id => $id };
+    $r->content_type('application/json');
+    $r->print( OpenSRF::Utils::JSON->perl2JSON($response_content) );
+    return Apache2::Const::OK;
+
+}
+
+# ... and here are all our util methods:
+
+# success
+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::FORBIDDEN;
+}
+
+# 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::FORBIDDEN;
+}
+
+1;
+

commit 8589d20554a27ec6ba27d2fb43d5b72bbc75d6dd
Author: Jeff Davis <jeff.davis at bc.libraries.coop>
Date:   Tue Feb 26 18:02:58 2019 -0800

    LP#1817645: configurable HTTP API for patron auth/retrieval
    
    Signed-off-by: Jeff Davis <jeff.davis at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/examples/apache_24/eg.conf.in b/Open-ILS/examples/apache_24/eg.conf.in
index 2ee0153ad2..b872a3e1a0 100644
--- a/Open-ILS/examples/apache_24/eg.conf.in
+++ b/Open-ILS/examples/apache_24/eg.conf.in
@@ -21,6 +21,7 @@ PerlChildInitHandler OpenILS::WWW::AddedContent::child_init
 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
diff --git a/Open-ILS/examples/apache_24/eg_startup.in b/Open-ILS/examples/apache_24/eg_startup.in
index 0ced7a9787..f805c60f42 100755
--- a/Open-ILS/examples/apache_24/eg_startup.in
+++ b/Open-ILS/examples/apache_24/eg_startup.in
@@ -14,6 +14,7 @@ use OpenILS::WWW::EGWeb ('@sysconfdir@/opensrf_core.xml', 'OpenILS::WWW::EGCatLo
 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);
@@ -27,6 +28,5 @@ use OpenILS::WWW::PrintTemplate ('/openils/conf/opensrf_core.xml', 0);
 #OpenILS::WWW::Redirect->parse_ips_file('@sysconfdir@/lib_ips.txt');
 
 
-
 1;
 
diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index b8fde1a4c6..dc236aeb55 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -12940,6 +12940,32 @@ SELECT  usr,
 		</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>
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth.pm
new file mode 100644
index 0000000000..7726429c34
--- /dev/null
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth.pm
@@ -0,0 +1,233 @@
+# 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;
+
diff --git a/Open-ILS/src/sql/Pg/150.remoteauth.sql b/Open-ILS/src/sql/Pg/150.remoteauth.sql
new file mode 100644
index 0000000000..0e7f823cb1
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/150.remoteauth.sql
@@ -0,0 +1,85 @@
+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;
+
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 9637a212ff..fb8f0b5ad6 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -1923,7 +1923,9 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES
  ( 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' ))
 ;
 
 
diff --git a/Open-ILS/src/sql/Pg/sql_file_manifest b/Open-ILS/src/sql/Pg/sql_file_manifest
index 97c92a0a35..e4f9152166 100644
--- a/Open-ILS/src/sql/Pg/sql_file_manifest
+++ b/Open-ILS/src/sql/Pg/sql_file_manifest
@@ -36,6 +36,7 @@ FTS_CONFIG_FILE
 100.circ_matrix.sql
 110.hold_matrix.sql
 120.floating_groups.sql
+150.remoteauth.sql
 
 210.schema.serials.sql
 200.schema.acq.sql
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.remoteauth.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.remoteauth.sql
new file mode 100644
index 0000000000..6936d7e3bf
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.remoteauth.sql
@@ -0,0 +1,91 @@
+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;
+

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/examples/apache_24/eg.conf.in             |   1 +
 Open-ILS/examples/apache_24/eg_startup.in          |   2 +-
 Open-ILS/examples/apache_24/eg_vhost.conf.in       |  20 ++
 Open-ILS/examples/fm_IDL.xml                       |  30 ++-
 .../server/admin-server-splash.component.html      |   2 +
 .../src/perlmods/lib/OpenILS/WWW/RemoteAuth.pm     | 237 +++++++++++++++++++++
 .../perlmods/lib/OpenILS/WWW/RemoteAuth/Basic.pm   | 144 +++++++++++++
 .../live_t/29-lp1817645-remoteauth-patron-api.t    | 143 +++++++++++++
 Open-ILS/src/sql/Pg/002.schema.config.sql          |   2 +-
 Open-ILS/src/sql/Pg/150.remoteauth.sql             |  86 ++++++++
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |   4 +-
 Open-ILS/src/sql/Pg/live_t/lp1817645-remoteauth.pg |  63 ++++++
 Open-ILS/src/sql/Pg/sql_file_manifest              |   1 +
 .../src/sql/Pg/upgrade/1180.schema.remoteauth.sql  |  92 ++++++++
 Open-ILS/tests/datasets/sql/load_all.sql           |   3 +
 Open-ILS/tests/datasets/sql/remoteauth.sql         |  11 +
 Open-ILS/tests/datasets/sql/users_patrons_100.sql  | 138 ++++++++++++
 .../Architecture/remoteauth.adoc                   |  18 ++
 docs/TechRef/remoteauth.adoc                       |  88 ++++++++
 19 files changed, 1081 insertions(+), 4 deletions(-)
 create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth.pm
 create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/WWW/RemoteAuth/Basic.pm
 create mode 100644 Open-ILS/src/perlmods/live_t/29-lp1817645-remoteauth-patron-api.t
 create mode 100644 Open-ILS/src/sql/Pg/150.remoteauth.sql
 create mode 100644 Open-ILS/src/sql/Pg/live_t/lp1817645-remoteauth.pg
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1180.schema.remoteauth.sql
 create mode 100644 Open-ILS/tests/datasets/sql/remoteauth.sql
 create mode 100644 docs/RELEASE_NOTES_NEXT/Architecture/remoteauth.adoc
 create mode 100644 docs/TechRef/remoteauth.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list