From c25e923c743a967bd6977457de7b6186b06a0f69 Mon Sep 17 00:00:00 2001 From: Julian Clementson <51331324+oajulianclementson@users.noreply.github.com> Date: Tue, 1 Oct 2019 16:17:53 +0100 Subject: [PATCH] LP#1842297: Implements patron sign-on to the OpenAthens service. Allows global or local administrators to configure a connection to the OpenAthens cloud-based single sign-on service. Allows patrons to connect seamlessly to third party resources that use OpenAthens authentication. For more detailed feature description visit https://wiki.evergreen-ils.org/doku.php?id=dev%3Aproposal%3Aopenathens_integration Signed-off-by: Julian Clementson --- Open-ILS/examples/fm_IDL.xml | 71 ++++ .../admin/local/admin-local-splash.component.html | 2 + .../app/staff/admin/local/admin-local.module.ts | 2 + .../admin/local/openathens-identity.component.html | 61 ++++ .../admin/local/openathens-identity.component.ts | 44 +++ .../src/app/staff/admin/local/routing.module.ts | 9 +- Open-ILS/src/extras/install/Makefile.debian-buster | 2 + Open-ILS/src/extras/install/Makefile.debian-jessie | 2 + .../src/extras/install/Makefile.debian-stretch | 2 + Open-ILS/src/extras/install/Makefile.fedora | 2 + Open-ILS/src/extras/install/Makefile.ubuntu-bionic | 2 + Open-ILS/src/extras/install/Makefile.ubuntu-xenial | 2 + .../src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm | 84 +++-- .../lib/OpenILS/WWW/EGCatLoader/OpenAthens.pm | 365 +++++++++++++++++++++ .../Pg/upgrade/XXXX.schema.openathens_identity.sql | 54 +++ .../Administration/OpenAthens_SignOn.adoc | 150 +++++++++ .../installation/pages/system_requirements.adoc | 1 - 17 files changed, 826 insertions(+), 29 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/local/openathens-identity.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/local/openathens-identity.component.ts create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/OpenAthens.pm create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.schema.openathens_identity.sql create mode 100644 docs/RELEASE_NOTES_NEXT/Administration/OpenAthens_SignOn.adoc diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index f549c1b775..4ce7a7b4c4 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -13530,6 +13530,77 @@ SELECT usr, + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html index 7fc28dc676..aa7a0437f6 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html @@ -52,6 +52,8 @@ routerLink="/staff/admin/local/config/non_cataloged_type"> + {{idlClassDef.label}} Update Succeeded + + +Update of {{idlClassDef.label}} failed + + +Delete of {{idlClassDef.label}} failed or was not allowed + + +{{idlClassDef.label}} Successfully Deleted + + +{{idlClassDef.label}} Succeessfully Created + + +Failed to create new {{idlClassDef.label}} + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/openathens-identity.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/openathens-identity.component.ts new file mode 100644 index 0000000000..8ce84c092b --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/openathens-identity.component.ts @@ -0,0 +1,44 @@ +import {Component, OnInit} from '@angular/core'; +import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.component'; + +@Component({ + templateUrl: './openathens-identity.component.html' +}) +export class OpenAthensIdentityComponent extends AdminPageComponent implements OnInit { + + idlClass = 'coai'; + classLabel: string; + + ngOnInit() { + super.ngOnInit(); + + this.classLabel = this.idlClassDef.label; + this.includeOrgDescendants = true; + } + + createNew = () => { + this.editDialog.recordId = null; + this.editDialog.record = null; + + const rec = this.idl.create('coai'); + rec.active(true); + rec.auto_signon_enabled(true); + rec.unique_identifier(1); + rec.display_name(1); + this.editDialog.record = rec; + + this.editDialog.open({size: this.dialogSize}).subscribe( + ok => { + this.createString.current() + .then(str => this.toast.success(str)); + this.grid.reload(); + }, + rejection => { + if (!rejection.dismissed) { + this.createErrString.current() + .then(str => this.toast.danger(str)); + } + } + ); + }; +} \ No newline at end of file diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts index 8ce6dce3cc..08ae67a972 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts @@ -4,6 +4,7 @@ import {AdminLocalSplashComponent} from './admin-local-splash.component'; import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.component'; import {AddressAlertComponent} from './address-alert.component'; import {AdminCarouselComponent} from './admin-carousel.component'; +import {OpenAthensIdentityComponent} from './openathens-identity.component'; import {StandingPenaltyComponent} from './standing-penalty.component'; import {CourseTermMapComponent} from './course-reserves/course-term-map.component'; @@ -21,12 +22,8 @@ const routes: Routes = [{ path: 'container/carousel', component: AdminCarouselComponent }, { - path: 'asset/course_list', - loadChildren: () => - import('./course-reserves/course-reserves.module').then(m => m.CourseReservesModule) -}, { - path: 'asset/course_module_term_course_map', - component: CourseTermMapComponent + path: 'config/openathens_identity', + component: OpenAthensIdentityComponent }, { path: 'config/standing_penalty', component: StandingPenaltyComponent diff --git a/Open-ILS/src/extras/install/Makefile.debian-buster b/Open-ILS/src/extras/install/Makefile.debian-buster index a561a0a92b..aee310fcb2 100644 --- a/Open-ILS/src/extras/install/Makefile.debian-buster +++ b/Open-ILS/src/extras/install/Makefile.debian-buster @@ -41,10 +41,12 @@ export DEBS = \ libemail-mime-perl\ libexcel-writer-xlsx-perl\ libgd-graph3d-perl\ + libhttp-async-perl\ liblocale-maketext-lexicon-perl\ liblog-log4perl-perl\ libmarc-charset-perl \ libncurses5-dev\ + libnet-https-nb-perl\ libnet-ip-perl\ libnet-ldap-perl \ libnet-server-perl\ diff --git a/Open-ILS/src/extras/install/Makefile.debian-jessie b/Open-ILS/src/extras/install/Makefile.debian-jessie index 21c906b96d..6ffce767b4 100644 --- a/Open-ILS/src/extras/install/Makefile.debian-jessie +++ b/Open-ILS/src/extras/install/Makefile.debian-jessie @@ -41,10 +41,12 @@ export DEBS = \ libemail-mime-perl\ libexcel-writer-xlsx-perl\ libgd-graph3d-perl\ + libhttp-async-perl\ liblocale-maketext-lexicon-perl\ liblog-log4perl-perl\ libmarc-charset-perl \ libncurses5-dev\ + libnet-https-nb-perl\ libnet-ip-perl\ libnet-ldap-perl \ libnet-server-perl\ diff --git a/Open-ILS/src/extras/install/Makefile.debian-stretch b/Open-ILS/src/extras/install/Makefile.debian-stretch index e5a9ce596f..13d28d9de2 100644 --- a/Open-ILS/src/extras/install/Makefile.debian-stretch +++ b/Open-ILS/src/extras/install/Makefile.debian-stretch @@ -41,10 +41,12 @@ export DEBS = \ libemail-mime-perl\ libexcel-writer-xlsx-perl\ libgd-graph3d-perl\ + libhttp-async-perl\ liblocale-maketext-lexicon-perl\ liblog-log4perl-perl\ libmarc-charset-perl \ libncurses5-dev\ + libnet-https-nb-perl\ libnet-ip-perl\ libnet-ldap-perl \ libnet-server-perl\ diff --git a/Open-ILS/src/extras/install/Makefile.fedora b/Open-ILS/src/extras/install/Makefile.fedora index 43ba4844b1..6143967c19 100644 --- a/Open-ILS/src/extras/install/Makefile.fedora +++ b/Open-ILS/src/extras/install/Makefile.fedora @@ -46,12 +46,14 @@ FEDORA_RPMS = \ perl-Email-Simple \ perl-Email-MIME \ perl-GDGraph3d \ + perl-HTTP-Async \ perl-JSON-XS \ perl-LDAP \ perl-Locale-Codes \ perl-Locale-Maketext-Lexicon \ perl-MARC-Charset \ perl-Module-Pluggable \ + perl-Net-HTTPS-NB \ perl-Net-IP \ perl-Net-SSH2 \ perl-OLE-Storage_Lite \ diff --git a/Open-ILS/src/extras/install/Makefile.ubuntu-bionic b/Open-ILS/src/extras/install/Makefile.ubuntu-bionic index 519d063381..5283751c64 100644 --- a/Open-ILS/src/extras/install/Makefile.ubuntu-bionic +++ b/Open-ILS/src/extras/install/Makefile.ubuntu-bionic @@ -40,9 +40,11 @@ export DEBS = \ libemail-mime-perl\ libexcel-writer-xlsx-perl\ libgd-graph3d-perl\ + libhttp-async-perl\ liblocale-maketext-lexicon-perl\ liblog-log4perl-perl\ libncurses5-dev\ + libnet-https-nb-perl\ libnet-ip-perl\ libnet-ldap-perl \ libnet-server-perl\ diff --git a/Open-ILS/src/extras/install/Makefile.ubuntu-xenial b/Open-ILS/src/extras/install/Makefile.ubuntu-xenial index 9f67c14d19..02ef459457 100644 --- a/Open-ILS/src/extras/install/Makefile.ubuntu-xenial +++ b/Open-ILS/src/extras/install/Makefile.ubuntu-xenial @@ -41,10 +41,12 @@ export DEBS = \ libemail-mime-perl\ libexcel-writer-xlsx-perl\ libgd-graph3d-perl\ + libhttp-async-perl\ liblocale-maketext-lexicon-perl\ liblog-log4perl-perl\ libmarc-charset-perl \ libncurses5-dev\ + libnet-https-nb-perl\ libnet-ip-perl\ libnet-ldap-perl \ libnet-server-perl\ diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm index 4fbe6a53f6..a50652fd11 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm @@ -28,6 +28,7 @@ use OpenILS::WWW::EGCatLoader::Course; use OpenILS::WWW::EGCatLoader::Container; use OpenILS::WWW::EGCatLoader::SMS; use OpenILS::WWW::EGCatLoader::Register; +use OpenILS::WWW::EGCatLoader::OpenAthens; my $U = 'OpenILS::Application::AppUtils'; @@ -176,6 +177,7 @@ sub load { return $self->load_password_reset if $path =~ m|opac/password_reset|; return $self->load_logout if $path =~ m|opac/logout|; return $self->load_patron_reg if $path =~ m|opac/register|; + return $self->load_openathens_logout if $path =~ m|opac/sso/openathens/logout$|; $self->load_simple("myopac") if $path =~ m:opac/myopac:; # A default page for myopac parts @@ -274,6 +276,8 @@ sub load { return $self->load_myopac_prefs_my_lists if $path =~ m|opac/myopac/prefs_my_lists|; return $self->load_myopac_prefs if $path =~ m|opac/myopac/prefs|; return $self->load_myopac_reservations if $path =~ m|opac/myopac/reservations|; + return $self->load_sms_cn if $path =~ m|opac/sms_cn|; + return $self->load_openathens_sso if $path =~ m|opac/sso/openathens$|; return $self->load_galileo if $path =~ m|opac/galileo|; @@ -639,10 +643,14 @@ sub load_login { ); } - return $self->generic_redirect( - $cgi->param('redirect_to') || $acct, - $cookie_list - ); + my $redirect_to = $cgi->param('redirect_to') || $acct; + + return + $self->_perform_any_sso_required($response, $redirect_to, $cookie_list) + || $self->generic_redirect( + $redirect_to, + $cookie_list + ); } # ----------------------------------------------------------------------------- @@ -650,7 +658,8 @@ sub load_login { # ----------------------------------------------------------------------------- sub load_logout { my $self = shift; - my $redirect_to = shift || $self->cgi->param('redirect_to'); + my $redirect_to = shift || $self->cgi->param('redirect_to') + || $self->ctx->{home_page}; # If the user was adding anyting to an anonymous cache # while logged in, go ahead and clear it out. @@ -664,23 +673,54 @@ sub load_logout { ); } catch Error with {}; - return $self->generic_redirect( - $redirect_to || $self->ctx->{home_page}, - [ - # clear value of and expire both of these login-related cookies - $self->cgi->cookie( - -name => COOKIE_SES, - -path => '/', - -value => '', - -expires => '-1h' - ), - $self->cgi->cookie( - -name => COOKIE_LOGGEDIN, - -path => '/', - -value => '', - -expires => '-1h' - ) - ] + # clear value of and expire both of these login-related cookies + my $cookie_list = [ + $self->cgi->cookie( + -name => COOKIE_SES, + -path => '/', + -value => '', + -expires => '-1h' + ), + $self->cgi->cookie( + -name => COOKIE_LOGGEDIN, + -path => '/', + -value => '', + -expires => '-1h' + ) + ]; + + return + $self->_perform_any_sso_signout_required($redirect_to, $cookie_list) + || $self->generic_redirect( + $redirect_to, + $cookie_list + ); +} + +# ----------------------------------------------------------------------------- +# Signs the user in to any third party services that their org unit is +# configured for. +# ----------------------------------------------------------------------------- +sub _perform_any_sso_required { + my ($self, $auth_response, $redirect_to, $cookie_list) = @_; + + return $self->perform_openathens_sso_if_required( + $auth_response, + $redirect_to, + $cookie_list + ); +} + +# ----------------------------------------------------------------------------- +# Signs the user out of any third party services that their org unit is +# configured for. +# ----------------------------------------------------------------------------- +sub _perform_any_sso_signout_required { + my ($self, $redirect_to, $cookie_list) = @_; + + return $self->perform_openathens_signout_if_required( + $redirect_to, + $cookie_list ); } diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/OpenAthens.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/OpenAthens.pm new file mode 100644 index 0000000000..8e338e7c8f --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/OpenAthens.pm @@ -0,0 +1,365 @@ +package OpenILS::WWW::EGCatLoader; + +use strict; use warnings; +use Apache2::Const -compile => qw(HTTP_BAD_REQUEST); +use HTTP::Async; +use HTTP::Request; +use XML::Simple; + +my $U = 'OpenILS::Application::AppUtils'; + +use constant OA_API_AUTH_TYPE => 'OAApiKey'; +use constant OA_API_WAIT_SECONDS => 2; +use constant OA_ATTR_PREFIX => 'prefix'; +use constant OA_ATTR_FIRST_GIVEN_NAME => 'first_given_name'; +use constant OA_ATTR_SECOND_GIVEN_NAME => 'second_given_name'; +use constant OA_ATTR_FAMILY_NAME => 'family_name'; +use constant OA_ATTR_SUFFIX => 'suffix'; +use constant OA_ATTR_EMAIL => 'email'; +use constant OA_ATTR_HOME_OU => 'home_ou'; +use constant OA_SIGNOUT_URL => 'https://login.openathens.net/signout'; +use constant OA_SESSION_REQUEST_TYPE => + 'application/vnd.eduserv.iam.auth.localAccountSessionRequest+json'; + +my @oa_config_fields = qw/active api_key connection_id connection_uri + auto_signon_enabled auto_signout_enabled release_prefix + release_first_given_name release_second_given_name release_family_name + release_suffix release_email release_home_ou/; + + +# ----------------------------------------------------------------------------- +# If sign in to OpenAthens is enabled, redirects to the local OpenAthens +# sign-in handler, including the original redirect as a parameter. +# ----------------------------------------------------------------------------- +sub perform_openathens_sso_if_required { + my ($self, $auth_response, $redirect_to, $cookie_list) = @_; + my $ctx = $self->ctx; + my $e = $self->editor; + + # Don't generate a new redirect to the OpenAthens handler if that's where + # we came from. + if (index( + $redirect_to, + $ctx->{opac_root} . '/sso/openathens' + ) == 0) { + return; + } + + # Use the auth_token to establish the context user and load the relevant + # OpenAthens config. This is needed because the OpenAthens behaviour + # depends on the org unit, but the user context has not yet been loaded. + if ($e->authtoken($auth_response->{payload}->{authtoken}) + && $e->checkauth + ) { + $ctx->{user} = $e->requestor; + } + + return unless $ctx->{user}; + + my $openathens_config = + $self->_get_openathens_config_for_org($ctx->{user}->home_ou); + + if ($openathens_config + && $U->is_true($openathens_config->{active}) + && $U->is_true($openathens_config->{auto_signon_enabled}) + ) { + my $redirect = $ctx->{opac_root} . '/sso/openathens?redirect_to=' + . uri_escape_utf8($redirect_to); + + if ($redirect) { + return $self->generic_redirect($redirect, $cookie_list); + } + } +} + +# ----------------------------------------------------------------------------- +# If sign out of OpenAthens is enabled, redirects to the local OpenAthens +# sign-out handler, including the original redirect as a parameter. +# ----------------------------------------------------------------------------- +sub perform_openathens_signout_if_required { + my ($self, $redirect_to, $cookie_list) = @_; + my $ctx = $self->ctx; + + return unless $ctx->{user}; + + my $openathens_config = + $self->_get_openathens_config_for_org($ctx->{user}->home_ou); + + if ($openathens_config + && $U->is_true($openathens_config->{active}) + && $U->is_true($openathens_config->{auto_signout_enabled}) + ) { + my $redirect = $ctx->{opac_root} + . '/sso/openathens/logout?redirect_to=' + . uri_escape_utf8($redirect_to); + + if ($redirect) { + return $self->generic_redirect($redirect, $cookie_list); + } + } + + return undef; +} + +# ----------------------------------------------------------------------------- +# Handler for /eg/opac/sso/openathens. Establishes single-sign-on session on +# OpenAthens, if configured. Implements +# http://docs.openathens.net/display/public/MD/Implementing+the+API+connector+in+your+code +# +# There are two flows supported: +# +# 1. The user just logged in locally, and we want to sign them on to +# OpenAthens as well (if this feature is enabled for the user's org unit). +# +# In this case 'redirect_to' will be set and will be the local URL that +# initiated login, e.g. /eg/opac/myopac/main. We will send the user to +# OpenAthens with a token that will establish their sign-on session, and with a +# redirect parameter instructing OpenAthens to send them back to the original +# local URL afterwards. +# +# 2. The user tried to access an OpenAthens-protected resource and chose to +# sign on via their account with us. +# +# In this case, 'returnData' will be supplied by OpenAthens and is opaque to +# us. We will send the user back to OpenAthens with a token that will +# establish their sign-on session, together with the returnData. OpenAthens +# can then forward the user on to whichever resource they were originally +# requesting. +# ----------------------------------------------------------------------------- +sub load_openathens_sso { + my $self = shift; + my $cgi = $self->cgi; + my $ctx = $self->ctx; + + my $redirect_to = $cgi->param('redirect_to') || ''; + my $return_data = $cgi->param('returnData') || ''; + my $status = $cgi->param('status') || ''; + + # 'redirect_to' must be empty or a local URL + return Apache2::Const::HTTP_BAD_REQUEST unless $redirect_to =~ m:^($|/):; + + # 'redirect_to' and 'returnData' are mutually exclusive + return Apache2::Const::HTTP_BAD_REQUEST if ($redirect_to && $return_data); + + my $openathens_config = + $self->_get_openathens_config_for_org($ctx->{user}->home_ou); + + if (!$openathens_config + || !$U->is_true($openathens_config->{active}) + ) { + return $self->generic_redirect(); + } + + if ($redirect_to) { + # OpenAthens sign-on has been initiated by local login. + + if ($status) { + # User has already been redirected to OpenAthens and back again. + # Status will indicate success/failure, but ignore: we don't want + # to show the user any errors because it's a non-interactive flow. + return $self->generic_redirect($redirect_to); + } else { + # Request has not yet gone to OpenAthens; initiate now by making + # API call then redirecting. + my $return_url = $ctx->{proto} . '://' . $ctx->{hostname} + . $ctx->{opac_root} . '/sso/openathens?redirect_to=' + . uri_escape_utf8($redirect_to); + + my $oa_redirect = $self->_get_openathens_session_initiator_url( + $return_url + ); + + return $self->generic_redirect($oa_redirect); + } + } elsif ($return_data) { + # OpenAthens has initiaited sign-on; make API call using supplied data, + # then redirect back. + my $oa_redirect = $self->_get_openathens_session_initiator_url( + undef, + $return_data + ); + + return $self->generic_redirect($oa_redirect); + } else { + # Page called with no relevant parameters; go to home. + return $self->generic_redirect(); + } +} + +# ----------------------------------------------------------------------------- +# Hanlder for /eg/opac/sso/openathens/logout. Ends OpenAthens session. +# Optionally called after local logout. +# ----------------------------------------------------------------------------- +sub load_openathens_logout { + my $self = shift; + my $ctx = $self->ctx; + + $self->generic_redirect(OA_SIGNOUT_URL); +} + +# ----------------------------------------------------------------------------- +# Retrieves the relevant OpenAthens config for the given org unit. If not set, +# searches up the org hierarchy to find one, or returns undef. If an org unit +# has multiple configs, only the first is used. +# ----------------------------------------------------------------------------- +sub _get_openathens_config_for_org { + my ($self, $org_id) = @_; + my $e = new_editor(); + + my @org_ancestors = reverse @{$U->get_org_ancestors($org_id, 1)}; + my $parent_org = $org_ancestors[1]; + + my $configs = $e->json_query({ + select => { + coai => \@oa_config_fields, + coauf => [ + { column => 'name', alias => 'id_field' } + ], + coanf => [ + { column => 'name', alias => 'dn_field' } + ] + }, + from => { + coai => { + coauf => {}, + coanf => {} + } + }, + where => { + '+coai' => { org_unit => $org_id } + }, + order_by => { 'coai' => ['id'] } + }); + + if (@$configs) { + return $configs->[0]; + } elsif ($parent_org) { + return $self->_get_openathens_config_for_org($parent_org); + } else { + return undef; + } +} + +# ----------------------------------------------------------------------------- +# Makes POST to OpenAthens local-auth API. Returns URL to which the user should +# be redirected to establish OpenAthens SSO session. +# ----------------------------------------------------------------------------- +sub _get_openathens_session_initiator_url { + my $self = shift; + my ($return_url, $return_data) = @_; + my $ctx = $self->ctx; + my $user = $ctx->{user}; + + my $openathens_config = + $self->_get_openathens_config_for_org($user->home_ou); + + # must have either returnUrl or returnData but not both + return undef if $return_url && $return_data; + return undef if !$return_url && !$return_data; + + # Select the chosen unique identifier attribute + my $unique_user_identifier; + if ($openathens_config->{id_field} eq 'id') { + $unique_user_identifier = $user->id; + } elsif ($openathens_config->{id_field} eq 'usrname') { + $unique_user_identifier = $user->usrname; + } + + # Select the chosen display name attribute + my $display_name; + if ($openathens_config->{dn_field} eq 'id') { + $display_name = $user->id; + } elsif ($openathens_config->{dn_field} eq 'usrname') { + $display_name = $user->usrname; + } elsif ($openathens_config->{dn_field} eq 'fullname') { + $display_name = + ($user->pref_first_given_name || $user->first_given_name) + . ' ' . ($user->pref_family_name || $user->family_name); + } + + # Build object to POST to OpenAthens + my $request_obj = { + 'connectionID' => $openathens_config->{connection_id}, + 'uniqueUserIdentifier' => $unique_user_identifier, + 'displayName' => $display_name, + 'attributes' => {} + }; + + # Optional attributes + if ($U->is_true($openathens_config->{release_prefix})) { + $request_obj->{attributes}->{OA_ATTR_PREFIX} = $user->prefix; + } + + if ($U->is_true($openathens_config->{release_first_given_name})) { + $request_obj->{attributes}->{OA_ATTR_FIRST_GIVEN_NAME} = + $user->pref_first_given_name || $user->first_given_name; + } + + if ($U->is_true($openathens_config->{release_second_given_name})) { + $request_obj->{attributes}->{OA_ATTR_SECOND_GIVEN_NAME} = + $user->pref_second_given_name || $user->second_given_name; + } + + if ($U->is_true($openathens_config->{release_family_name})) { + $request_obj->{attributes}->{OA_ATTR_FAMILY_NAME} = + $user->pref_family_name || $user->family_name; + } + + if ($U->is_true($openathens_config->{release_suffix})) { + $request_obj->{attributes}->{OA_ATTR_SUFFIX} = $user->suffix; + } + + if ($U->is_true($openathens_config->{release_email})) { + $request_obj->{attributes}->{OA_ATTR_EMAIL} = $user->email; + } + + my $ou_id = $user->home_ou; + if ($ou_id && $U->is_true($openathens_config->{release_home_ou})) { + my $ou = $ctx->{get_aou}->($ou_id); + if ($ou) { + $request_obj->{attributes}->{OA_ATTR_HOME_OU} = $ou->shortname; + } + } + + if ($return_url) { + $request_obj->{returnUrl} = $return_url; + } elsif ($return_data) { + $request_obj->{returnData} = $return_data; + } + + # Execute OpenAthens API request + my $auth_header = OA_API_AUTH_TYPE . ' ' . $openathens_config->{api_key}; + my $body = JSON::XS->new->utf8->encode($request_obj); + my $async = HTTP::Async->new; + $async->add(HTTP::Request->new( + 'POST', + $openathens_config->{connection_uri}, + [ + 'Authorization' => $auth_header, + 'Content-type' => OA_SESSION_REQUEST_TYPE + ], + $body + )); + + my $response = $async->wait_for_next_response(OA_API_WAIT_SECONDS); + if ($response->is_error) { + $self->apache->log->error('Error POSTing to OpenAthens API: ' + . $response->code . ' ' . $response->message); + + return undef; + } + + # JSON response should contain the sessionInitiatorUrl + my $response_obj = JSON::XS->new->utf8->decode($response->content); + my $session_initiator_url = $response_obj->{sessionInitiatorUrl}; + if (!$session_initiator_url) { + $self->apache->log->error( + 'No sessionInitiatorUrl included in response from OpenAthens'); + + return undef; + } + + return $session_initiator_url; +} + +1; diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.openathens_identity.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.openathens_identity.sql new file mode 100644 index 0000000000..e43010d478 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.openathens_identity.sql @@ -0,0 +1,54 @@ +BEGIN; + +SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version); + +CREATE TABLE config.openathens_uid_field ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL +); + +INSERT INTO config.openathens_uid_field + (id, name) +VALUES + (1,'id'), + (2,'usrname') +; + +SELECT SETVAL('config.openathens_uid_field_id_seq'::TEXT, 100); + +CREATE TABLE config.openathens_name_field ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL +); + +INSERT INTO config.openathens_name_field + (id, name) +VALUES + (1,'id'), + (2,'usrname'), + (3,'fullname') +; + +SELECT SETVAL('config.openathens_name_field_id_seq'::TEXT, 100); + +CREATE TABLE config.openathens_identity ( + id SERIAL PRIMARY KEY, + active BOOL NOT NULL DEFAULT true, + org_unit INT NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + api_key TEXT NOT NULL, + connection_id TEXT NOT NULL, + connection_uri TEXT NOT NULL, + auto_signon_enabled BOOL NOT NULL DEFAULT true, + auto_signout_enabled BOOL NOT NULL DEFAULT false, + unique_identifier INT NOT NULL REFERENCES config.openathens_uid_field (id) DEFAULT 1, + display_name INT NOT NULL REFERENCES config.openathens_name_field (id) DEFAULT 1, + release_prefix BOOL NOT NULL DEFAULT false, + release_first_given_name BOOL NOT NULL DEFAULT false, + release_second_given_name BOOL NOT NULL DEFAULT false, + release_family_name BOOL NOT NULL DEFAULT false, + release_suffix BOOL NOT NULL DEFAULT false, + release_email BOOL NOT NULL DEFAULT false, + release_home_ou BOOL NOT NULL DEFAULT false +); + +COMMIT; diff --git a/docs/RELEASE_NOTES_NEXT/Administration/OpenAthens_SignOn.adoc b/docs/RELEASE_NOTES_NEXT/Administration/OpenAthens_SignOn.adoc new file mode 100644 index 0000000000..1c526d3d50 --- /dev/null +++ b/docs/RELEASE_NOTES_NEXT/Administration/OpenAthens_SignOn.adoc @@ -0,0 +1,150 @@ +Configuring sign-on to OpenAthens +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +If your institution uses OpenAthens single sign-on, you can configure Evergreen +to link with OpenAthens. This will let patrons connect to OpenAthens resources +seamlessly once they have logged in to Evergreen. Patrons are automatically +assigned an OpenAthens identity dynamically based on their Evergreen login, +and do not need accounts created manually in OpenAthens. + +Registering your Evergreen installation with the OpenAthens service ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +Using your OpenAthens administrator account at https://admin.openathens.net/, +complete the following steps: + +. Register a local authentication connection for Evergreen: + .. Go to *Management* -> *Connections*. + .. Under *Local authentication* click *Add*. + .. In the wizard that appears, select *Evergreen* as the local authentication + system type (or *API* if Evergreen is not listed) and click *Configure*. + .. For *Display name*, enter the name of your Evergreen portal that your + patrons will be familiar with. They will need to be able to recognise and + select this name from a list of sign-in options on OpenAthens. + .. For *Callback URL* enter *https:///eg/opac/sso/openathens* where + is the public hostname of your Evergreen installation, and click + *Save*. (If you have installed Evergreen somewhere other than /eg, modify the + URL accordingly.) + .. On the details page that appears, take a copy of the *Connection ID* and + *Connection URI* that have been generated. You will need these when + configuring Evergreen. +. Generate an API key: + .. Go to *Management* -> *API keys* and click *Add*. + .. For *Name*, enter 'Evergreen' or whatever name you use for your Evergreen + portal internally, and click *Save*. + .. Take a copy of the 36-character key that has been generated. You will need + this when configuring Evergreen. + +Full OpenAthens documentation for local authentication API connections is +available at http://docs.openathens.net/display/public/MD/API+connector. + +Configuring Evergreen ++++++++++++++++++++++ +OpenAthens sign-on is configured in the staff client under *Local +Administration* -> *OpenAthens Sign-on*. To make a connection, select *New +Sign-on to OpenAthens*, and set the values as follows: + +* *Owner* - the organisation within your library hierarchy that owns the +connection to OpenAthens. If your whole consortium has signed up to OpenAthens +as a single customer, then you would select the top-level. If only one +regional library system or branch is the OpenAthens customer, select that. +Whichever organisation you select, the OpenAthens connection will take effect +for all libraries below it in your organisational hierarchy. A single +OpenAthens sign-on configuration normally equates to a single *domain* in the +OpenAthens service. If in doubt refer to your OpenAthens account manager or +implementation partner. +* *Active* - Enable this connection (enabled by default). +* *API key* - the 36-character OpenAthens *API key* that was generated in step + 2 above. +* *Connection ID* - the numerical *Connection ID* that was generated for the + OpenAthens local authentication connection in step 1 above. +* *Connection URI* - the *Connection URI* that was generated for the + OpenAthens local authentication connection in step 1 above. +* *Auto sign-on* - controls _when_ patrons are signed on to OpenAthens: + ** *enabled* (recommended) - As soon as a patron logs in to Evergreen, they + are signed in to OpenAthens. This happens via a quick redirect that the user + should not notice. + ** *disabled* - The patron is not signed in to OpenAthens to start with. When + they first access an OpenAthens-protected resource, they will need to search + for your institution at the OpenAthens log-in page and choose your Evergreen + portal as the sign-in method (they will see the name you entered as the + *Display name* in step 1 above). Evergreen will then prompt for log-in if + they have not already logged in. After that, they are signed in to OpenAthens + and OpenAthens redirects them to the resource. +* *Auto sign-out* - controls whether the patron is signed out of OpenAthens + when they log out of Evergreen. If *enabled* the patron will be sent to the + OpenAthens sign-out page when they log out of Evergreen. You can optionally + configure the OpenAthens service to send them back to your home page again + after this; the setting can be found at https://admin.openathens.net/ under + *Preferences* -> *Domain* -> *After sign out*. +* *Unique identifier field* - controls which attribute of patron accounts is + used as the unique identifier in OpenAthens. The supported values are 'id' + and 'usrname', but you should leave this set to the default value of 'id' + unless you have a reason to do otherwise. It is important that this attribute + does not change during the lifetime of a patron account, otherwise they would + lose any personalised settings they have saved on third party resources. It + is also important that you do not re-use old patron accounts for new users, + otherwise a new user could see personalised settings saved by an old user. +* *Display name field* - controls which attribute of patron accounts is + displayed in the OpenAthens portal at https://admin.openathens.net/. (This + is where you can see which accounts have been used, and what use patrons are + making of third party resources.) The supported values are 'id', 'usrname' + and 'fullname'. Whichever you choose, OpenAthens will only use it within + your portal view; it won't be released to third-party resources. +* *Release X* - one setting for each of the attributes that it is possible to + release to OpenAthens. Depending on your user privacy policy, you can + configure any of these attributes to be released to OpenAthens as part of + the sign-on process. None are enabled by default. OpenAthens in turn doesn't + store or release any of these attributes to third party resources, unless + you configure that separately in the OpenAthens portal. You have to + configure this in two stages. Firstly, mapping Evergreen attributes to + OpenAthens attributes, and secondly releasing OpenAthens attributes to third + party resources. See the OpenAthens documenation pages at + http://docs.openathens.net/display/public/MD/Attribute+mapping and + http://docs.openathens.net/display/public/MD/Attribute+release. You will need + to know the exact names of the attributes that are released. These are listed + in the following table: + +|=== +|Setting|Attribute released|Description + +|Release prefix +|prefix +|the patron's prefix, overriden by the preferred prefix if that is set + +|Release first name +|first_given_name +|the patron's first name, overriden by the preferred first name if that is set + +|Release middle name +|second_given_name +|the patron's middle name, overriden by the preferred middle name if that is set + +|Release surname +|family_name +|the patron's last name, overriden by the preferred last name if that is set + +|Release suffix +|suffix +|the patron's suffix, overriden by the preferred suffix if that is set + +|Release email +|email +|the patron's email address + +|Release home library +|home_ou +|the _shortcode_ of the patron's home library (e.g. 'BR1' in the Concerto +sample data set) +|=== + +Network access +++++++++++++++ +As part of the sign-on process, Evergreen makes a connection to the OpenAthens +service to transfer details of the user that is signing on. This data does not +go via the user's browser, to avoid revealing the private API key and to avoid +the risk of spoofing. You need to open up port 443 outbound in your firewall, +from your Evergreen server to login.openathens.net. + +Admin permissions ++++++++++++++++++ +To delegate OpenAthens configuration to other staff users, assign the +*ADMIN_OPENATHENS* permission. diff --git a/docs/modules/installation/pages/system_requirements.adoc b/docs/modules/installation/pages/system_requirements.adoc index 31cbd72e56..2a73ccadc5 100644 --- a/docs/modules/installation/pages/system_requirements.adoc +++ b/docs/modules/installation/pages/system_requirements.adoc @@ -10,7 +10,6 @@ The following are the base requirements setting Evergreen up on a test server: * Linux Operating System (community supports Debian, Ubuntu, or Fedora) * Ports 80 and 443 should be opened in your firewall for TCP connections to allow OPAC and staff client connections to the Evergreen server. -== Web Client Requirements == The current stable release of Firefox or Chrome is required to run the web client in a browser. -- 2.11.0