diff --git a/module/finc/src/finc/ILS/Driver/FincILS.php b/module/finc/src/finc/ILS/Driver/FincILS.php index e8989d339b4df54be9889a83682df51a3a581d96..0c147c2aa44fcfdecaf991d4a0df675cd339babb 100644 --- a/module/finc/src/finc/ILS/Driver/FincILS.php +++ b/module/finc/src/finc/ILS/Driver/FincILS.php @@ -26,8 +26,7 @@ * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki */ namespace finc\ILS\Driver; -use VuFind\Exception\ILS as ILSException, - VuFindSearch\Query\Query, VuFindSearch\Service as SearchService, +use VuFindSearch\Query\Query, VuFindSearch\Service as SearchService, ZfcRbac\Service\AuthorizationServiceAwareInterface, ZfcRbac\Service\AuthorizationServiceAwareTrait, Zend\Log\LoggerAwareInterface as LoggerAwareInterface, @@ -507,8 +506,10 @@ class FincILS extends PAIA implements LoggerAwareInterface $password, $username ); - } catch (ILSException $e) { - $this->debug('Session expired, login again', 'info'); + } catch (Exception $e) { + // TODO? $this->debug('Session expired, login again', 'info'); + // all error handling is done in paiaHandleErrors so pass on the excpetion + throw $e; } } @@ -520,8 +521,9 @@ class FincILS extends PAIA implements LoggerAwareInterface $username ); } - } catch (ILSException $e) { - throw new ILSException($e->getMessage()); + } catch (Exception $e) { + // all error handling is done in paiaHandleErrors so pass on the excpetion + throw $e; } } else { return parent::patronLogin($username, $password); @@ -568,9 +570,15 @@ class FincILS extends PAIA implements LoggerAwareInterface } if (!isset($itemsResponse) || $itemsResponse == null) { - $itemsResponse = $this->paiaGetAsArray( - 'core/'.$patron['cat_username'].'/items' - ); + try { + $itemsResponse = $this->paiaGetAsArray( + 'core/'.$patron['cat_username'].'/items' + ); + } catch (Exception $e) { + // all error handling is done in paiaHandleErrors so pass on the excpetion + throw $e; + } + if ($this->paiaCacheEnabled) { $this->putCachedData($patron['cat_username'] . '_items', $itemsResponse); } diff --git a/module/finc/src/finc/ILS/Driver/PAIA.php b/module/finc/src/finc/ILS/Driver/PAIA.php index 4e402a69116f4d0e3b9892cff9a0bfbd68a25431..4bb7097b646b2b0af0af68a89f05184fbbf7a82d 100644 --- a/module/finc/src/finc/ILS/Driver/PAIA.php +++ b/module/finc/src/finc/ILS/Driver/PAIA.php @@ -31,7 +31,8 @@ */ namespace finc\ILS\Driver; -use VuFind\Exception\ILS as ILSException; +use VuFind\Exception\Auth as AuthException, + VuFind\Exception\ILS as ILSException; /** * PAIA ILS Driver for VuFind to get patron information @@ -99,6 +100,19 @@ class PAIA extends DAIA '5' => 'rejected', ]; + /** + * PAIA scopes as defined in http://gbv.github.io/paia/paia.html#access-tokens-and-scopes + */ + const SCOPE_READ_PATRON = 'read_patron'; + const SCOPE_UPDATE_PATRON = 'update_patron'; + const SCOPE_UPDATE_PATRON_NAME = 'update_patron_name'; + const SCOPE_UPDATE_PATRON_EMAIL = 'update_patron_email'; + const SCOPE_UPDATE_PATRON_ADDRESS = 'update_patron_address'; + const SCOPE_READ_FEES = 'read_fees'; + const SCOPE_READ_ITEMS = 'read_items'; + const SCOPE_WRITE_ITEMS = 'write_items'; + const SCOPE_CHANGE_PASSWORD = 'change_password'; + /** * Constructor * @@ -242,6 +256,11 @@ class PAIA extends DAIA */ public function cancelHolds($cancelDetails) { + // check if user has appropriate scope + if (!$this->paiaCheckScope(self::SCOPE_WRITE_ITEMS)) { + throw new ILSException('You are not entitled to write items.'); + } + $it = $cancelDetails['details']; $items = []; foreach ($it as $item) { @@ -254,7 +273,7 @@ class PAIA extends DAIA $array_response = $this->paiaPostAsArray( 'core/'.$patron['cat_username'].'/cancel', $post_data ); - } catch (ILSException $e) { + } catch (Exception $e) { $this->debug($e->getMessage()); return [ 'success' => false, @@ -319,6 +338,11 @@ class PAIA extends DAIA */ public function changePassword($details) { + // check if user has appropriate scope + if (!$this->paiaCheckScope(self::SCOPE_CHANGE_PASSWORD)) { + throw new ILSException('You are not entitled to change password.'); + } + $post_data = [ "patron" => $details['patron']['cat_username'], "username" => $details['patron']['cat_username'], @@ -330,7 +354,7 @@ class PAIA extends DAIA $array_response = $this->paiaPostAsArray( 'auth/change', $post_data ); - } catch (ILSException $e) { + } catch (Exception $e) { $this->debug($e->getMessage()); return [ 'success' => false, @@ -431,9 +455,19 @@ class PAIA extends DAIA */ public function getMyFines($patron) { - $fees = $this->paiaGetAsArray( - 'core/'.$patron['cat_username'].'/fees' - ); + // check if user has appropriate scope + if (!$this->paiaCheckScope(self::SCOPE_READ_FEES)) { + throw new ILSException('You are not entitled to read fees.'); + } + + try { + $fees = $this->paiaGetAsArray( + 'core/'.$patron['cat_username'].'/fees' + ); + } catch (Exception $e) { + // all error handling is done in paiaHandleErrors so pass on the excpetion + throw $e; + } // PAIA simple data type money: a monetary value with currency (format // [0-9]+\.[0-9][0-9] [A-Z][A-Z][A-Z]), for instance 0.80 USD. @@ -556,7 +590,7 @@ class PAIA extends DAIA 'expires' => isset($patron['expires']) ? $this->convertDate($patron['expires']) : null, 'statuscode' => isset($patron['status']) ? $patron['status'] : null, - 'canWrite' => in_array('write_items', $this->getSession()->scope), + 'canWrite' => in_array(self::SCOPE_WRITE_ITEMS, $this->getSession()->scope), ]; } return []; @@ -692,8 +726,10 @@ class PAIA extends DAIA $this->paiaGetUserDetails($session->patron), $password ); - } catch (ILSException $e) { - $this->debug('Session expired, login again', ['info' => 'info']); + } catch (Exception $e) { + // TODO? $this->debug('Session expired, login again', ['info' => 'info']); + // all error handling is done in paiaHandleErrors so pass on the excpetion + throw $e; } } try { @@ -703,8 +739,71 @@ class PAIA extends DAIA $password ); } - } catch (ILSException $e) { - throw new ILSException($e->getMessage()); + } catch (Exception $e) { + // all error handling is done in paiaHandleErrors so pass on the excpetion + throw $e; + } + } + + /** + * Handle PAIA request errors and throw appropriate exception. + * + * @param array $error Array containing error messages + * @throws AuthException + * @throws ILSException + */ + protected function paiaHandleErrors($array) + { + // TODO: also have exception contain content of 'error' as for at least + // error code 403 two differing errors are possible + // (cf. http://gbv.github.io/paia/paia.html#request-errors) + if (isset($array['error'])) { + switch ($array['error']) { + // cf. http://gbv.github.io/paia/paia.html#request-errors + // error code error_description + // access_denied 403 Wrong or missing credentials to get an access token + case 'access_denied': + throw new AuthException( + isset($array['error_description']) + ? $array['error_description'] : $array['error'], + isset($array['code']) ? $array['code'] : '' + ); + // not_found 404 Unknown request URL or unknown patron. Implementations SHOULD first check authentication and prefer error invalid_grant or access_denied to prevent leaking patron identifiers. + case 'not_found': + + // not_implemented 501 Known but unsupported request URL (for instance a PAIA auth server server may not implement http://example.org/core/change) + case 'not_implemented': + + // invalid_request 405 Unexpected HTTP verb + // invalid_request 400 Malformed request (for instance error parsing JSON, unsupported request content type, etc.) + // invalid_request 422 The request parameters could be parsed but they don’t match the request method (for instance missing fields, invalid values, etc.) + case 'invalid_request': + + // invalid_grant 401 The access token was missing, invalid, or expired + case 'invalid_grant': + + // insufficient_scope 403 The access token was accepted but it lacks permission for the request + case 'insufficient_scope': + + // internal_error 500 An unexpected error occurred. This error corresponds to a bug in the implementation of a PAIA auth/core server + case 'internal_error': + + // service_unavailable 503 The request couldn’t be serviced because of a temporary failure + case 'service_unavailable': + + // bad_gateway 502 The request couldn’t be serviced because of a backend failure (for instance the library system’s database) + case 'bad_gateway': + + // gateway_timeout 504 The request couldn’t be serviced because of a backend failure' + case 'gateway_timeout': + + default: + throw new ILSException( + isset($array['error_description']) + ? $array['error_description'] : $array['error'], + isset($array['code']) ? $array['code'] : '' + ); + } } } @@ -760,6 +859,11 @@ class PAIA extends DAIA */ public function placeHold($holdDetails) { + // check if user has appropriate scope + if (!$this->paiaCheckScope(self::SCOPE_WRITE_ITEMS)) { + throw new ILSException('You are not entitled to write items.'); + } + $item = $holdDetails['item_id']; $patron = $holdDetails['patron']; @@ -774,7 +878,7 @@ class PAIA extends DAIA $array_response = $this->paiaPostAsArray( 'core/'.$patron['cat_username'].'/request', $post_data ); - } catch (ILSException $e) { + } catch (Exception $e) { $this->debug($e->getMessage()); return [ 'success' => false, @@ -852,6 +956,11 @@ class PAIA extends DAIA */ public function renewMyItems($details) { + // check if user has appropriate scope + if (!$this->paiaCheckScope(self::SCOPE_WRITE_ITEMS)) { + throw new ILSException('You are not entitled to write items.'); + } + $it = $details['details']; $items = []; foreach ($it as $item) { @@ -864,7 +973,7 @@ class PAIA extends DAIA $array_response = $this->paiaPostAsArray( 'core/'.$patron['cat_username'].'/renew', $post_data ); - } catch (ILSException $e) { + } catch (Exception $e) { $this->debug($e->getMessage()); return [ 'success' => false, @@ -953,15 +1062,25 @@ class PAIA extends DAIA */ protected function paiaGetItems($patron, $filter = []) { + // check if user has appropriate scope + if (!$this->paiaCheckScope(self::SCOPE_READ_ITEMS)) { + throw new ILSException('You are not entitled to read items.'); + } + // check for existing data in cache if ($this->paiaCacheEnabled) { $itemsResponse = $this->getCachedData($patron['cat_username'] . '_items'); } if (!isset($itemsResponse) || $itemsResponse == null) { - $itemsResponse = $this->paiaGetAsArray( - 'core/'.$patron['cat_username'].'/items' - ); + try { + $itemsResponse = $this->paiaGetAsArray( + 'core/'.$patron['cat_username'].'/items' + ); + } catch (Exception $e) { + // all error handling is done in paiaHandleErrors so pass on the excpetion + throw $e; + } if ($this->paiaCacheEnabled) { $this->putCachedData($patron['cat_username'] . '_items', $itemsResponse); } @@ -1092,7 +1211,7 @@ class PAIA extends DAIA $result['item_id'] = (isset($doc['item']) ? $doc['item'] : ''); $result['cancel_details'] - = (isset($doc['cancancel']) && $doc['cancancel']) + = (isset($doc['cancancel']) && $doc['cancancel'] && $this->paiaCheckScope(self::SCOPE_WRITE_ITEMS)) ? $result['item_id'] : ''; // edition (0..1) URI of a the document (no particular copy) @@ -1181,7 +1300,7 @@ class PAIA extends DAIA $result['item_id'] = (isset($doc['item']) ? $doc['item'] : ''); $result['cancel_details'] - = (isset($doc['cancancel']) && $doc['cancancel']) + = (isset($doc['cancancel']) && $doc['cancancel'] && $this->paiaCheckScope(self::SCOPE_WRITE_ITEMS)) ? $result['item_id'] : ''; // edition (0..1) URI of a the document (no particular copy) @@ -1242,14 +1361,14 @@ class PAIA extends DAIA foreach ($items as $doc) { $result = []; // canrenew (0..1) whether a document can be renewed (bool) - $result['renewable'] = (isset($doc['canrenew']) - ? $doc['canrenew'] : false); + $result['renewable'] = (isset($doc['canrenew']) && $this->paiaCheckScope(self::SCOPE_WRITE_ITEMS)) + ? $doc['canrenew'] : false; // item (0..1) URI of a particular copy $result['item_id'] = (isset($doc['item']) ? $doc['item'] : ''); $result['renew_details'] - = (isset($doc['canrenew']) && $doc['canrenew']) + = (isset($doc['canrenew']) && $doc['canrenew'] && $this->paiaCheckScope(self::SCOPE_WRITE_ITEMS)) ? $result['item_id'] : ''; // edition (0..1) URI of a the document (no particular copy) @@ -1411,11 +1530,14 @@ class PAIA extends DAIA { $responseArray = json_decode($file, true); + // if we have an error response handle it accordingly (any will throw an + // exception at the moment) and pass on the resulting exception if (isset($responseArray['error'])) { - throw new ILSException( - $responseArray['error'], - $responseArray['code'] - ); + try { + $this->paiaHandleErrors($responseArray); + } catch (Exception $e) { + throw $e; + } } return $responseArray; @@ -1438,9 +1560,9 @@ class PAIA extends DAIA try { $responseArray = $this->paiaParseJsonAsArray($responseJson); - } catch (ILSException $e) { - $this->debug($e->getCode() . ':' . $e->getMessage()); - return []; + } catch (Exception $e) { + // all error handling is done in paiaHandleErrors so pass on the excpetion + throw $e; } return $responseArray; @@ -1466,9 +1588,8 @@ class PAIA extends DAIA try { $responseArray = $this->paiaParseJsonAsArray($responseJson); } catch (ILSException $e) { - $this->debug($e->getCode() . ':' . $e->getMessage()); - /* TODO: do not return empty array, this causes eventually confusion */ - return []; + // all error handling is done in paiaHandleErrors so pass on the excpetion + throw $e; } return $responseArray; @@ -1491,20 +1612,19 @@ class PAIA extends DAIA "username" => $username, "password" => $password, "grant_type" => "password", - "scope" => "read_patron read_fees read_items write_items " . - "change_password" + "scope" => self::SCOPE_READ_PATRON . " " . + self::SCOPE_READ_FEES . " " . + self::SCOPE_READ_ITEMS . " " . + self::SCOPE_WRITE_ITEMS . " " . + self::SCOPE_CHANGE_PASSWORD ]; $responseJson = $this->paiaPostRequest('auth/login', $post_data); try { $responseArray = $this->paiaParseJsonAsArray($responseJson); - } catch (ILSException $e) { - if ($e->getMessage() === 'access_denied') { - return false; - } - throw new ILSException( - $e->getCode() . ':' . $e->getMessage() - ); + } catch (Exception $e) { + // all error handling is done in paiaHandleErrors so pass on the excpetion + throw $e; } if (!isset($responseArray['access_token'])) { @@ -1548,6 +1668,11 @@ class PAIA extends DAIA */ protected function paiaGetUserDetails($patron) { + // check if user has appropriate scope + if (!$this->paiaCheckScope(self::SCOPE_READ_PATRON)) { + throw new ILSException('You are not entitled to read patron.'); + } + $responseJson = $this->paiaGetRequest( 'core/' . $patron, $this->getSession()->access_token ); @@ -1555,13 +1680,23 @@ class PAIA extends DAIA try { $responseArray = $this->paiaParseJsonAsArray($responseJson); } catch (ILSException $e) { - throw new ILSException( - $e->getMessage(), $e->getCode() - ); + // all error handling is done in paiaHandleErrors so pass on the excpetion + throw $e; } return $this->paiaParseUserDetails($patron, $responseArray); } + /** + * Checks if the current scope is set for active session. + * + * @return boolean + */ + protected function paiaCheckScope($scope) + { + return (!empty($scope) && is_array($this->getSession()->scope)) + ? in_array($scope, $this->getSession()->scope) : false; + } + /** * Check if storage retrieval request available * @@ -1599,7 +1734,7 @@ class PAIA extends DAIA if ( isset($patron['status']) && $patron['status'] == 0 && isset($patron['expires']) && $patron['expires'] > date('Y-m-d') - && in_array('write_items', $this->getSession()->scope) + && in_array(self::SCOPE_WRITE_ITEMS, $this->getSession()->scope) ) { return true; }