LP#1413624: OpenILS::WWW::AccessHandler Module
authorThomas Berezansky <tsbere@mvlc.org>
Tue, 20 Jan 2015 17:42:53 +0000 (12:42 -0500)
committerGalen Charlton <gmc@esilibrary.com>
Thu, 19 Feb 2015 16:17:13 +0000 (16:17 +0000)
Perl module compatible with Apache PerlAccessHandler directives.

Includes Release Notes and basic documentation (in the form of a mildly edited
copy of the release notes)

Signed-off-by: Thomas Berezansky <tsbere@mvlc.org>
Signed-off-by: Galen Charlton <gmc@esilibrary.com>
Open-ILS/src/perlmods/lib/OpenILS/WWW/AccessHandler.pm [new file with mode: 0644]
docs/RELEASE_NOTES_NEXT/Administration/apache_access_handler.txt [new file with mode: 0644]
docs/TechRef/Apache/apache_access_handler.txt [new file with mode: 0644]

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/AccessHandler.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/AccessHandler.pm
new file mode 100644 (file)
index 0000000..e9227e4
--- /dev/null
@@ -0,0 +1,216 @@
+package OpenILS::WWW::AccessHandler;
+use strict; use warnings;
+
+# Apache Requirements
+use Apache2::Const -compile => qw(:common HTTP_OK HTTP_FORBIDDEN HTTP_MOVED_TEMPORARILY HTTP_MOVED_PERMANENTLY);
+use Apache2::RequestRec;
+use Apache2::URI;
+
+# OpenSRF requirements
+use OpenSRF::System;
+
+# Other requirements
+use URI::Escape;
+
+# Auth Handler
+sub handler {
+    my ($r) = @_;
+
+    # Configuration options
+
+    # URL to redirect to for login
+    my $lurl = $r->dir_config('OILSAccessHandlerLoginURL') || '/eg/opac/login';
+    # Redirection variable to come "back" from the previous URL
+    my $lurlvar = $r->dir_config('OILSAccessHandlerLoginURLRedirectVar') || 'redirect_to';
+    # No access page? If not set, just return forbidden.
+    my $failurl = $r->dir_config('OILSAccessHandlerFailURL');
+    # Required permission (if set)
+    my $userperm = $r->dir_config('OILSAccessHandlerPermission');
+    # Need to be in good standing?
+    my $userstanding = $r->dir_config('OILSAccessHandlerGoodStanding') || 0;
+    # Required org unit (if set)
+    my $userou = $r->dir_config('OILSAccessHandlerHomeOU');
+    # OU for permission/standing checks, if not set use home OU
+    my $checkou = $r->dir_config('OILSAccessHandlerCheckOU');
+    # Set referrer based on OU Setting?
+    my $referrersetting = $r->dir_config('OILSAccessHandlerReferrerSetting');
+
+    my $url = $r->construct_url();
+
+    # push everyone to the secure site
+    if ($url =~ /^http:/o) {
+        my $target = $r->construct_url($r->unparsed_uri);
+        $target =~ s/^http:/https:/o;
+        $r->headers_out->set(Location => $target);
+        return Apache2::Const::HTTP_MOVED_PERMANENTLY;
+    }
+
+    # We could use CGI....but that creates issues if post data may be submitted
+    my $auth_ses = ($r->headers_in->get('Cookie') =~ /(?:^|\s)ses=([^;]*)/)[0];
+    my $user = _verify_login($auth_ses);
+
+    if (!defined($user)) {
+        my $redirect = $r->construct_url($r->unparsed_uri);
+        my $target = $r->construct_url($lurl) . '?' . $lurlvar . '=' . uri_escape($redirect);
+        $target =~ s/^http:/https:/o; # This should never be needed due to the redirect above, but better safe than sorry
+        $r->headers_out->set(Location => $target);
+        # Lets not cache this either, just in case.
+        $r->headers_out->set('Cache-Control' => 'no-cache, no-store, must-revalidate');
+        $r->headers_out->set(Pragma => 'no-cache');
+        $r->headers_out->set(Expires => 0);
+        return Apache2::Const::HTTP_MOVED_TEMPORARILY;
+    }
+
+    # Convert check OU from shortname, if needed
+    $checkou = _get_org_id($checkou);
+
+    # If we have no check OU at this point, use the user's home OU
+    $checkou ||= $user->home_ou;
+
+    my $failed = 0;
+
+    if ($userperm) {
+        my @permissions = split(/\s*,\s*/, $userperm);
+        $failed++ unless _verify_permission($auth_ses, $user, $checkou, \@permissions);
+    }
+    if (!$failed && $userstanding) {
+        $failed++ unless _verify_standing($user);
+    }
+    if (!$failed && $userou) {
+        $failed++ unless _verify_home_ou($user, $userou);
+    }
+
+    # If we failed one of the above checks they aren't allowed in
+    if ($failed > 0) {
+        if ($failurl) {
+            my $target = $r->construct_url($failurl);
+            $r->headers_out->set(Location => $target);
+            return Apache2::Const::HTTP_MOVED_TEMPORARILY;
+        } else {
+            return Apache2::Const::HTTP_FORBIDDEN;
+        }
+    }
+
+    # Forced referrer for some referrer auth services?
+    if ($referrersetting) {
+        my $referrervalue = _get_ou_setting($referrersetting, $checkou);
+        if ($referrervalue && $referrervalue->{value}) {
+            $r->headers_in->set('Referer', $referrervalue->{value});
+        }
+    }
+
+    # If we haven't thrown them out yet, let them through
+    return Apache2::Const::OK;
+}
+
+# "Private" functions
+
+# Verify our login
+sub _verify_login {
+    my ($token) = @_;
+    return undef unless $token;
+
+    my $user = OpenSRF::AppSession
+        ->create('open-ils.auth')
+        ->request('open-ils.auth.session.retrieve', $token)
+        ->gather(1);
+
+    if (ref($user) eq 'HASH' && $user->{ilsevent} == 1001) {
+        return undef;
+    }
+
+    return $user if ref($user);
+    return undef;
+}
+
+# OU Shortname to ID
+sub _get_org_id {
+    my ($org_identifier) = @_;
+    # Does this look like a number?
+    if ($org_identifier !~ '^[0-9]+$') {
+        # Not a database id
+        if ($org_identifier) { 
+            # Look up org unit by shortname
+            my $org_unit = OpenSRF::AppSession
+                ->create('open-ils.actor')
+                ->request('open-ils.actor.org_unit.retrieve_by_shortname', $org_identifier)
+                ->gather(1);
+            if ($org_unit && ref($org_unit) != 'HASH') {
+                # We appear to have an org unit! So return the ID.
+                return $org_unit->id;
+            }
+        }
+    } else {
+        # Looks like a database ID, so leave it alone.
+        return $org_identifier;
+    }
+    # If we have reached this point, assume that we found no useful ID.
+    return undef;
+}
+
+# Verify home OU
+sub _verify_home_ou {
+    my ($user, $home_ou) = @_;
+    my $org_tree = OpenSRF::AppSession
+        ->create('open-ils.actor')
+        ->request('open-ils.actor.org_tree.ancestors.retrieve', $user->home_ou)
+        ->gather(1);
+    if ($org_tree && ref($org_tree) ne 'HASH') {
+        my %user_orgs;
+        do {
+            $user_orgs{$org_tree->id} = 1;
+            if ($org_tree->children) {
+                $org_tree = @{$org_tree->children}[0];
+            } else {
+                $org_tree = undef;
+            }
+        } while ($org_tree);
+
+        my @home_ous = split(/\s*,\s*/,$home_ou);
+        for my $cur_ou (@home_ous) {
+            $cur_ou = _get_org_id($cur_ou);
+            if ($user_orgs{$cur_ou}) {
+                return 1;
+            }
+        }
+    }
+    return 0;
+}
+
+# Verify permission
+sub _verify_permission {
+    my ($token, $user, $org_unit, $permissions) = @_;
+
+    my $failures = OpenSRF::AppSession
+        ->create('open-ils.actor')
+        ->request('open-ils.actor.user.perm.check', $token, $user->id, $org_unit, $permissions)
+        ->gather(1);
+
+    return !scalar(@$failures);
+}
+
+# Verify standing
+sub _verify_standing {
+    my ($user) = @_;
+
+    # If barred you are not in good standing
+    return 0 if $user->barred;
+    # If inactive you are also not in good standing
+    return 0 unless $user->active;
+
+    # Possible addition: Standing Penalty Checks?
+
+    return 1;
+}
+
+sub _get_ou_setting {
+    my ($setting, $org_unit) = @_;
+
+    my $value = OpenSRF::AppSession->create('open-ils.actor')
+        ->request('open-ils.actor.ou_setting.ancestor_default', $org_unit, $setting)
+        ->gather(1);
+
+    return $value;
+}
+
+1;
diff --git a/docs/RELEASE_NOTES_NEXT/Administration/apache_access_handler.txt b/docs/RELEASE_NOTES_NEXT/Administration/apache_access_handler.txt
new file mode 100644 (file)
index 0000000..3e35ed3
--- /dev/null
@@ -0,0 +1,139 @@
+Apache Access Handler: OpenILS::WWW::AccessHandler
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+This Perl module is intended for limiting patron access to configured locations
+in Apache. These locations could be folder trees, static files, non-Evergreen
+dynamic content, or other Apache features/modules. It is intended as a more
+patron-oriented and transparent version of the OpenILS::WWW::Proxy and
+OpenILS::WWW:Proxy::Authen modules.
+
+Instead of using Basic Authentication the AccessHandler module instead redirects
+to the OPAC for login. Once logged in additional checks can be performed, based
+on configured variables:
+
+ * Permission Checks (at Home OU or specified location)
+ * Home OU Checks (Org Unit or Descendant)
+ * "Good standing" Checks (Not Inactive or Barred)
+
+Use of the module is a simple addition to a Location block in Apache:
+
+[source,conf]
+<Location /path/to/be/protected>
+    PerlAccessHandler OpenILS::WWW::AccessHandler
+    # For each option you wish to set:
+    PerlSetVar OPTION "VALUE"
+</Location>
+
+The available options are:
+
+OILSAccessHandlerLoginURL::
+  Default: /eg/opac/login +
+  The page to redirect to when Login is needed
+OILSAccessHandlerLoginURLRedirectVar::
+  Default: redirect_to +
+  The variable the login page wants the "destination" URL stored in
+OILSAccessHandlerFailURL::
+  Default: <unset> +
+  URL to go to if Permission, Good Standing, or Home OU checks fail. If not set
+  a 403 error is generated instead. To customize the 403 you could use an
+  ErrorDocument statement.
+OILSAccessHandlerCheckOU::
+  Default: <User Home OU> +
+  Org Unit to check Permissions at and/or to load Referrer from. Can be a
+  shortname or an ID.
+OILSAccessHandlerPermission::
+  Default: <unset> +
+  Permission, or comma delimited set of permissions, the user must have to
+  access the protected area.
+OILSAccessHandlerGoodStanding::
+  Default: 0 +
+  If set to a true value the user must be both Active and not Barred.
+OILSAccessHandlerHomeOU::
+  Default: <unset> +
+  An Org Unit, or comma delimited set of Org Units, that the user's Home OU must
+  be equal to or a descendant of to access this resource. Can be set to
+  shortnames or IDs.
+OILSAccessHandlerReferrerSetting::
+  Default: <unset> +
+  Library Setting to pull a forced referrer string out of, if set.
+
+As the AccessHandler module does not actually serve the content it is
+protecting, but instead merely hands control back to Apache when it is done
+authenticating, you can protect almost anything else you can serve with Apache.
+
+Use Cases
++++++++++
+The general use of this module is "protect access to something else" - what that
+something else is will vary. Some possibilities:
+
+ * Apache features
+ ** Automatic Directory Indexes
+ ** Proxies (see below)
+ *** Electronic Databases
+ *** Software on other servers/ports
+ * Non-Evergreen software
+ ** Timekeeping software for staff
+ ** Specialized patron request packages
+ * Static files and folders
+ ** Semi-public Patron resources
+ ** Staff-only downloads
+
+Proxying Websites
++++++++++++++++++
+One potentially interesting use of the AccessHandler module is to protect an
+Apache Proxy configuration. For example, after installing and enabling
+mod_proxy, mod_proxy_http, and mod_proxy_html you could proxy websites like so:
+
+[source,conf]
+----
+<Location /proxy/>
+    # Base "Rewrite URLs" configuration
+    ProxyHTMLLinks  a       href
+    ProxyHTMLLinks  area        href
+    ProxyHTMLLinks  link        href
+    ProxyHTMLLinks  img     src longdesc usemap
+    ProxyHTMLLinks  object      classid codebase data usemap
+    ProxyHTMLLinks  q       cite
+    ProxyHTMLLinks  blockquote  cite
+    ProxyHTMLLinks  ins     cite
+    ProxyHTMLLinks  del     cite
+    ProxyHTMLLinks  form        action
+    ProxyHTMLLinks  input       src usemap
+    ProxyHTMLLinks  head        profile
+    ProxyHTMLLinks  base        href
+    ProxyHTMLLinks  script      src for
+
+    # To support scripting events (with ProxyHTMLExtended On)
+    ProxyHTMLEvents onclick ondblclick onmousedown onmouseup \
+            onmouseover onmousemove onmouseout onkeypress \
+            onkeydown onkeyup onfocus onblur onload \
+            onunload onsubmit onreset onselect onchange
+
+    # Limit all Proxy connections to authenticated sessions by default
+    PerlAccessHandler OpenILS::WWW::AccessHandler
+
+    # Strip out Evergreen cookies before sending to remote server
+    RequestHeader edit Cookie "^(.*?)ses=.*?(?:$|;)(.*)$" $1$2
+    RequestHeader edit Cookie "^(.*?)eg_loggedin=.*?(?:$|;)(.*)$" $1$2
+</Location>
+
+<Location /proxy/example/>
+    # Proxy example.net
+    ProxyPass http://www.example.net/
+    ProxyPassReverse http://www.example.net/
+    ProxyPassReverseCookieDomain example.net example.com
+    ProxyPassReverseCookiePath / /proxy/example/
+
+    ProxyHTMLEnable On
+    ProxyHTMLURLMap http://www.example.net/ /proxy/example/
+    ProxyHTMLURLMap / /proxy/mail/
+    ProxyHTMLCharsetOut *
+
+    # Limit to BR1 and BR3 users
+    PerlSetVar OILSAccessHandlerHomeOU "BR1,BR3"
+</Location>
+----
+
+As mentioned above, this can be used for multiple reasons. In addition to
+websites such as online databases for patron use you may wish to proxy software
+for staff or patron use to make it appear on your catalog domain, or perhaps to
+keep from needing to open extra ports in a firewall.
diff --git a/docs/TechRef/Apache/apache_access_handler.txt b/docs/TechRef/Apache/apache_access_handler.txt
new file mode 100644 (file)
index 0000000..08bf7b6
--- /dev/null
@@ -0,0 +1,139 @@
+Apache Access Handler: OpenILS::WWW::AccessHandler
+--------------------------------------------------
+This Perl module is intended for limiting patron access to configured locations
+in Apache. These locations could be folder trees, static files, non-Evergreen
+dynamic content, or other Apache features/modules. It is intended as a more
+patron-oriented and transparent version of the OpenILS::WWW::Proxy and
+OpenILS::WWW:Proxy::Authen modules.
+
+Instead of using Basic Authentication the AccessHandler module instead redirects
+to the OPAC for login. Once logged in additional checks can be performed, based
+on configured variables:
+
+ * Permission Checks (at Home OU or specified location)
+ * Home OU Checks (Org Unit or Descendant)
+ * "Good standing" Checks (Not Inactive or Barred)
+
+Use of the module is a simple addition to a Location block in Apache:
+
+[source,conf]
+<Location /path/to/be/protected>
+    PerlAccessHandler OpenILS::WWW::AccessHandler
+    # For each option you wish to set:
+    PerlSetVar OPTION "VALUE"
+</Location>
+
+The available options are:
+
+OILSAccessHandlerLoginURL::
+  Default: /eg/opac/login +
+  The page to redirect to when Login is needed
+OILSAccessHandlerLoginURLRedirectVar::
+  Default: redirect_to +
+  The variable the login page wants the "destination" URL stored in
+OILSAccessHandlerFailURL::
+  Default: <unset> +
+  URL to go to if Permission, Good Standing, or Home OU checks fail. If not set
+  a 403 error is generated instead. To customize the 403 you could use an
+  ErrorDocument statement.
+OILSAccessHandlerCheckOU::
+  Default: <User Home OU> +
+  Org Unit to check Permissions at and/or to load Referrer from. Can be a
+  shortname or an ID.
+OILSAccessHandlerPermission::
+  Default: <unset> +
+  Permission, or comma delimited set of permissions, the user must have to
+  access the protected area.
+OILSAccessHandlerGoodStanding::
+  Default: 0 +
+  If set to a true value the user must be both Active and not Barred.
+OILSAccessHandlerHomeOU::
+  Default: <unset> +
+  An Org Unit, or comma delimited set of Org Units, that the user's Home OU must
+  be equal to or a descendant of to access this resource. Can be set to
+  shortnames or IDs.
+OILSAccessHandlerReferrerSetting::
+  Default: <unset> +
+  Library Setting to pull a forced referrer string out of, if set.
+
+As the AccessHandler module does not actually serve the content it is
+protecting, but instead merely hands control back to Apache when it is done
+authenticating, you can protect almost anything else you can serve with Apache.
+
+Use Cases
+~~~~~~~~~
+The general use of this module is "protect access to something else" - what that
+something else is will vary. Some possibilities:
+
+ * Apache features
+ ** Automatic Directory Indexes
+ ** Proxies (see below)
+ *** Electronic Databases
+ *** Software on other servers/ports
+ * Non-Evergreen software
+ ** Timekeeping software for staff
+ ** Specialized patron request packages
+ * Static files and folders
+ ** Semi-public Patron resources
+ ** Staff-only downloads
+
+Proxying Websites
+~~~~~~~~~~~~~~~~~
+One potentially interesting use of the AccessHandler module is to protect an
+Apache Proxy configuration. For example, after installing and enabling
+mod_proxy, mod_proxy_http, and mod_proxy_html you could proxy websites like so:
+
+[source,conf]
+----
+<Location /proxy/>
+    # Base "Rewrite URLs" configuration
+    ProxyHTMLLinks  a       href
+    ProxyHTMLLinks  area        href
+    ProxyHTMLLinks  link        href
+    ProxyHTMLLinks  img     src longdesc usemap
+    ProxyHTMLLinks  object      classid codebase data usemap
+    ProxyHTMLLinks  q       cite
+    ProxyHTMLLinks  blockquote  cite
+    ProxyHTMLLinks  ins     cite
+    ProxyHTMLLinks  del     cite
+    ProxyHTMLLinks  form        action
+    ProxyHTMLLinks  input       src usemap
+    ProxyHTMLLinks  head        profile
+    ProxyHTMLLinks  base        href
+    ProxyHTMLLinks  script      src for
+
+    # To support scripting events (with ProxyHTMLExtended On)
+    ProxyHTMLEvents onclick ondblclick onmousedown onmouseup \
+            onmouseover onmousemove onmouseout onkeypress \
+            onkeydown onkeyup onfocus onblur onload \
+            onunload onsubmit onreset onselect onchange
+
+    # Limit all Proxy connections to authenticated sessions by default
+    PerlAccessHandler OpenILS::WWW::AccessHandler
+
+    # Strip out Evergreen cookies before sending to remote server
+    RequestHeader edit Cookie "^(.*?)ses=.*?(?:$|;)(.*)$" $1$2
+    RequestHeader edit Cookie "^(.*?)eg_loggedin=.*?(?:$|;)(.*)$" $1$2
+</Location>
+
+<Location /proxy/example/>
+    # Proxy example.net
+    ProxyPass http://www.example.net/
+    ProxyPassReverse http://www.example.net/
+    ProxyPassReverseCookieDomain example.net example.com
+    ProxyPassReverseCookiePath / /proxy/example/
+
+    ProxyHTMLEnable On
+    ProxyHTMLURLMap http://www.example.net/ /proxy/example/
+    ProxyHTMLURLMap / /proxy/mail/
+    ProxyHTMLCharsetOut *
+
+    # Limit to BR1 and BR3 users
+    PerlSetVar OILSAccessHandlerHomeOU "BR1,BR3"
+</Location>
+----
+
+As mentioned above, this can be used for multiple reasons. In addition to
+websites such as online databases for patron use you may wish to proxy software
+for staff or patron use to make it appear on your catalog domain, or perhaps to
+keep from needing to open extra ports in a firewall.