Source code for api.firstbook

from flask_babel import lazy_gettext as _
import requests
import logging
from .authenticator import (
    BasicAuthenticationProvider,
    PatronData,
)
from .config import (
    Configuration,
    CannotLoadConfiguration,
)
from .circulation_exceptions import RemoteInitiatedServerError
import urllib.parse
from core.model import (
    get_one_or_create,
    ExternalIntegration,
    Patron,
)


[docs]class FirstBookAuthenticationAPI(BasicAuthenticationProvider): NAME = 'First Book (New 8/2022)' DESCRIPTION = _(""" An authentication service for Open eBooks that authenticates using access codes and PINs. (This is the new version as of 8/2022.)""") DISPLAY_NAME = NAME DEFAULT_IDENTIFIER_LABEL = _("Access Code") LOGIN_BUTTON_IMAGE = "FirstBookLoginButton280.png" # If FirstBook sends this message it means they accepted the # patron's credentials. SUCCESS_MESSAGE = 'Valid Code Pin Pair' # Server-side validation happens before the identifier # is converted to uppercase, which means lowercase characters # are valid. DEFAULT_IDENTIFIER_REGULAR_EXPRESSION = '^[A-Za-z0-9@]+$' DEFAULT_PASSWORD_REGULAR_EXPRESSION = '^[0-9]+$' API_PATH = 'rest/V1/serialcode?' SETTINGS = [ {"key": ExternalIntegration.URL, "format": "url", "label": _("URL"), "required": True}, {"key": ExternalIntegration.PASSWORD, "label": _("Key"), "required": True}, ] + BasicAuthenticationProvider.SETTINGS log = logging.getLogger("First Book authentication API") def __init__(self, library_id, integration, analytics=None, root=None, secret=None): super(FirstBookAuthenticationAPI, self).__init__( library_id, integration, analytics) self.key = secret or integration.password if not root: root = integration.url if not (root and self.key): raise CannotLoadConfiguration( "First Book server not configured." ) self.root = root # Begin implementation of BasicAuthenticationProvider abstract # methods.
[docs] def remote_authenticate(self, username, password): # All FirstBook credentials are in upper-case. username = username.upper() # If they fail a PIN test, there is no authenticated patron. if not self.remote_pin_test(username, password): return None # FirstBook keeps track of absolutely no information # about the patron other than the permanent ID, # which is also the authorization identifier. return PatronData( permanent_id=username, authorization_identifier=username, )
# End implementation of BasicAuthenticationProvider abstract methods.
[docs] def remote_pin_test(self, barcode, pin): url = self.root + self.API_PATH + "code=%s&pin=%s" % (barcode, pin) header = {'Authorization': 'Bearer %s' % self.key} try: response = self.request(url, header) except requests.exceptions.ConnectionError as e: raise RemoteInitiatedServerError( str(e), self.NAME ) content = response.content.decode("utf8") if response.status_code != 200: msg = "Got unexpected response code %d. Content: %s" % ( response.status_code, content ) raise RemoteInitiatedServerError(msg, self.NAME) if self.SUCCESS_MESSAGE in content: return True return False
[docs] def request(self, url, header={}): """Make an HTTP request. Defined to be overridden in test mock. """ return requests.get(url, headers=header)
[docs]class MockFirstBookResponse(object): def __init__(self, status_code, content): self.status_code = status_code # Guarantee that the response content is always a bytestring, # as it would be in real life. if isinstance(content, str): content = content.encode("utf8") self.content = content
[docs]class MockFirstBookAuthenticationAPI(FirstBookAuthenticationAPI): SUCCESS = '"Valid Code Pin Pair"' FAILURE = '{"code":404,"message":"Access Code Pin Pair not found"}' def __init__(self, library, integration, valid={}, bad_connection=False, failure_status_code=None): super(MockFirstBookAuthenticationAPI, self).__init__( library, integration, root="http://example.com/" ) self.identifier_re = None self.password_re = None self.valid = valid self.bad_connection = bad_connection self.failure_status_code = failure_status_code
[docs] def request(self, url, header): if not header: raise RemoteInitiatedServerError if self.bad_connection: # Simulate a bad connection. raise requests.exceptions.ConnectionError("Could not connect!") elif self.failure_status_code: # Simulate a server returning an unexpected error code. return MockFirstBookResponse( self.failure_status_code, "Error %s" % self.failure_status_code ) qa = urllib.parse.parse_qs(url) for key in qa: if key == 'pin': (pin,) = qa['pin'] else: (code,) = qa[key] if code in self.valid and self.valid[code] == pin: return MockFirstBookResponse(200, self.SUCCESS) else: return MockFirstBookResponse(200, self.FAILURE)
# Specify which of the classes defined in this module is the # authentication provider. AuthenticationProvider = FirstBookAuthenticationAPI