diff --git a/config/vufind/Folio.ini b/config/vufind/Folio.ini index a355a4faca4..912cea29f91 100644 --- a/config/vufind/Folio.ini +++ b/config/vufind/Folio.ini @@ -97,6 +97,12 @@ password_field = false ; %%username_field%% = The username_field config setting (above) ; %%password_field%% = The password_field config setting (above) ;cql = '%%username_field%% == "%%username%%" and %%password_field%% == "%%password%%"' +; The `cql_by_auth_method` setting allows you to specify different CQL queries +; for different VuFind authentication methods. The keys should be the names +; of the authentication methods as defined in your VuFind configuration, and +; the values should be CQL queries that can use the same placeholders as +; described above for the general `cql` setting. For instance, for Shibboleth: +;cql_by_auth_method[Shibboleth] = 'externalSystemId == "%%username%%"' ; Should we try to log the user into the Okapi API (true) or just look them ; up in the database using [API] credentials above (false). If set to true, @@ -105,6 +111,10 @@ okapi_login = true ; Should we override the Okapi token created using [API] credentials with the ; user's credentials after they log in? (Only valid if okapi_login = true) use_user_token = false +; If set to true, `debug_login` will log the CQL query (with password redated), +; the environment variables, and the response from the FOLIO `/users` API call. +;debug_login = false + ; This section controls hold behavior; note that you must also ensure that Holds are ; enabled in the [Catalog] section of config.ini in order to take advantage of these diff --git a/module/VuFind/src/VuFind/ILS/Driver/Folio.php b/module/VuFind/src/VuFind/ILS/Driver/Folio.php index aa3b5b68f2f..def73ea4a54 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/Folio.php +++ b/module/VuFind/src/VuFind/ILS/Driver/Folio.php @@ -115,6 +115,13 @@ class Folio extends AbstractAPI implements */ protected $dateConverter; + /** + * Auth method + * + * @var string + */ + protected $authMethod = null; + /** * Default availability messages, in case they are not defined in Folio.ini * @@ -145,13 +152,16 @@ class Folio extends AbstractAPI implements * @param \VuFind\Date\Converter $dateConverter Date converter object * @param callable $sessionFactory Factory function returning * SessionContainer object + * @param str $authMethod Authentication method */ public function __construct( \VuFind\Date\Converter $dateConverter, - $sessionFactory + $sessionFactory, + string $authMethod ) { $this->dateConverter = $dateConverter; $this->sessionFactory = $sessionFactory; + $this->authMethod = $authMethod; } /** @@ -1270,6 +1280,41 @@ protected function patronLoginWithOkapi($username, $password) return $query; } + /** + * Get the CQL query template for retrieving the patron's information. + * + * This supports both a single CQL query configured for all auth methods, and + * method-specific CQL queries, which can be configured by including the auth + * method name as a key in the `cql_by_auth_method` array in the User section of + * the config file. If method-specific CQL is configured, the driver will use + * the query for the selected auth method if it exists, and fall back to the + * general CQL query if not. If neither is configured, it will fall back to a + * default query that looks for the username and password in the fields + * specified by username_field and password_field in the config file, or username + * and password if those fields are not specified. + * + * @param string $usernameField The field to use for the username in the CQL query + * @param string $passwordField The field to use for password in the CQL query + * + * @return string The patron lookup CQL query with placeholders. + */ + protected function getLoginCql($usernameField, $passwordField) + { + $cql = $this->config['User']['cql_by_auth_method'][$this->authMethod] + ?? $this->config['User']['cql'] + ?? '%%username_field%% == "%%username%%"' . + ($passwordField ? ' and %%password_field%% == "%%password%%"' : ''); + $placeholders = [ + '%%username_field%%', + '%%password_field%%', + ]; + $values = [ + $usernameField, + $passwordField, + ]; + return str_replace($placeholders, $values, $cql); + } + /** * Support method for patronLogin(): authenticate the patron with a CQL looup. * Returns the CQL query for retrieving more information about the user. @@ -1284,28 +1329,32 @@ protected function getUserWithCql($username, $password) // Construct user query using barcode, username, etc. $usernameField = $this->config['User']['username_field'] ?? 'username'; $passwordField = $this->config['User']['password_field'] ?? false; - $cql = $this->config['User']['cql'] - ?? '%%username_field%% == "%%username%%"' - . ($passwordField ? ' and %%password_field%% == "%%password%%"' : ''); + $cql = $this->getLoginCql($usernameField, $passwordField); $placeholders = [ - '%%username_field%%', - '%%password_field%%', '%%username%%', '%%password%%', ]; $values = [ - $usernameField, - $passwordField, $this->escapeCql($username), $this->escapeCql($password), ]; - return str_replace($placeholders, $values, $cql); + $cql_expanded = str_replace($placeholders, $values, $cql); + + $cql_redacted = str_replace($this->escapeCql($password), 'XXXX', $cql_expanded); + if ($this->config['User']['debug_login'] ?? false) { + $this->debug('FOLIO patron login CQL: ' . $cql_redacted); + $this->debug('Environment: ' . json_encode($_SERVER)); + } + return $cql_expanded; } /** * Given a CQL query, fetch a single user; if we get an unexpected count, treat * that as an unsuccessful login by returning null. * + * Note that the `json_decode` then the `json_encode` in the debug statement + * is used to ensure that the JSON output appears in on line of the log file. + * * @param string $query CQL query * * @return object @@ -1314,6 +1363,9 @@ protected function fetchUserWithCql($query) { $response = $this->makeRequest('GET', '/users', compact('query')); $json = json_decode($response->getBody()); + if ($this->config['User']['debug_login'] ?? false) { + $this->debug('FOLIO response body: ' . json_encode($json)); + } return count($json->users ?? []) === 1 ? $json->users[0] : null; } diff --git a/module/VuFind/src/VuFind/ILS/Driver/FolioFactory.php b/module/VuFind/src/VuFind/ILS/Driver/FolioFactory.php index f589d1ae878..58787f28d89 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/FolioFactory.php +++ b/module/VuFind/src/VuFind/ILS/Driver/FolioFactory.php @@ -71,6 +71,8 @@ public function __invoke( $manager = $container->get(\Laminas\Session\SessionManager::class); return new \Laminas\Session\Container("Folio_$namespace", $manager); }; - return parent::__invoke($container, $requestedName, [$sessionFactory]); + $authManager = $container->get(\VuFind\Auth\Manager::class); + $selectedAuthMethod = $authManager->getSelectedAuthMethod(); + return parent::__invoke($container, $requestedName, [$sessionFactory, $selectedAuthMethod]); } } diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/FolioTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/FolioTest.php index 3ae58c6ddd8..2dd30d13d4e 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/FolioTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/FolioTest.php @@ -172,7 +172,7 @@ protected function createConnector(string $test, ?array $config = null): void }; // Create a stub for the SomeClass class $this->driver = $this->getMockBuilder(Folio::class) - ->setConstructorArgs([new \VuFind\Date\Converter(), $factory]) + ->setConstructorArgs([new \VuFind\Date\Converter(), $factory, 'fake_auth_method']) ->onlyMethods(['makeRequest']) ->getMock(); // Configure the stub