</permacrud>
</class>
+ <class id="coauf"
+ controller="open-ils.cstore open-ils.pcrud"
+ oils_obj:fieldmapper="config::openathens_uid_field"
+ oils_persist:tablename="config.openathens_uid_field"
+ reporter:label="OpenAthens unique identifiers">
+ <fields oils_persist:primary="id" oils_persist:sequence="config.openathens_uid_field_id_seq">
+ <field reporter:label="ID" name="id" reporter:selector="name" reporter:datatype="id" />
+ <field reporter:label="Name" name="name" reporter:datatype="text" oils_obj:required="true"/>
+ </fields>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve/>
+ </actions>
+ </permacrud>
+ </class>
+
+ <class id="coanf"
+ controller="open-ils.cstore open-ils.pcrud"
+ oils_obj:fieldmapper="config::openathens_name_field"
+ oils_persist:tablename="config.openathens_name_field"
+ reporter:label="OpenAthens name fields">
+ <fields oils_persist:primary="id" oils_persist:sequence="config.openathens_name_field_id_seq">
+ <field reporter:label="ID" name="id" reporter:selector="name" reporter:datatype="id" />
+ <field reporter:label="Name" name="name" reporter:datatype="text" oils_obj:required="true"/>
+ </fields>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve/>
+ </actions>
+ </permacrud>
+ </class>
+
+ <class id="coai"
+ controller="open-ils.cstore open-ils.pcrud"
+ oils_obj:fieldmapper="config::openathens_identity"
+ oils_persist:tablename="config.openathens_identity"
+ reporter:label="Sign-on to OpenAthens">
+ <fields oils_persist:primary="id" oils_persist:sequence="config.openathens_identity_id_seq">
+ <field reporter:label="ID" name="id" reporter:datatype="id" reporter:selector="name"/>
+ <field reporter:label="Owner" name="org_unit" reporter:datatype="org_unit" oils_obj:required="true"/>
+ <field reporter:label="Active" name="active" reporter:datatype="bool"/>
+ <field reporter:label="API key" name="api_key" reporter:datatype="text" oils_obj:required="true"/>
+ <field reporter:label="Connection ID" name="connection_id" reporter:datatype="text" oils_obj:required="true"/>
+ <field reporter:label="Connection URI" name="connection_uri" reporter:datatype="text" oils_obj:required="true"/>
+ <field reporter:label="Auto sign-on" name="auto_signon_enabled" reporter:datatype="bool"/>
+ <field reporter:label="Auto sign-out" name="auto_signout_enabled" reporter:datatype="bool"/>
+ <field reporter:label="Unique identifier field" name="unique_identifier" reporter:datatype="link" oils_obj:required="true"/>
+ <field reporter:label="Display name field" name="display_name" reporter:datatype="link" oils_obj:required="true"/>
+ <field reporter:label="Release prefix" name="release_prefix" reporter:datatype="bool"/>
+ <field reporter:label="Release first name" name="release_first_given_name" reporter:datatype="bool"/>
+ <field reporter:label="Release middle name" name="release_second_given_name" reporter:datatype="bool"/>
+ <field reporter:label="Release surname" name="release_family_name" reporter:datatype="bool"/>
+ <field reporter:label="Release suffix" name="release_suffix" reporter:datatype="bool"/>
+ <field reporter:label="Release email" name="release_email" reporter:datatype="bool"/>
+ <field reporter:label="Release home library" name="release_home_ou" reporter:datatype="bool"/>
+ </fields>
+ <links>
+ <link field="org_unit" reltype="has_a" key="id" map="" class="aou"/>
+ <link field="unique_identifier" reltype="has_a" key="id" map="" class="coauf"/>
+ <link field="display_name" reltype="has_a" key="id" map="" class="coanf"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="ADMIN_OPENATHENS" global_required="true"/>
+ <retrieve/>
+ <update permission="ADMIN_OPENATHENS" global_required="true"/>
+ <delete permission="ADMIN_OPENATHENS" global_required="true"/>
+ </actions>
+ </permacrud>
+ </class>
+
<!-- ********************************************************************************************************************* -->
</IDL>
routerLink="/staff/admin/local/config/non_cataloged_type"></eg-link-table-link>
<eg-link-table-link i18n-label label="Notifications / Action Triggers"
url="/eg/staff/admin/local/action_trigger/event_definition"></eg-link-table-link>
+ <eg-link-table-link i18n-label label="OpenAthens Sign-on"
+ routerLink="/staff/admin/local/config/openathens_identity"></eg-link-table-link>
<eg-link-table-link i18n-label label="Patrons with Negative Balances"
url="/eg/staff/admin/local/circ/neg_balance_users"></eg-link-table-link>
<eg-link-table-link i18n-label label="Permission Tree Display Entries"
import {AdminLocalSplashComponent} from './admin-local-splash.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';
@NgModule({
AdminLocalSplashComponent,
AddressAlertComponent,
AdminCarouselComponent,
+ OpenAthensIdentityComponent,
StandingPenaltyComponent
],
imports: [
--- /dev/null
+<ng-template #successStrTmpl i18n>{{idlClassDef.label}} Update Succeeded</ng-template>
+<eg-string #successString [template]="successStrTmpl"></eg-string>
+
+<ng-template #updateFailedStrTmpl i18n>Update of {{idlClassDef.label}} failed</ng-template>
+<eg-string #updateFailedString [template]="updateFailedStrTmpl"></eg-string>
+
+<ng-template #deleteFailedStrTmpl i18n>Delete of {{idlClassDef.label}} failed or was not allowed</ng-template>
+<eg-string #deleteFailedString [template]="deleteFailedStrTmpl"></eg-string>
+
+<ng-template #deleteSuccessStrTmpl i18n>{{idlClassDef.label}} Successfully Deleted</ng-template>
+<eg-string #deleteSuccessString [template]="deleteSuccessStrTmpl"></eg-string>
+
+<ng-template #createStrTmpl i18n>{{idlClassDef.label}} Succeessfully Created</ng-template>
+<eg-string #createString [template]="createStrTmpl"></eg-string>
+
+<ng-template #createErrStrTmpl i18n>Failed to create new {{idlClassDef.label}}</ng-template>
+<eg-string #createErrString [template]="createErrStrTmpl"></eg-string>
+
+<eg-title i18n-prefix prefix="{{classLabel}} Administration">
+</eg-title>
+<eg-staff-banner bannerText="{{classLabel}} Configuration" i18n-bannerText>
+</eg-staff-banner>
+
+<ng-container *ngIf="orgField">
+ <eg-org-family-select
+ [limitPerms]="viewPerms"
+ [selectedOrgId]="contextOrg.id()"
+ [(ngModel)]="searchOrgs"
+ (ngModelChange)="grid.reload()">
+ </eg-org-family-select>
+ <hr/>
+</ng-container>
+
+<eg-grid #grid idlClass="{{idlClass}}" [dataSource]="dataSource"
+ [sortable]="true" persistKey="{{persistKey}}" [showLinkSelectors]="true">
+ <eg-grid-toolbar-button [disabled]="!canCreate"
+ label="New {{idlClassDef.label}}" i18n-label [action]="createNew">
+ </eg-grid-toolbar-button>
+ <eg-grid-toolbar-button [disabled]="translatableFields.length == 0"
+ label="Apply Translations" i18n-label [action]="translate">
+ </eg-grid-toolbar-button>
+ <eg-grid-toolbar-action label="Edit Selected" i18n-label [action]="editSelected">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Delete Selected" i18n-label [action]="deleteSelected">
+ </eg-grid-toolbar-action>
+</eg-grid>
+
+<ng-template #orgTemplate
+ let-field="field" let-record="record">
+ <eg-multi-select idlClass="aou"
+ [startValue]="record['owning_lib_filter']()"
+ (onChange)="record['owning_lib_filter']($event)">
+ </eg-multi-select>
+</ng-template>
+
+<eg-fm-record-editor #editDialog
+ idlClass="{{idlClass}}"
+ [preloadLinkedValues]="true"
+ [fieldOptions]="{owning_lib_filter:{customTemplate:{template:orgTemplate}}}"
+ fieldOrder="id,org_unit,active,api_key,connection_id,connection_uri,auto_signon_enabled,auto_signout_enabled,unique_identifier,display_name,release_prefix,release_first_given_name,release_second_given_name,release_family_name,release_suffix,release_email,release_home_ou"
+></eg-fm-record-editor>
--- /dev/null
+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
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';
const routes: Routes = [{
path: 'container/carousel',
component: AdminCarouselComponent
}, {
+ path: 'config/openathens_identity',
+ component: OpenAthensIdentityComponent
+}, {
path: 'config/standing_penalty',
component: StandingPenaltyComponent
}, {
libemail-simple-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\
libemail-simple-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\
libemail-simple-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\
perl-Email-Send \
perl-Email-Simple \
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 \
libemail-simple-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\
libemail-simple-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\
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';
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
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 Apache2::Const::OK;
}
);
}
- 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
+ );
}
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
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.
);
} 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
);
}
--- /dev/null
+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;
--- /dev/null
+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;
--- /dev/null
+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://<HOSTNAME>/eg/opac/sso/openathens* where
+ <HOSTNAME> 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.
* 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.
+Optional Feature Requirements
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you will be configuring Evergreen to sign users on to OpenAthens:
+
+ * Port 443 should be opened _outbound_ in your firewall to allow the
+ Evergreen server to connect to https://login.openathens.net.
+
Web Client Requirements
~~~~~~~~~~~~~~~~~~~~~~~