From 855219cf896ff2565cb703309d40f371c0fd8ada Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Sun, 22 Feb 2026 20:19:52 +0000 Subject: [PATCH] Bug 39224: Identity providers Vue SPA and admin interface Replaces the legacy jQuery-based identity providers admin page with a modern Vue 3 SPA: - Vue router with views for list, show, add, and edit providers - Provider, domain, and mapping resource components - API client for identity provider endpoints - Rspack build configuration for the new module - Updated OPAC templates for hostname-based provider selection - Updated system preferences (moves provider prefs to admin section) - Apache configuration for Vue SPA routing - Simplified admin/identity_providers.pl (delegates to Vue SPA) Test plan: 1. Navigate to Administration > Identity providers 2. Verify the list view shows existing providers 3. Add a new identity provider with mappings 4. Edit an existing provider 5. Verify OPAC login shows appropriate provider buttons 6. Run: yarn build && restart_all Sponsored-by: ByWater Solutions --- C4/Auth.pm | 6 +- admin/identity_providers.pl | 320 ++----- debian/templates/apache-shared-intranet.conf | 1 + etc/koha-httpd.conf | 1 + .../prog/en/includes/admin-menu.inc | 1 - .../en/modules/admin/identity_providers.tt | 603 +----------- .../en/modules/admin/preferences/opac.pref | 6 - .../admin/preferences/staff_interface.pref | 6 - .../intranet-tmpl/prog/js/fetch/api-client.js | 3 + .../js/fetch/identity-providers-api-client.js | 171 ++++ .../IdentityProviders/DomainResource.vue | 261 ++++++ .../vue/components/IdentityProviders/Main.vue | 76 ++ .../IdentityProviders/MappingResource.vue | 215 +++++ .../IdentityProviders/ProviderResource.vue | 856 ++++++++++++++++++ .../js/vue/components/Islands/AdminMenu.vue | 13 - .../prog/js/vue/fetch/api-client.js | 2 + .../prog/js/vue/modules/identity-providers.ts | 76 ++ .../prog/js/vue/routes/identity-providers.js | 125 +++ .../prog/js/vue/stores/identity-providers.js | 15 + .../prog/js/vue/stores/navigation.js | 3 + .../bootstrap/en/includes/masthead.inc | 2 +- .../bootstrap/en/modules/opac-auth.tt | 168 ++-- opac/opac-user.pl | 7 +- rspack.config.js | 2 + 24 files changed, 1967 insertions(+), 972 deletions(-) create mode 100644 koha-tmpl/intranet-tmpl/prog/js/fetch/identity-providers-api-client.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/DomainResource.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/Main.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/MappingResource.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/ProviderResource.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/modules/identity-providers.ts create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/routes/identity-providers.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/stores/identity-providers.js diff --git a/C4/Auth.pm b/C4/Auth.pm index bae5ebe4150..78152373569 100644 --- a/C4/Auth.pm +++ b/C4/Auth.pm @@ -173,7 +173,7 @@ sub get_template_and_user { my $cookie_mgr = Koha::CookieManager->new; # Get shibboleth login attribute - my $shib = C4::Context->preference('ShibbolethAuthentication') && shib_ok(); + my $shib = shib_ok(); my $shib_login = $shib ? get_login_shib() : undef; C4::Context->interface( $in->{type} ); @@ -811,7 +811,7 @@ sub checkauth { my $query = shift; # Get shibboleth login attribute - my $shib = C4::Context->preference('ShibbolethAuthentication') && shib_ok(); + my $shib = shib_ok(); my $shib_login = $shib ? get_login_shib() : undef; # $authnotrequired will be set for scripts which will run without authentication @@ -2009,7 +2009,7 @@ sub checkpw { $type = 'opac' unless $type; # Get shibboleth login attribute - my $shib = C4::Context->preference('ShibbolethAuthentication') && shib_ok(); + my $shib = shib_ok(); my $shib_login = $shib ? get_login_shib() : undef; my $anonymous_patron = C4::Context->preference('AnonymousPatron'); diff --git a/admin/identity_providers.pl b/admin/identity_providers.pl index 7f2d6a775d3..1104f7a2fc8 100755 --- a/admin/identity_providers.pl +++ b/admin/identity_providers.pl @@ -19,295 +19,79 @@ use Modern::Perl; -use CGI qw ( -utf8 ); -use Scalar::Util qw( blessed ); -use Try::Tiny qw( catch try ); +use CGI qw ( -utf8 ); -use C4::Auth qw( get_template_and_user ); -use C4::Output qw( output_html_with_http_headers ); +use C4::Auth qw( get_template_and_user ); +use C4::Context qw(); +use C4::Output qw( output_html_with_http_headers ); -use Koha::Database; -use Koha::Auth::Identity::Providers; +use Koha::Patron::Attribute::Types; +use Koha::Libraries; +use Koha::Patron::Categories; +use Koha::Patrons; -my $input = CGI->new; -my $op = $input->param('op') || 'list'; -my $domain_ops = $input->param('domain_ops'); -my $identity_provider_id = $input->param('identity_provider_id'); -my $identity_provider; - -$identity_provider = Koha::Auth::Identity::Providers->find($identity_provider_id) - unless !$identity_provider_id; - -my $template_name = $domain_ops ? 'admin/identity_provider_domains.tt' : 'admin/identity_providers.tt'; +my $input = CGI->new; my ( $template, $borrowernumber, $cookie ) = get_template_and_user( { - template_name => $template_name, + template_name => 'admin/identity_providers.tt', query => $input, type => "intranet", flagsrequired => { parameters => 'manage_identity_providers' }, } ); -my @messages; - -if ( !$domain_ops && $op eq 'cud-add' ) { - - # IdP configuration params - my $code = $input->param('code'); - my $config = $input->param('config'); - my $description = $input->param('description'); - my $icon_url = $input->param('icon_url'); - my $mapping = $input->param('mapping'); - my $matchpoint = $input->param('matchpoint'); - my $protocol = $input->param('protocol'); - - # Domain configuration params - my $allow_opac = $input->param('allow_opac') // 0; - my $allow_staff = $input->param('allow_staff') // 0; - my $auto_register_opac = $input->param('auto_register_opac') // 0; - my $auto_register_staff = $input->param('auto_register_staff') // 0; - my $default_category_id = $input->param('default_category_id'); - my $default_library_id = $input->param('default_library_id'); - my $domain = $input->param('domain'); - my $update_on_auth = $input->param('update_on_auth'); - - try { - Koha::Database->new->schema->txn_do( - sub { - my $provider = Koha::Auth::Identity::Provider->new( - { - code => $code, - config => $config, - description => $description, - icon_url => $icon_url, - mapping => $mapping, - matchpoint => $matchpoint, - protocol => $protocol, - } - )->store; - - Koha::Auth::Identity::Provider::Domain->new( - { - identity_provider_id => $provider->identity_provider_id, - allow_opac => $allow_opac, - allow_staff => $allow_staff, - auto_register_opac => $auto_register_opac, - auto_register_staff => $auto_register_staff, - default_category_id => $default_category_id, - default_library_id => $default_library_id, - domain => $domain, - update_on_auth => $update_on_auth, - } - )->store; - - push @messages, { type => 'message', code => 'success_on_insert' }; - } - ); - } catch { - if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) { - push @messages, - { - type => 'alert', - code => 'error_on_insert', - reason => 'duplicate_id' - }; - } - }; - - # list servers after adding - $op = 'list'; -} elsif ( $domain_ops && $op eq 'cud-add' ) { - - my $allow_opac = $input->param('allow_opac'); - my $allow_staff = $input->param('allow_staff'); - my $identity_provider_id = $input->param('identity_provider_id'); - my $auto_register_opac = $input->param('auto_register_opac'); - my $auto_register_staff = $input->param('auto_register_staff'); - my $default_category_id = $input->param('default_category_id') || undef; - my $default_library_id = $input->param('default_library_id') || undef; - my $domain = $input->param('domain'); - my $update_on_auth = $input->param('update_on_auth'); - - try { - - Koha::Auth::Identity::Provider::Domain->new( - { - allow_opac => $allow_opac, - allow_staff => $allow_staff, - identity_provider_id => $identity_provider_id, - auto_register_opac => $auto_register_opac, - auto_register_staff => $auto_register_staff, - default_category_id => $default_category_id, - default_library_id => $default_library_id, - domain => $domain, - update_on_auth => $update_on_auth, - } - )->store; - - push @messages, { type => 'message', code => 'success_on_insert' }; - } catch { - if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) { - push @messages, - { - type => 'alert', - code => 'error_on_insert', - reason => 'duplicate_id' - }; - } - }; - - # list servers after adding - $op = 'list'; -} elsif ( !$domain_ops && $op eq 'edit_form' ) { - - if ($identity_provider) { - $template->param( identity_provider => $identity_provider ); - } else { - push @messages, - { - type => 'alert', - code => 'error_on_edit', - reason => 'invalid_id' - }; - } -} elsif ( $domain_ops && $op eq 'edit_form' ) { - my $identity_provider_domain_id = $input->param('identity_provider_domain_id'); - my $identity_provider_domain; - - $identity_provider_domain = Koha::Auth::Identity::Provider::Domains->find($identity_provider_domain_id) - unless !$identity_provider_domain_id; - - if ($identity_provider_domain) { - $template->param( identity_provider_domain => $identity_provider_domain ); - } else { - push @messages, - { - type => 'alert', - code => 'error_on_edit', - reason => 'invalid_id' - }; - } -} elsif ( !$domain_ops && $op eq 'cud-edit_save' ) { - - if ($identity_provider) { - - my $code = $input->param('code'); - my $config = $input->param('config'); - my $description = $input->param('description'); - my $icon_url = $input->param('icon_url'); - my $mapping = $input->param('mapping'); - my $matchpoint = $input->param('matchpoint'); - my $protocol = $input->param('protocol'); - - try { +my $borrowers_source = Koha::Patrons->_resultset->result_source; +my @borrower_columns; +my %skip_columns = map { $_ => 1 } qw( password updated_on timestamp ); +foreach my $column ( sort $borrowers_source->columns ) { + next if $skip_columns{$column}; + my $column_info = $borrowers_source->column_info($column); + my $label = $column_info->{comments} || $column; + push @borrower_columns, { value => $column, label => $label }; +} - $identity_provider->set( - { - code => $code, - config => $config, - description => $description, - icon_url => $icon_url, - mapping => $mapping, - matchpoint => $matchpoint, - protocol => $protocol, - } - )->store; +my @libraries_map = map { { value => $_->branchcode, label => $_->branchname } } + Koha::Libraries->search( {}, { order_by => 'branchname' } )->as_list; - push @messages, - { - type => 'message', - code => 'success_on_update' - }; - } catch { - push @messages, - { - type => 'alert', - code => 'error_on_update' - }; - }; +my @categories_map = map { { value => $_->categorycode, label => $_->description } } + Koha::Patron::Categories->search( {}, { order_by => 'description' } )->as_list; - # list servers after adding - $op = 'list'; - } else { - push @messages, - { - type => 'alert', - code => 'error_on_update', - reason => 'invalid_id' - }; +my @unique_patron_attributes = map { + { + value => 'patron_attribute:' . $_->code, + label => sprintf( '%s (%s)', $_->description, $_->code ), } -} elsif ( $domain_ops && $op eq 'cud-edit_save' ) { - - my $identity_provider_domain_id = $input->param('identity_provider_domain_id'); - my $identity_provider_domain; - - $identity_provider_domain = Koha::Auth::Identity::Provider::Domains->find($identity_provider_domain_id) - unless !$identity_provider_domain_id; +} Koha::Patron::Attribute::Types->search( + { unique_id => 1 }, + { order_by => 'description' } +)->as_list; - if ($identity_provider_domain) { - - my $identity_provider_id = $input->param('identity_provider_id'); - my $domain = $input->param('domain'); - my $auto_register_opac = $input->param('auto_register_opac') // 0; - my $auto_register_staff = $input->param('auto_register_staff') // 0; - my $update_on_auth = $input->param('update_on_auth'); - my $default_library_id = $input->param('default_library_id') || undef; - my $default_category_id = $input->param('default_category_id') || undef; - my $allow_opac = $input->param('allow_opac'); - my $allow_staff = $input->param('allow_staff'); - - try { - - $identity_provider_domain->set( - { - identity_provider_id => $identity_provider_id, - domain => $domain, - auto_register_opac => $auto_register_opac, - auto_register_staff => $auto_register_staff, - update_on_auth => $update_on_auth, - default_library_id => $default_library_id, - default_category_id => $default_category_id, - allow_opac => $allow_opac, - allow_staff => $allow_staff, - } - )->store; - - push @messages, - { - type => 'message', - code => 'success_on_update' - }; - } catch { - push @messages, - { - type => 'alert', - code => 'error_on_update' - }; - }; - - # list servers after adding - $op = 'list'; - } else { - push @messages, - { - type => 'alert', - code => 'error_on_update', - reason => 'invalid_id' - }; +my @all_patron_attributes = map { + { + value => 'patron_attribute:' . $_->code, + label => sprintf( '%s (%s)', $_->description, $_->code ), } -} - -if ($domain_ops) { - $template->param( - identity_provider_code => $identity_provider->code, - identity_provider_id => $identity_provider_id, - ); +} Koha::Patron::Attribute::Types->search( + {}, + { order_by => 'description' } +)->as_list; + +my @idp_default_hostnames; +for my $pref (qw( OPACBaseURL staffClientBaseURL )) { + my $url = C4::Context->preference($pref); + next unless $url; + my ($hostname) = $url =~ m|^https?://([^/:?#]+)|; + push @idp_default_hostnames, $hostname if $hostname; } $template->param( - op => $op, - messages => \@messages, + borrower_columns => \@borrower_columns, + libraries_map => \@libraries_map, + categories_map => \@categories_map, + idp_default_hostnames => \@idp_default_hostnames, + unique_patron_attributes => \@unique_patron_attributes, + all_patron_attributes => \@all_patron_attributes, ); output_html_with_http_headers $input, $cookie, $template->output; diff --git a/debian/templates/apache-shared-intranet.conf b/debian/templates/apache-shared-intranet.conf index 54155faef88..46c83507a13 100644 --- a/debian/templates/apache-shared-intranet.conf +++ b/debian/templates/apache-shared-intranet.conf @@ -27,6 +27,7 @@ RewriteRule ^/cgi-bin/koha/sip2/.*$ /cgi-bin/koha/sip2/sip2.pl [PT] RewriteCond %{REQUEST_URI} !^/cgi-bin/koha/preservation/.*.pl$ RewriteRule ^/cgi-bin/koha/preservation/.*$ /cgi-bin/koha/preservation/home.pl [PT] RewriteRule ^/cgi-bin/koha/admin/record_sources(.*)?$ /cgi-bin/koha/admin/record_sources.pl$1 [PT] +RewriteRule ^/cgi-bin/koha/admin/identity_providers(/.*)?$ /cgi-bin/koha/admin/identity_providers.pl [PT] RewriteCond %{QUERY_STRING} booksellerid=(.*) RewriteRule ^/cgi-bin/koha/acqui/supplier.pl$ /cgi-bin/koha/acquisition/vendors/%1? [R] RewriteRule ^/cgi-bin/koha/acquisition/vendors(.*)?$ /cgi-bin/koha/acqui/vendors.pl$1 [PT] diff --git a/etc/koha-httpd.conf b/etc/koha-httpd.conf index 367d3bce8f6..558c538722c 100644 --- a/etc/koha-httpd.conf +++ b/etc/koha-httpd.conf @@ -239,6 +239,7 @@ RewriteCond %{REQUEST_URI} !^/cgi-bin/koha/preservation/.*.pl$ RewriteRule ^/cgi-bin/koha/preservation/.*$ /cgi-bin/koha/preservation/home.pl [PT] RewriteRule ^/cgi-bin/koha/admin/record_sources(.*)?$ /cgi-bin/koha/admin/record_sources.pl$1 [PT] + RewriteRule ^/cgi-bin/koha/admin/identity_providers(/.*)?$ /cgi-bin/koha/admin/identity_providers.pl [PT] RewriteCond %{QUERY_STRING} booksellerid=(.*) RewriteRule ^/cgi-bin/koha/acqui/supplier.pl$ /cgi-bin/koha/acquisition/vendors/%1? [R] RewriteRule ^/cgi-bin/koha/acquisition/vendors(.*)?$ /cgi-bin/koha/acqui/vendors.pl$1 [PT] diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc index 4488545dcd5..b93e631c573 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc @@ -45,7 +45,6 @@ can_user_parameters_manage_additional_fields="[% CAN_user_parameters_manage_additional_fields | html %]" can_user_parameters_manage_keyboard_shortcuts="[% CAN_user_parameters_manage_keyboard_shortcuts | html %]" can_user_ill="[% CAN_user_ill | html %]" - shibbolethauthentication="[% Koha.Preference('ShibbolethAuthentication') | html %]" usecirculationdesks="[% Koha.Preference('UseCirculationDesks') | html %]" usecashregisters="[% Koha.Preference('UseCashRegisters') | html %]" savedsearchfilters="[% Koha.Preference('SavedSearchFilters') | html %]" diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/identity_providers.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/identity_providers.tt index 0b488f5520c..d315e03ea83 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/identity_providers.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/identity_providers.tt @@ -1,609 +1,44 @@ [% USE raw %] -[% USE Koha %] [% USE Asset %] -[% USE Branches %] -[% USE Categories %] +[% USE To %] [% PROCESS 'i18n.inc' %] [% SET footerjs = 1 %] [% INCLUDE 'doc-head-open.inc' %] [% FILTER collapse %] - [% IF op == 'add_form' %] - [% t("New identity provider") | html %] - › - [% ELSIF op == 'edit_form' %] - [% tx("Modify identity provider '{id_provider}'", {id_provider = identity_provider.code}) | html %] - › - [% END %] [% t("Identity providers") | html %] › [% t("Administration") | html %] › [% t("Koha") | html %] - [% END %] + [% END %] + [% INCLUDE 'doc-head-close.inc' %] -[% INCLUDE 'header.inc' %] -[% INCLUDE 'prefs-admin-search.inc' %] +[% WRAPPER 'header.inc' %] + [% INCLUDE 'prefs-admin-search.inc' %] +[% END %] [% WRAPPER 'sub-header.inc' %] - [% WRAPPER breadcrumbs %] - [% WRAPPER breadcrumb_item %] - Administration - [% END %] - - [% IF op == 'add_form' %] - [% WRAPPER breadcrumb_item %] - Identity providers - [% END %] - [% WRAPPER breadcrumb_item bc_active= 1 %] - New identity provider - [% END %] - [% ELSIF op == 'edit_form' %] - [% WRAPPER breadcrumb_item %] - Identity providers - [% END %] - [% WRAPPER breadcrumb_item bc_active= 1 %] - [% tx("Modify identity provider '{id_provider}'", {id_provider = identity_provider.code}) | html %] - [% END %] - [% ELSE %] - [% WRAPPER breadcrumb_item bc_active= 1 %] - Identity providers - [% END %] - [% END %] - [% END #/ WRAPPER breadcrumbs %] +
[% END #/ WRAPPER sub-header.inc %] [% WRAPPER 'main-container.inc' aside='admin-menu' %] - - [% FOREACH m IN messages %] -
- [% SWITCH m.code %] - [% CASE 'error_on_update' %] - An error occurred trying to open the identity provider for editing. The passed ID is invalid. - [% CASE 'error_on_insert' %] - An error occurred when adding a new identity provider. Check the logs for details. - [% CASE 'success_on_update' %] - Identity provider updated successfully. - [% CASE 'success_on_insert' %] -
Identity provider added successfully.
-
You will need to restart Koha for the provider to work.
- [% CASE %] - [% m.code | html %] - [% END %] -
- [% END %] - - - - - [% IF op == 'add_form' %] -

New identity provider

- -
- [% INCLUDE 'csrf-token.inc' %] - -
- Basic configuration -
    -
  1. - - - Required -
    Code that identifies this provider. Only alphanumeric and "_" characters are allowed
    -
  2. -
  3. - - - Required -
    User friendly name of this provider
    -
  4. -
  5. - - -
    Choose the protocol this external identity provider uses
    -
  6. -
-
- -
- Advanced configuration -
    -
  1. - - - Required -
    Provider's main configuration.
    - -
    - -
    -
  2. -
  3. - - - Required -
    Map provider's result to Koha patron's fields.
    - -
    - -
    -
  4. -
  5. - - -
    Koha patron's field that will be used to match provider's user with Koha's
    -
    It must be present in mapping
    -
  6. -
  7. - - -
  8. -
-
- -
- Domain configuration -
    -
  1. - - - Required -
    Use * for any domain. You can add new domains later on the dedicated admin page.
    -
  2. -
  3. - - - Required -
    Use this library for the patron on auto register
    -
  4. -
  5. - - [% SET categories = Categories.all() %] - - Required -
    Use this category for the patron on auto register
    -
  6. -
  7. - - -
    Allow users of this domain to log into the OPAC using this identity provider.
    -
  8. -
  9. - - -
    Allow staff access to users from this domain to login with this identity provider.
    -
  10. -
  11. - - - users to auto register on login (OPAC) -
  12. -
  13. - - - users to auto register on login (Staff interface) -
  14. -
  15. - - -
    Update user data on login.
    -
-
- -
- - Cancel -
-
- [% END %] - - [% IF op == 'edit_form' %] -

[% tx("Modify identity provider '{id_provider}'", {id_provider = identity_provider.code}) | html %]

- -
- [% INCLUDE 'csrf-token.inc' %] - - -
- Basic configuration -
    -
  1. - - - Required -
    Code that identifies this provider. Only alphanumeric and "_" characters are allowed
    -
  2. -
  3. - - - Required -
    User friendly name of this provider
    -
  4. -
  5. - - -
    Choose the protocol this external identity provider uses
    -
  6. -
-
- -
- Advanced configuration -
    -
  1. - - - Required -
    Provider's main configuration.
    - -
    - -
    -
  2. -
  3. - - - Required -
    Map provider's result to Koha patron's fields.
    - -
    - -
    -
  4. -
  5. - - -
    Koha patron's field that will be used to match provider's user with Koha's
    -
    It must be present in mapping
    -
  6. -
  7. - - -
  8. -
-
-
- - Cancel -
-
- [% END %] - - [% IF op == 'list' %] -
- New identity provider -
- -

Identity providers

-
- - - - - - - - - -
CodeDescriptionProtocolActions
-
- [% END %] - - - -[% END %] +
+[% END #/ WRAPPER main-container.inc %] [% MACRO jsinclude BLOCK %] - [% Asset.js("js/admin-menu.js") | $raw %] + [% INCLUDE 'calendar.inc' %] [% INCLUDE 'datatables.inc' %] + [% Asset.js("js/admin-menu.js") | $raw %] + [% Asset.js("js/vue/dist/admin/identity_providers.js") | $raw %] [% END %] - [% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref index 37202dc17c6..db3f9cae2b1 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref @@ -1015,9 +1015,3 @@ OPAC: - "fields (separate values with |). Tabs appear in the order listed.
" - "Currently supported values: Item types (itemtypes), Collection, (ccode) and Shelving location (loc)." Authentication: - - - - pref: OPACShibOnly - choices: - 1: "Don't allow" - 0: Allow - - patrons to login by means other than Shibboleth. diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/staff_interface.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/staff_interface.pref index dbd3a877a32..2c321a3fe9f 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/staff_interface.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/staff_interface.pref @@ -204,12 +204,6 @@ Staff interface: - 'records when downloading from the reports module.
NOTE: Only a positive value will enforce a limit. A specific limit in the report overrides this setting.' Authentication: - - - - pref: staffShibOnly - choices: - 1: "Don't allow" - 0: Allow - - staff to log in by means other than Shibboleth. - - pref: TwoFactorAuthentication choices: diff --git a/koha-tmpl/intranet-tmpl/prog/js/fetch/api-client.js b/koha-tmpl/intranet-tmpl/prog/js/fetch/api-client.js index 918947db605..8779b53848e 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/fetch/api-client.js +++ b/koha-tmpl/intranet-tmpl/prog/js/fetch/api-client.js @@ -197,6 +197,9 @@ export const APIClient = { patron: createClientProxy(() => import("./patron-api-client.js")), patron_list: createClientProxy(() => import("./patron-list-api-client.js")), recall: createClientProxy(() => import("./recall-api-client.js")), + identity_providers: createClientProxy( + () => import("./identity-providers-api-client.js") + ), sysprefs: createClientProxy( () => import("./system-preferences-api-client.js") ), diff --git a/koha-tmpl/intranet-tmpl/prog/js/fetch/identity-providers-api-client.js b/koha-tmpl/intranet-tmpl/prog/js/fetch/identity-providers-api-client.js new file mode 100644 index 00000000000..9465fffa460 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/fetch/identity-providers-api-client.js @@ -0,0 +1,171 @@ +export class IdentityProvidersAPIClient { + constructor(HttpClient) { + this.httpClient = new HttpClient({ + baseURL: "/api/v1/auth/identity_providers", + }); + this.allHostnamesHttpClient = new HttpClient({ + baseURL: "/api/v1/auth/hostnames", + }); + } + + get providers() { + return { + getAll: params => + this.httpClient.getAll({ + endpoint: "/", + params, + }), + get: id => + this.httpClient.get({ + endpoint: "/" + id, + headers: { + "x-koha-embed": "domains,hostnames,mappings", + }, + }), + create: provider => + this.httpClient.post({ + endpoint: "/", + body: provider, + }), + update: (provider, id) => + this.httpClient.put({ + endpoint: "/" + id, + body: provider, + }), + delete: id => + this.httpClient.delete({ + endpoint: "/" + id, + }), + count: (query = {}) => + this.httpClient.count({ + endpoint: + "?" + + new URLSearchParams({ + _page: 1, + _per_page: 1, + ...(query && { q: JSON.stringify(query) }), + }), + }), + }; + } + + get mappings() { + return { + getAll: (providerId, params) => + this.httpClient.getAll({ + endpoint: `/${providerId}/mappings`, + params, + }), + get: (providerId, mappingId) => + this.httpClient.get({ + endpoint: `/${providerId}/mappings/${mappingId}`, + }), + create: (providerId, mapping) => + this.httpClient.post({ + endpoint: `/${providerId}/mappings`, + body: mapping, + }), + update: (providerId, mapping, mappingId) => + this.httpClient.put({ + endpoint: `/${providerId}/mappings/${mappingId}`, + body: mapping, + }), + delete: (providerId, mappingId) => + this.httpClient.delete({ + endpoint: `/${providerId}/mappings/${mappingId}`, + }), + count: (providerId, query = {}) => + this.httpClient.count({ + endpoint: + `/${providerId}/mappings?` + + new URLSearchParams({ + _page: 1, + _per_page: 1, + ...(query && { q: JSON.stringify(query) }), + }), + }), + }; + } + + get hostnames() { + return { + getAll: (providerId, params) => + this.httpClient.getAll({ + endpoint: `/${providerId}/hostnames`, + params, + }), + get: (providerId, hostnameId) => + this.httpClient.get({ + endpoint: `/${providerId}/hostnames/${hostnameId}`, + }), + create: (providerId, hostname) => + this.httpClient.post({ + endpoint: `/${providerId}/hostnames`, + body: hostname, + }), + update: (providerId, hostname, hostnameId) => + this.httpClient.put({ + endpoint: `/${providerId}/hostnames/${hostnameId}`, + body: hostname, + }), + delete: (providerId, hostnameId) => + this.httpClient.delete({ + endpoint: `/${providerId}/hostnames/${hostnameId}`, + }), + }; + } + + get allHostnames() { + return { + getAll: params => + this.allHostnamesHttpClient.getAll({ + endpoint: "/", + params, + }), + get: id => + this.allHostnamesHttpClient.get({ + endpoint: "/" + id, + }), + }; + } + + get domains() { + return { + getAll: (providerId, params) => + this.httpClient.getAll({ + endpoint: `/${providerId}/domains`, + params, + }), + get: (providerId, domainId) => + this.httpClient.get({ + endpoint: `/${providerId}/domains/${domainId}`, + }), + create: (providerId, domain) => + this.httpClient.post({ + endpoint: `/${providerId}/domains`, + body: domain, + }), + update: (providerId, domain, domainId) => + this.httpClient.put({ + endpoint: `/${providerId}/domains/${domainId}`, + body: domain, + }), + delete: (providerId, domainId) => + this.httpClient.delete({ + endpoint: `/${providerId}/domains/${domainId}`, + }), + count: (providerId, query = {}) => + this.httpClient.count({ + endpoint: + `/${providerId}/domains?` + + new URLSearchParams({ + _page: 1, + _per_page: 1, + ...(query && { q: JSON.stringify(query) }), + }), + }), + }; + } +} + +export default IdentityProvidersAPIClient; diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/DomainResource.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/DomainResource.vue new file mode 100644 index 00000000000..641398db5f4 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/DomainResource.vue @@ -0,0 +1,261 @@ + + + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/Main.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/Main.vue new file mode 100644 index 00000000000..852a82ff178 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/Main.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/MappingResource.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/MappingResource.vue new file mode 100644 index 00000000000..97eb89012c3 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/MappingResource.vue @@ -0,0 +1,215 @@ + + + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/ProviderResource.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/ProviderResource.vue new file mode 100644 index 00000000000..e8808070244 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/ProviderResource.vue @@ -0,0 +1,856 @@ + + + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Islands/AdminMenu.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Islands/AdminMenu.vue index bc806611f19..5f769af62e4 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Islands/AdminMenu.vue +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Islands/AdminMenu.vue @@ -120,18 +120,6 @@ >{{ $__("Self-service circulation (SIP2)") }} -
  • - {{ $__("Shibboleth configuration") }} -
  • { + navigationStore.$patch({ current: to.matched, params: to.params || {} }); + removeMessages(); +}); diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/routes/identity-providers.js b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/identity-providers.js new file mode 100644 index 00000000000..62f565660fa --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/identity-providers.js @@ -0,0 +1,125 @@ +import { markRaw } from "vue"; + +import ResourceWrapper from "../components/ResourceWrapper.vue"; +import { $__ } from "../i18n"; + +export const routes = [ + { + path: "/cgi-bin/koha/admin/admin-home.pl", + title: $__("Administration"), + children: [ + { + path: "/cgi-bin/koha/admin/identity_providers.pl", + is_default: true, + is_base: true, + title: $__("Identity providers"), + children: [ + { + path: "", + redirect: { name: "ProvidersList" }, + }, + { + path: "/cgi-bin/koha/admin/identity_providers", + is_empty: true, + is_end_node: true, + resource: "IdentityProviders/ProviderResource.vue", + children: [ + { + path: "", + name: "ProvidersList", + is_empty: true, + component: markRaw(ResourceWrapper), + }, + { + path: ":identity_provider_id", + name: "ProviderShow", + component: markRaw(ResourceWrapper), + title: $__("Show provider"), + }, + { + path: "add", + name: "ProviderFormAdd", + component: markRaw(ResourceWrapper), + title: $__("New identity provider"), + }, + { + path: "edit/:identity_provider_id", + name: "ProviderFormEdit", + component: markRaw(ResourceWrapper), + title: $__("Edit identity provider"), + }, + { + path: ":identity_provider_id/mappings", + title: $__("Field mappings"), + icon: "fa fa-exchange-alt", + is_end_node: true, + resource: + "IdentityProviders/MappingResource.vue", + children: [ + { + path: "", + name: "MappingsList", + is_empty: true, + component: markRaw(ResourceWrapper), + }, + { + path: ":identity_provider_mapping_id", + name: "MappingShow", + component: markRaw(ResourceWrapper), + title: $__("Show mapping"), + }, + { + path: "add", + name: "MappingsFormAdd", + component: markRaw(ResourceWrapper), + title: $__("New field mapping"), + }, + { + path: "edit/:identity_provider_mapping_id", + name: "MappingsFormEdit", + component: markRaw(ResourceWrapper), + title: $__("Edit field mapping"), + }, + ], + }, + { + path: ":identity_provider_id/domains", + title: $__("Domains"), + icon: "fa fa-globe", + is_end_node: true, + resource: + "IdentityProviders/DomainResource.vue", + children: [ + { + path: "", + name: "DomainsList", + is_empty: true, + component: markRaw(ResourceWrapper), + }, + { + path: ":identity_provider_domain_id", + name: "DomainShow", + component: markRaw(ResourceWrapper), + title: $__("Show domain"), + }, + { + path: "add", + name: "DomainsFormAdd", + component: markRaw(ResourceWrapper), + title: $__("New domain"), + }, + { + path: "edit/:identity_provider_domain_id", + name: "DomainsFormEdit", + component: markRaw(ResourceWrapper), + title: $__("Edit domain"), + }, + ], + }, + ], + }, + ], + }, + ], + }, +]; diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/identity-providers.js b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/identity-providers.js new file mode 100644 index 00000000000..52521e6586b --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/identity-providers.js @@ -0,0 +1,15 @@ +import { defineStore } from "pinia"; + +export const useIdentityProvidersStore = defineStore("identity_providers", { + state: () => ({ + current_provider: null, + }), + actions: { + setCurrentProvider(provider) { + this.current_provider = provider; + }, + clearCurrentProvider() { + this.current_provider = null; + }, + }, +}); diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/navigation.js b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/navigation.js index 887f0fd0df1..589ff696e24 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/navigation.js +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/navigation.js @@ -190,6 +190,9 @@ export const useNavigationStore = defineStore("navigation", () => { } }), leftNavigation: computed(() => { + if (!store.current || store.current.length === 0) { + return _getNavigationElements(store.routeState); + } const currentRoute = store.current[store.current.length - 1]; if (currentRoute) { const alternateMenuRequired = diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc index 2b6d4478d6d..bb02cd28d30 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc @@ -136,7 +136,7 @@ Log out [% ELSE %] [% IF Koha.Preference('casAuthentication') %] - [%# CAS authentication is too complicated for modal window %] + [%# CAS authentication and Shibboleth force SSO are too complicated for modal window %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt index 6d6c135cc1a..db140ccbcec 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt @@ -97,110 +97,104 @@
    - [% UNLESS ( Koha.Preference('OPACShibOnly') ) %] - [% IF ( casAuthentication ) %] - [% IF ( invalidCasLogin ) %] - -

    Sorry, the CAS login also failed. If you have a local login you may use that below.

    - [% ELSE %] -

    If you have a CAS account, you may use that below.

    - [% END %] + [% IF ( casAuthentication ) %] + [% IF ( invalidCasLogin ) %] + +

    Sorry, the CAS login also failed. If you have a local login you may use that below.

    [% ELSE %] -

    If you have a local account, you may use that below.

    +

    If you have a CAS account, you may use that below.

    [% END %] + [% ELSE %] +

    If you have a local account, you may use that below.

    [% END %]
    [% ELSE %]

    Shibboleth login

    Log in using a Shibboleth account.

    [% END # /IF invalidShibLogin %] - [% UNLESS ( Koha.Preference('OPACShibOnly') ) %] - [% IF ( casAuthentication ) %] -

    CAS login

    -

    If you do not have a Shibboleth account, but you do have a CAS account, you can use CAS.

    - [% ELSE %] -

    Local login

    -

    If you do not have a Shibboleth account, but you do have a local login, then you may login below.

    - [% END %] + [% IF ( casAuthentication ) %] +

    CAS login

    +

    If you do not have a Shibboleth account, but you do have a CAS account, you can use CAS.

    + [% ELSE %] +

    Local login

    +

    If you do not have a Shibboleth account, but you do have a local login, then you may login below.

    [% END %] [% END # /IF shibbolethAuthentication %] - [% UNLESS ( Koha.Preference('OPACShibOnly') ) %] - [% IF ( casAuthentication ) %] - [% IF ( shibbolethAuthentication ) %] - [% IF ( casServerUrl ) %] -

    Log in.

    - [% END %] - - [% IF ( casServersLoop ) %] -

    Please choose against which one you would like to authenticate:

    - - [% END %] - [% ELSE %] - [% IF ( invalidCasLogin ) %] -
    - - -
    - [% END %] - -

    CAS login

    + [% IF ( casAuthentication ) %] + [% IF ( shibbolethAuthentication ) %] + [% IF ( casServerUrl ) %] +

    Log in.

    + [% END %] - [% IF ( casServerUrl ) %] -

    Log in using a CAS account.

    - [% END %] + [% IF ( casServersLoop ) %] +

    Please choose against which one you would like to authenticate:

    + + [% END %] + [% ELSE %] + [% IF ( invalidCasLogin ) %] +
    + + +
    + [% END %] - [% IF ( casServersLoop ) %] -

    If you have a CAS account, please choose against which one you would like to authenticate:

    - - [% END %] - [% END # /IF shibbolethAuthentication %] +

    CAS login

    - [% IF ( shibbolethAuthentication ) %] -

    Nothing

    - [% ELSE %] -

    Local login

    -

    If you do not have a CAS account, but do have a local account, you can still log in:

    + [% IF ( casServerUrl ) %] +

    Log in using a CAS account.

    [% END %] - [% END # / IF casAuthentication %] - [% SET identity_providers = AuthClient.get_providers('opac') %] - [% IF ( ! identity_providers.empty ) %] - [% FOREACH provider IN identity_providers %] -

    - - [% IF provider.icon_url %] - - [% ELSE %] - - [% END %] - Log in with [% provider.description | html %] - -

    + [% IF ( casServersLoop ) %] +

    If you have a CAS account, please choose against which one you would like to authenticate:

    + [% END %] -
    -

    If you do not have an external account, but do have a local account, you can still log in:

    - [% END # /IF identity_providers %] + [% END # /IF shibbolethAuthentication %] - [% IF ( Koha.Preference('GoogleOpenIDConnect') == 1 ) %] - [% IF ( invalidGoogleOpenIDConnectLogin ) %] -

    Google login

    -

    Sorry, your Google login failed. [% invalidGoogleOpenIDConnectLogin | html %]

    -

    Please note that the Google login will only work if you are using the e-mail address registered with this library.

    -

    If you want to, you can try to log in using a different account

    - [% END %] - Log in with Google -

    If you do not have a Google account, but do have a local account, you can still log in:

    - [% END # /IF GoogleOpenIDConnect %] - [% END # /UNLESS OPACShibOnly %] + [% IF ( shibbolethAuthentication ) %] +

    Nothing

    + [% ELSE %] +

    Local login

    +

    If you do not have a CAS account, but do have a local account, you can still log in:

    + [% END %] + [% END # / IF casAuthentication %] + + [% SET identity_providers = AuthClient.get_providers('opac') %] + [% IF ( ! identity_providers.empty ) %] + [% FOREACH provider IN identity_providers %] +

    + + [% IF provider.icon_url %] + + [% ELSE %] + + [% END %] + Log in with [% provider.description | html %] + +

    + [% END %] +
    +

    If you do not have an external account, but do have a local account, you can still log in:

    + [% END # /IF identity_providers %] + + [% IF ( Koha.Preference('GoogleOpenIDConnect') == 1 ) %] + [% IF ( invalidGoogleOpenIDConnectLogin ) %] +

    Google login

    +

    Sorry, your Google login failed. [% invalidGoogleOpenIDConnectLogin | html %]

    +

    Please note that the Google login will only work if you are using the e-mail address registered with this library.

    +

    If you want to, you can try to log in using a different account

    + [% END %] + Log in with Google +

    If you do not have a Google account, but do have a local account, you can still log in:

    + [% END # /IF GoogleOpenIDConnect %] [% IF !(invalid_username_or_password || too_many_login_attempts) and is_anonymous_patron %]
    @@ -227,7 +221,7 @@ [% ELSE %]

    You must contact the library to reset your password

    [% END %] - [% ELSIF !Koha.Preference('OPACShibOnly') or SCO_login or SCI_login %] + [% ELSIF SCO_login or SCI_login %] [% SET form_action = script_name %] [% IF SCO_login %] [% form_action = "/cgi-bin/koha/sco/sco-main.pl" %] diff --git a/opac/opac-user.pl b/opac/opac-user.pl index e0671b33aa5..be4f6374096 100755 --- a/opac/opac-user.pl +++ b/opac/opac-user.pl @@ -21,8 +21,9 @@ use Modern::Perl; use CGI qw ( -utf8 ); use URI; -use C4::Auth qw( get_template_and_user ); -use C4::Koha qw( +use C4::Auth qw( get_template_and_user ); +use C4::Auth_with_shibboleth qw( shib_ok ); +use C4::Koha qw( getitemtypeimagelocation GetNormalizedISBN GetNormalizedUPC @@ -82,7 +83,7 @@ for ( C4::Context->preference("OPACShowHoldQueueDetails") ) { my $patronupdate = $query->param('patronupdate'); my $canrenew = 1; -$template->param( shibbolethAuthentication => C4::Context->config('useshibboleth') ); +$template->param( shibbolethAuthentication => shib_ok() ); # get borrower information .... my $patron = Koha::Patrons->find($borrowernumber); diff --git a/rspack.config.js b/rspack.config.js index 62cf6b1956c..ad2ab7e3335 100644 --- a/rspack.config.js +++ b/rspack.config.js @@ -28,6 +28,8 @@ module.exports = [ "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/acquisitions.ts", islands: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts", sip2: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/sip2.ts", + "admin/identity_providers": + "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/identity-providers.ts", }, output: { filename: "[name].js", -- 2.53.0