From 1b6b498413012052892a787bcbc202f8e856d8f8 Mon Sep 17 00:00:00 2001 From: Jacob O'Mara Date: Sun, 22 Feb 2026 20:16:16 +0000 Subject: [PATCH] Bug 39224: Database updates for unified identity providers Adds database tables and schema changes to support unified identity providers with hostname-based selection: - identity_provider_mappings: Maps identity provider fields to Koha patron fields - identity_provider_hostnames: Links identity providers to hostnames for automatic provider selection - hostnames: Registry of library hostnames for provider routing - Adds SAML2 to supported protocols - Migrates system preferences and configuration to new tables Test plan: 1. Apply the atomic update 2. Verify new tables exist in the database 3. Verify identity_providers table has new columns 4. Verify any existing identity providers, including SAML2 entries from koha-conf.xml are now in the new tables Sponsored-by: ByWater Solutions Co-authored-by: Martin Renvoize --- .../data/mysql/atomicupdate/bug_39224.pl | 398 ++++++++++++++++++ installer/data/mysql/kohastructure.sql | 63 ++- installer/data/mysql/mandatory/sysprefs.sql | 2 - 3 files changed, 458 insertions(+), 5 deletions(-) create mode 100755 installer/data/mysql/atomicupdate/bug_39224.pl diff --git a/installer/data/mysql/atomicupdate/bug_39224.pl b/installer/data/mysql/atomicupdate/bug_39224.pl new file mode 100755 index 00000000000..3eb159d7c0a --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_39224.pl @@ -0,0 +1,398 @@ +use Modern::Perl; +use Koha::Installer::Output qw(say_warning say_success say_info); +use JSON; + +return { + bug_number => "39224", + description => + "Unified identity providers: normalized mappings, hostname-based selection, SAML2/Shibboleth migration", + up => sub { + my ($args) = @_; + my ( $dbh, $out ) = @$args{qw(dbh out)}; + + # ── 1. Update identity_providers.protocol enum ───────────────────────────── + # Remove CAS and LDAP (unsupported via this interface), add SAML2. + + if ( column_exists( 'identity_providers', 'protocol' ) ) { + my ($current_type) = $dbh->selectrow_array( + "SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'identity_providers' + AND COLUMN_NAME = 'protocol'" + ); + + unless ( $current_type && $current_type eq "enum('OAuth','OIDC','SAML2')" ) { + + # Warn about and remove any providers using protocols no longer in the enum. + for my $protocol (qw(CAS LDAP)) { + my ($count) = $dbh->selectrow_array( + "SELECT COUNT(*) FROM identity_providers WHERE protocol = ?", + undef, $protocol + ); + if ($count) { + say_warning( + $out, + "Removing $count identity provider(s) with protocol '$protocol'" + . " (no longer supported via this interface)" + ); + $dbh->do( + "DELETE FROM identity_providers WHERE protocol = ?", + undef, $protocol + ); + } + } + + $dbh->do( + q{ + ALTER TABLE identity_providers + MODIFY COLUMN `protocol` + enum('OAuth','OIDC','SAML2') + COLLATE utf8mb4_unicode_ci NOT NULL + COMMENT 'Protocol provider speaks' + } + ); + say_success( + $out, + "Updated identity_providers.protocol enum to ('OAuth','OIDC','SAML2')" + ); + } + } + + # ── 2. Add enabled column to identity_providers ──────────────────────────── + + unless ( column_exists( 'identity_providers', 'enabled' ) ) { + $dbh->do( + q{ + ALTER TABLE identity_providers + ADD COLUMN `enabled` tinyint(1) NOT NULL DEFAULT 1 + COMMENT 'Whether this provider is active' + AFTER `config` + } + ); + say_success( $out, "Added column 'identity_providers.enabled'" ); + } + + # ── 3. Create identity_provider_mappings table ───────────────────────────── + + unless ( TableExists('identity_provider_mappings') ) { + $dbh->do( + q{ + CREATE TABLE `identity_provider_mappings` ( + `mapping_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key', + `identity_provider_id` int(11) NOT NULL COMMENT 'Reference to identity provider', + `provider_field` varchar(255) DEFAULT NULL COMMENT 'Attribute name from the identity provider', + `koha_field` varchar(255) NOT NULL COMMENT 'Corresponding field in Koha borrowers table', + `default_content` varchar(255) DEFAULT NULL COMMENT 'Default value if provider does not supply this field', + `is_matchpoint` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Use this field to match existing patrons', + PRIMARY KEY (`mapping_id`), + UNIQUE KEY `provider_koha_field` (`identity_provider_id`, `koha_field`), + KEY `provider_field_idx` (`provider_field`), + CONSTRAINT `idp_mapping_ibfk_1` FOREIGN KEY (`identity_provider_id`) + REFERENCES `identity_providers` (`identity_provider_id`) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + } + ); + say_success( $out, "Added new table 'identity_provider_mappings'" ); + } + + # ── 4. Migrate OAuth/OIDC JSON mappings to normalized rows ───────────────── + # The old identity_providers table stored mappings as a JSON blob. + + if ( column_exists( 'identity_providers', 'mapping' ) ) { + my $providers = $dbh->selectall_arrayref( + "SELECT identity_provider_id, mapping, matchpoint FROM identity_providers + WHERE mapping IS NOT NULL AND mapping != '' AND mapping != '{}'", + { Slice => {} } + ); + + my $migrated = 0; + for my $provider (@$providers) { + my $mapping_json = $provider->{mapping}; + my $matchpoint = $provider->{matchpoint}; + my $provider_id = $provider->{identity_provider_id}; + + my $mapping; + eval { $mapping = decode_json($mapping_json) }; + next unless $mapping && ref $mapping eq 'HASH'; + + for my $koha_field ( keys %$mapping ) { + my $provider_field = $mapping->{$koha_field}; + my $is_matchpoint = ( defined $matchpoint && $matchpoint eq $koha_field ) ? 1 : 0; + + $dbh->do( + q{ + INSERT IGNORE INTO identity_provider_mappings + (identity_provider_id, provider_field, koha_field, is_matchpoint) + VALUES (?, ?, ?, ?) + }, undef, + $provider_id, $provider_field, $koha_field, $is_matchpoint + ); + $migrated++; + } + } + + say_success( + $out, + "Migrated $migrated OAuth/OIDC field mapping(s) to identity_provider_mappings" + ); + + $dbh->do("ALTER TABLE identity_providers DROP COLUMN `mapping`"); + say_success( $out, "Dropped column 'identity_providers.mapping'" ); + + $dbh->do("ALTER TABLE identity_providers DROP COLUMN `matchpoint`"); + say_success( $out, "Dropped column 'identity_providers.matchpoint'" ); + } + + # ── 5. Add send_welcome_email to identity_provider_domains ──────────────── + + unless ( column_exists( 'identity_provider_domains', 'send_welcome_email' ) ) { + $dbh->do( + q{ + ALTER TABLE identity_provider_domains + ADD COLUMN `send_welcome_email` tinyint(1) NOT NULL DEFAULT 0 + COMMENT 'Send welcome email to patron on first login' + AFTER `auto_register_staff` + } + ); + say_success( $out, "Added column 'identity_provider_domains.send_welcome_email'" ); + } + + # ── 6. Create canonical hostnames table ──────────────────────────────────── + + unless ( TableExists('hostnames') ) { + $dbh->do( + q{ + CREATE TABLE `hostnames` ( + `hostname_id` int(11) NOT NULL AUTO_INCREMENT + COMMENT 'Unique identifier for this hostname', + `hostname` varchar(255) NOT NULL + COMMENT 'Server hostname string', + PRIMARY KEY (`hostname_id`), + UNIQUE KEY `hostname` (`hostname`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Canonical hostname registry for identity provider selection' + } + ); + say_success( $out, "Added new table 'hostnames'" ); + + # Seed reserved rows: hostname_id=1 for OPACBaseURL, hostname_id=2 for staffClientBaseURL. + for my $info ( [ 'OPACBaseURL', 1 ], [ 'staffClientBaseURL', 2 ] ) { + my ( $pref_name, $id ) = @$info; + my ($url) = $dbh->selectrow_array( + "SELECT value FROM systempreferences WHERE LOWER(variable) = LOWER(?)", + undef, $pref_name + ); + my ($hostname) = ( $url // '' ) =~ m{^https?://([^/:?#]+)} or next; + $dbh->do( + "INSERT IGNORE INTO hostnames (hostname_id, hostname) VALUES (?, ?)", + undef, $id, $hostname + ); + say_success( $out, "Seeded hostname_id=$id from $pref_name ($hostname)" ); + } + } + + # ── 7. Create/upgrade identity_provider_hostnames bridge table ───────────── + + unless ( TableExists('identity_provider_hostnames') ) { + $dbh->do( + q{ + CREATE TABLE `identity_provider_hostnames` ( + `identity_provider_hostname_id` int(11) NOT NULL AUTO_INCREMENT + COMMENT 'unique key, used to identify the hostname entry', + `hostname_id` int(11) NOT NULL + COMMENT 'FK to hostnames table', + `identity_provider_id` int(11) NOT NULL + COMMENT 'Identity provider associated with this hostname', + `is_enabled` tinyint(1) NOT NULL DEFAULT 1 + COMMENT 'Whether this hostname is active for this provider', + `force_sso` tinyint(1) NOT NULL DEFAULT 0 + COMMENT 'Force SSO redirect for users on this hostname', + PRIMARY KEY (`identity_provider_hostname_id`), + UNIQUE KEY `hostname_id_provider` (`hostname_id`, `identity_provider_id`), + KEY `idp_hostname_provider_idx` (`identity_provider_id`), + CONSTRAINT `fk_iph_hostname` FOREIGN KEY (`hostname_id`) + REFERENCES `hostnames` (`hostname_id`) + ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `idp_hostname_ibfk_1` FOREIGN KEY (`identity_provider_id`) + REFERENCES `identity_providers` (`identity_provider_id`) + ON DELETE CASCADE ON UPDATE RESTRICT + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Maps server hostnames to identity providers (many-to-many).' + } + ); + say_success( $out, "Added new table 'identity_provider_hostnames'" ); + } + + # ── 8. Migrate Shibboleth config from kha-conf.xml to identity_providers ────────────────────── + # Reads useshibboleth from koha-conf.xml and migrates it to a DB syspref. + # Also migrates the mapping config to a SAML2 identity provider. + + { + require C4::Context; + my $use_shib = C4::Context->config('useshibboleth') // 0; + + if ($use_shib) { + my $shib_cfg = C4::Context->config('shibboleth'); + unless ($shib_cfg) { + say_warning( + $out, + "useshibboleth=1 in koha-conf.xml but no section found; " + . "please configure the SAML2 identity provider manually" + ); + } else { + + # Check if a SAML2 provider already exists (idempotent) + my ($saml2_id) = $dbh->selectrow_array( + "SELECT identity_provider_id FROM identity_providers + WHERE protocol = 'SAML2' LIMIT 1" + ); + + unless ($saml2_id) { + my $config = encode_json( + { + autocreate => $shib_cfg->{autocreate} ? 1 : 0, + sync => $shib_cfg->{sync} ? 1 : 0, + welcome => $shib_cfg->{welcome} ? 1 : 0, + } + ); + + $dbh->do( + q{ + INSERT INTO identity_providers + (code, description, protocol, config, enabled) + VALUES ('shibboleth', 'Shibboleth (migrated from koha-conf.xml)', 'SAML2', ?, 1) + }, undef, $config + ); + $saml2_id = $dbh->last_insert_id( undef, undef, 'identity_providers', undef ); + say_success( + $out, + "Migrated Shibboleth config to identity_providers (id=$saml2_id)" + ); + + # Create a default wildcard domain entry for the Shibboleth provider + $dbh->do( + q{ + INSERT IGNORE INTO identity_provider_domains + (identity_provider_id, domain, allow_opac, allow_staff, + auto_register_opac, auto_register_staff, update_on_auth, send_welcome_email) + VALUES (?, NULL, 1, 1, ?, 0, ?, ?) + }, undef, + $saml2_id, + $shib_cfg->{autocreate} ? 1 : 0, + $shib_cfg->{sync} ? 1 : 0, + $shib_cfg->{welcome} ? 1 : 0, + ); + say_success( + $out, + "Created default domain entry for Shibboleth provider" + ); + } + + # Migrate field mappings from the section + if ( $saml2_id && $shib_cfg->{mapping} && ref $shib_cfg->{mapping} eq 'HASH' ) { + my $matchpoint = $shib_cfg->{matchpoint}; + my $mapped = 0; + + for my $koha_field ( keys %{ $shib_cfg->{mapping} } ) { + my $entry = $shib_cfg->{mapping}{$koha_field}; + + # Entry may be a hashref with {is => 'attr'} or just a string + my $provider_field = ref $entry eq 'HASH' ? $entry->{is} : $entry; + my $is_matchpoint = ( defined $matchpoint && $matchpoint eq $koha_field ) ? 1 : 0; + + $dbh->do( + q{ + INSERT IGNORE INTO identity_provider_mappings + (identity_provider_id, provider_field, koha_field, is_matchpoint) + VALUES (?, ?, ?, ?) + }, undef, + $saml2_id, $provider_field, $koha_field, $is_matchpoint + ); + $mapped++; + } + + say_success( + $out, + "Migrated $mapped Shibboleth field mapping(s) to identity_provider_mappings" + ); + } + } + } + } + + # ── 9. Migrate OPACShibOnly / staffShibOnly to hostname-based force_sso ─── + # For each shibOnly syspref that is ON, create a force_sso=1 entry in the + # identity_provider_hostnames table using the corresponding base URL syspref + # to determine the hostname. + + my %shib_only_map = ( + OPACShibOnly => 'OPACBaseURL', + staffShibOnly => 'staffClientBaseURL', + ); + + for my $pref_name ( sort keys %shib_only_map ) { + my ($shib_only) = $dbh->selectrow_array( + "SELECT value FROM systempreferences WHERE variable = ?", + undef, $pref_name + ); + next unless $shib_only; + + my ($saml2_id) = $dbh->selectrow_array( + "SELECT identity_provider_id FROM identity_providers + WHERE protocol = 'SAML2' LIMIT 1" + ); + unless ($saml2_id) { + say_warning( + $out, + "'$pref_name' is enabled but no SAML2 provider found; " + . "please configure force_sso manually after adding a SAML2 provider" + ); + next; + } + + my $url_pref = $shib_only_map{$pref_name}; + my ($url) = $dbh->selectrow_array( + "SELECT value FROM systempreferences WHERE variable = ?", + undef, $url_pref + ); + unless ( $url && $url =~ m{^https?://([^/:]+)} ) { + say_warning( + $out, + "'$pref_name' is enabled but '$url_pref' is not set or not a valid URL; " + . "please configure force_sso manually" + ); + next; + } + my $hostname = $1; + + $dbh->do( + "INSERT IGNORE INTO hostnames (hostname) VALUES (?)", + undef, $hostname + ); + my ($hostname_id) = $dbh->selectrow_array( + "SELECT hostname_id FROM hostnames WHERE hostname = ?", + undef, $hostname + ); + + $dbh->do( + q{ + INSERT IGNORE INTO identity_provider_hostnames + (hostname_id, identity_provider_id, is_enabled, force_sso) + VALUES (?, ?, 1, 1) + }, undef, $hostname_id, $saml2_id + ); + say_success( + $out, + "Enabled force_sso for hostname '$hostname' (migrated from $pref_name)" + ); + } + + $dbh->do("DELETE FROM systempreferences WHERE variable IN ('OPACShibOnly', 'staffShibOnly')"); + say_success( + $out, + "Removed system preferences 'OPACShibOnly' and 'staffShibOnly'" + ); + + return 1; + }, +}; diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index 1222ef6387c..4aad38fea6f 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -3642,6 +3642,21 @@ CREATE TABLE `housebound_visit` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `hostnames` +-- + +DROP TABLE IF EXISTS `hostnames`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8mb4 */; +CREATE TABLE `hostnames` ( + `hostname_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Unique identifier for this hostname', + `hostname` varchar(255) NOT NULL COMMENT 'Server hostname string', + PRIMARY KEY (`hostname_id`), + UNIQUE KEY `hostname` (`hostname`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Canonical hostname registry for identity provider selection'; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `identity_provider_domains` -- @@ -3660,6 +3675,7 @@ CREATE TABLE `identity_provider_domains` ( `allow_staff` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Allow provider from staff interface', `auto_register_opac` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Allow user auto register (OPAC)', `auto_register_staff` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Allow user auto register (Staff interface)', + `send_welcome_email` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Send welcome email to patron on first login', PRIMARY KEY (`identity_provider_domain_id`), UNIQUE KEY `identity_provider_id` (`identity_provider_id`,`domain`), KEY `domain` (`domain`), @@ -3673,6 +3689,48 @@ CREATE TABLE `identity_provider_domains` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `identity_provider_hostnames` +-- + +DROP TABLE IF EXISTS `identity_provider_hostnames`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8mb4 */; +CREATE TABLE `identity_provider_hostnames` ( + `identity_provider_hostname_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'unique key, used to identify the hostname entry', + `hostname_id` int(11) NOT NULL COMMENT 'FK to hostnames table', + `identity_provider_id` int(11) NOT NULL COMMENT 'Identity provider associated with this hostname', + `is_enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Whether this hostname is active for this provider', + `force_sso` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Force SSO redirect for users on this hostname', + PRIMARY KEY (`identity_provider_hostname_id`), + UNIQUE KEY `hostname_id_provider` (`hostname_id`,`identity_provider_id`), + KEY `idp_hostname_provider_idx` (`identity_provider_id`), + CONSTRAINT `fk_iph_hostname` FOREIGN KEY (`hostname_id`) REFERENCES `hostnames` (`hostname_id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `idp_hostname_ibfk_1` FOREIGN KEY (`identity_provider_id`) REFERENCES `identity_providers` (`identity_provider_id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Maps server hostnames to identity providers (many-to-many). A hostname may be linked to multiple providers.'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `identity_provider_mappings` +-- + +DROP TABLE IF EXISTS `identity_provider_mappings`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8mb4 */; +CREATE TABLE `identity_provider_mappings` ( + `mapping_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key', + `identity_provider_id` int(11) NOT NULL COMMENT 'Reference to identity provider', + `provider_field` varchar(255) DEFAULT NULL COMMENT 'Attribute name from the identity provider', + `koha_field` varchar(255) NOT NULL COMMENT 'Corresponding field in Koha borrowers table', + `default_content` varchar(255) DEFAULT NULL COMMENT 'Default value if provider does not supply this field', + `is_matchpoint` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Use this field to match existing patrons', + PRIMARY KEY (`mapping_id`), + UNIQUE KEY `provider_koha_field` (`identity_provider_id`,`koha_field`), + KEY `provider_field_idx` (`provider_field`), + CONSTRAINT `idp_mapping_ibfk_1` FOREIGN KEY (`identity_provider_id`) REFERENCES `identity_providers` (`identity_provider_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `identity_providers` -- @@ -3684,10 +3742,9 @@ CREATE TABLE `identity_providers` ( `identity_provider_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'unique key, used to identify the provider', `code` varchar(20) NOT NULL COMMENT 'Provider code', `description` varchar(255) NOT NULL COMMENT 'Description for the provider', - `protocol` enum('OAuth','OIDC','LDAP','CAS') NOT NULL COMMENT 'Protocol provider speaks', + `protocol` enum('OAuth','OIDC','SAML2') NOT NULL COMMENT 'Protocol provider speaks', `config` longtext NOT NULL COMMENT 'Configuration of the provider in JSON format', - `mapping` longtext NOT NULL COMMENT 'Configuration to map provider data to Koha user', - `matchpoint` enum('email','userid','cardnumber') NOT NULL COMMENT 'The patron attribute to be used as matchpoint', + `enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Whether this provider is active', `icon_url` varchar(255) DEFAULT NULL COMMENT 'Provider icon URL', PRIMARY KEY (`identity_provider_id`), UNIQUE KEY `code` (`code`), diff --git a/installer/data/mysql/mandatory/sysprefs.sql b/installer/data/mysql/mandatory/sysprefs.sql index 585d7a5d8d8..174170dc3ce 100644 --- a/installer/data/mysql/mandatory/sysprefs.sql +++ b/installer/data/mysql/mandatory/sysprefs.sql @@ -573,7 +573,6 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('opacSerialDefaultTab','subscriptions','holdings|serialcollection|subscriptions|titlenotes','Define the default tab for serials in OPAC.','Choice'), ('OPACSerialIssueDisplayCount','3',NULL,'Number of serial issues to display per subscription in the OPAC','Integer'), ('OPACShelfBrowser','1',NULL,'Enable/disable Shelf Browser on item details page. WARNING: this feature is very resource consuming on collections with large numbers of items.','YesNo'), -('OPACShibOnly','0',NULL,'If ON enables shibboleth only authentication for the opac','YesNo'), ('OPACShowCheckoutName','0',NULL,'Displays in the OPAC the name of patron who has checked out the material. WARNING: Most sites should leave this off. It is intended for corporate or special sites which need to track who has the item.','YesNo'), ('OPACShowHoldQueueDetails','none','none|priority|holds|holds_priority','Show holds details in OPAC','Choice'), ('OPACShowLibraries', '1', NULL, 'If enabled, a "Libraries" link appears in the OPAC pointing to a page with library information', 'YesNo'), @@ -782,7 +781,6 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('StaffLoginRestrictLibraryByIP','0',NULL,'If ON, IP authentication is enabled, blocking access to the staff interface from unauthorized IP addresses based on branch','YesNo'), ('StaffSearchResultsDisplayBranch','holdingbranch','holdingbranch|homebranch','Controls the display of the home or holding branch for staff search results','Choice'), ('StaffSerialIssueDisplayCount','3',NULL,'Number of serial issues to display per subscription in the staff interface','Integer'), -('staffShibOnly','0',NULL,'If ON enables shibboleth only authentication for the staff client','YesNo'), ('StaticHoldsQueueWeight','0',NULL,'Specify a list of library location codes separated by commas -- the list of codes will be traversed and weighted with first values given higher weight for holds fulfillment -- alternatively, if RandomizeHoldsQueueWeight is set, the list will be randomly selective','Integer'), ('StatisticsFields','location|itype|ccode', NULL, 'Define Fields (from the items table) used for statistics members','Free'), ('StockRotation','0',NULL,'If ON, enables the stock rotation module','YesNo'), -- 2.53.0