def test_authdata_token_credential_lookup_success(self): # Create an authdata token Credential for Bob. now = datetime.datetime.utcnow() token, ignore = Credential.persistent_token_create( self._db, self.data_source, self.model.AUTHDATA_TOKEN_TYPE, self.bob_patron) # The token is persistent. eq_(None, token.expires) # Use that token to perform a lookup of Bob's Adobe Vendor ID # UUID. urn, label = self.model.authdata_lookup(token.credential) # There is now an anonymized identifier associated with Bob's # patron account. internal = DataSource.lookup(self._db, DataSource.INTERNAL_PROCESSING) bob_anonymized_identifier = Credential.lookup( self._db, internal, AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER, self.bob_patron, None) # That anonymized identifier is associated with a # DelegatedPatronIdentifier whose delegated_identifier is a # UUID. [bob_delegated_patron_identifier ] = self._db.query(DelegatedPatronIdentifier).filter( DelegatedPatronIdentifier.patron_identifier == bob_anonymized_identifier.credential).all() # That UUID is the one returned by authdata_lookup. eq_(urn, bob_delegated_patron_identifier.delegated_identifier)
def test_lookup_patron_affiliation_id( self, _, subject, expected_affiliation_id, affiliation_attributes=None ): # Arrange credential_manager = ProQuestCredentialManager() patron = self._patron() if subject: expected_token = json.dumps(subject, cls=SAMLSubjectJSONEncoder) data_source = DataSource.lookup( self._db, BaseSAMLAuthenticationProvider.TOKEN_DATA_SOURCE_NAME, autocreate=True, ) Credential.temporary_token_create( self._db, data_source, BaseSAMLAuthenticationProvider.TOKEN_TYPE, patron, datetime.timedelta(hours=1), expected_token, ) # Act if affiliation_attributes: token = credential_manager.lookup_patron_affiliation_id( self._db, patron, affiliation_attributes ) else: token = credential_manager.lookup_patron_affiliation_id(self._db, patron) # Assert assert expected_affiliation_id == token
def test_uuid_and_label_creates_delegatedpatronid_from_credential(self): # This patron once used the old system to create an Adobe # account ID which was stored in a Credential. For whatever # reason, the migration script did not give them a # DelegatedPatronIdentifier. adobe = self.data_source def set_value(credential): credential.credential = "A dummy value" old_style_credential = Credential.lookup( self._db, adobe, self.model.VENDOR_ID_UUID_TOKEN_TYPE, self.bob_patron, set_value, True ) # Now uuid_and_label works. uuid, label = self.model.uuid_and_label(self.bob_patron) eq_("A dummy value", uuid) eq_("Delegated account ID A dummy value", label) # There is now an anonymized identifier associated with Bob's # patron account. internal = DataSource.lookup(self._db, DataSource.INTERNAL_PROCESSING) bob_anonymized_identifier = Credential.lookup( self._db, internal, AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER, self.bob_patron, None ) # That anonymized identifier is associated with a # DelegatedPatronIdentifier whose delegated_identifier is # taken from the old-style Credential. [bob_delegated_patron_identifier] = self._db.query( DelegatedPatronIdentifier).filter( DelegatedPatronIdentifier.patron_identifier ==bob_anonymized_identifier.credential ).all() eq_("A dummy value", bob_delegated_patron_identifier.delegated_identifier) # If the DelegatedPatronIdentifier and the Credential # have different values, the DelegatedPatronIdentifier wins. old_style_credential.credential = "A different value." uuid, label = self.model.uuid_and_label(self.bob_patron) eq_("A dummy value", uuid) # We can even delete the old-style Credential, and # uuid_and_label will still give the value that was stored in # it. self._db.delete(old_style_credential) self._db.commit() uuid, label = self.model.uuid_and_label(self.bob_patron) eq_("A dummy value", uuid)
def patron_remote_identifier(self, patron): """Locate the identifier for the given Patron's account on the RBdigital side, creating a new RBdigital account if necessary. The identifier is cached in a persistent Credential object. :return: The remote identifier for this patron, taken from the corresponding Credential. """ def refresher(credential): remote_identifier = self.patron_remote_identifier_lookup(patron) if not remote_identifier: remote_identifier = self.create_patron(patron) credential.credential = remote_identifier credential.expires = None _db = Session.object_session(patron) credential = Credential.lookup( _db, DataSource.RB_DIGITAL, Credential.IDENTIFIER_FROM_REMOTE_SERVICE, patron, refresher_method=refresher, allow_persistent_token=True) if not credential.credential: refresher(credential) return credential.credential
def get_patron_credential(self, patron, pin): """Create an OAuth token for the given patron.""" def refresh(credential): return self.refresh_patron_access_token( credential, patron, pin) return Credential.lookup( self._db, DataSource.OVERDRIVE, "OAuth Token", patron, refresh)
def test_authdata_lookup_failure_wrong_token(self): temp_token, ignore = Credential.temporary_token_create( self._db, self.data_source, self.model.TEMPORARY_TOKEN_TYPE, self.bob_patron, datetime.timedelta(seconds=60)) urn, label = self.model.authdata_lookup("nosuchauthdata") eq_(None, urn) eq_(None, label)
def _create_token(self, db, patron, subject, cm_session_lifetime=None): """Create a Credential object that ties the given patron to the given provider token. :param db: Database session :type db: sqlalchemy.orm.session.Session :param patron: Patron object :type patron: Patron :param subject: SAML subject :type subject: api.saml.metadata.model.SAMLSubject :param cm_session_lifetime: (Optional) Circulation Manager's session lifetime expressed in days :type cm_session_lifetime: Optional[int] :return: Credential object :rtype: Credential """ session_lifetime = subject.valid_till if cm_session_lifetime: session_lifetime = datetime.timedelta( days=int(cm_session_lifetime)) token = self._create_token_value(subject) data_source, ignore = self._get_token_data_source(db) return Credential.temporary_token_create(db, data_source, self.TOKEN_TYPE, patron, session_lifetime, token)
def test_username_password_lookup_success(self): urn, label = self.model.standard_lookup(self.credentials) # There is now an anonymized identifier associated with Bob's # patron account. internal = DataSource.lookup(self._db, DataSource.INTERNAL_PROCESSING) bob_anonymized_identifier = Credential.lookup( self._db, internal, AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER, self.bob_patron, None ) # That anonymized identifier is associated with a # DelegatedPatronIdentifier whose delegated_identifier is a # UUID. [bob_delegated_patron_identifier] = self._db.query( DelegatedPatronIdentifier).filter( DelegatedPatronIdentifier.patron_identifier ==bob_anonymized_identifier.credential ).all() eq_("Delegated account ID %s" % urn, label) eq_(urn, bob_delegated_patron_identifier.delegated_identifier) assert urn.startswith("urn:uuid:0") assert urn.endswith('685b35c00f05')
def _lookup_saml_token(self, db, patron): """Look up for a SAML token. :param db: Database session :type db: sqlalchemy.orm.session.Session :param patron: Patron object :type patron: core.model.patron.Patron :return: SAML subject (if any) :rtype: Optional[api.saml.metadata.Subject] """ self._logger.debug("Started looking up for a SAML token") from api.authenticator import BaseSAMLAuthenticationProvider credential = Credential.lookup_by_patron( db, BaseSAMLAuthenticationProvider.TOKEN_DATA_SOURCE_NAME, BaseSAMLAuthenticationProvider.TOKEN_TYPE, patron, allow_persistent_token=False, auto_create_datasource=True, ) self._logger.debug( "Finished looking up for a SAML token: {0}".format(credential) ) return credential
def uuid_and_label(self, patron): """Create or retrieve a Vendor ID UUID and human-readable Vendor ID label for the given patron. """ if not patron: return None, None def generate_uuid(credential): # This is the first time a credential has ever been # created for this patron. Set the value of the # credential to a new UUID. print "GENERATING NEW UUID" credential.credential = self.uuid() credential = Credential.lookup( self._db, self.data_source, self.VENDOR_ID_UUID_TOKEN_TYPE, patron, generate_uuid, True) identifier = patron.authorization_identifier if not identifier: # Maybe this should be an error, but even though the lack # of an authorization identifier is a problem, the problem # should manifest when the patron tries to actually use # their credential. return "Unknown card number.", "Unknown card number" return credential.credential, "Card number " + identifier
def authenticated_patron(self, db, token): """Go from a token to an authenticated Patron. :param db: Database session :type db: sqlalchemy.orm.session.Session :param token: The provider token extracted from the Authorization header. This is _not_ the bearer token found in the Authorization header; it's the provider-specific token embedded in that token. :type token: Dict :return: A Patron, if one can be authenticated. None, if the credentials do not authenticate any particular patron. A ProblemDetail if an error occurs. :rtype: Union[Patron, ProblemDetail] """ data_source, ignore = self._get_token_data_source(db) credential = Credential.lookup_by_token(db, data_source, self.TOKEN_TYPE, token) if credential: return credential.patron # This token wasn't in our database, or was expired. The # patron will have to log in through the SAML provider again # to get a new token. return None
def test_migrate_adobe_id_success(self): from api.opds import CirculationManagerAnnotator patron = self._patron() # This patron has a Credential containing their Adobe ID data_source = DataSource.lookup(self._db, DataSource.ADOBE) adobe_id = Credential( patron=patron, data_source=data_source, type=AdobeVendorIDModel.VENDOR_ID_UUID_TOKEN_TYPE, credential="My Adobe ID" ) # Run the migration. new_credential, delegated_identifier = self.authdata.migrate_adobe_id(patron) # The patron now has _two_ Credentials -- the old one # containing the Adobe ID, and a new one. eq_(set([new_credential, adobe_id]), set(patron.credentials)) # The new credential contains an anonymized patron identifier # used solely to connect the patron to their Adobe ID. eq_(AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER, new_credential.type) # We can use that identifier to look up a DelegatedPatronIdentifier # def explode(): # This method won't be called because the # DelegatedPatronIdentifier already exists. raise Exception() identifier, is_new = DelegatedPatronIdentifier.get_one_or_create( self._db, self.authdata.library_uri, new_credential.credential, DelegatedPatronIdentifier.ADOBE_ACCOUNT_ID, explode ) eq_(delegated_identifier, identifier) eq_(False, is_new) eq_("My Adobe ID", identifier.delegated_identifier) # An integration-level test: # AdobeVendorIDModel.to_delegated_patron_identifier_uuid works # now. model = AdobeVendorIDModel(self._default_library, None, None) uuid, label = model.to_delegated_patron_identifier_uuid( self.authdata.library_uri, new_credential.credential ) eq_("My Adobe ID", uuid) eq_('Delegated account ID My Adobe ID', label) # If we run the migration again, nothing new happens. new_credential_2, delegated_identifier_2 = self.authdata.migrate_adobe_id(patron) eq_(new_credential, new_credential_2) eq_(delegated_identifier, delegated_identifier_2) eq_(2, len(patron.credentials)) uuid, label = model.to_delegated_patron_identifier_uuid( self.authdata.library_uri, new_credential.credential ) eq_("My Adobe ID", uuid) eq_('Delegated account ID My Adobe ID', label)
def test_create_authdata(self): credential = self.model.create_authdata(self.bob_patron) # There's now a temporary token associated with Bob's # patron account, and that's the token returned by create_authdata() bob_authdata = Credential.lookup( self._db, self.data_source, self.model.TEMPORARY_TOKEN_TYPE, self.bob_patron, None) eq_(credential.credential, bob_authdata.credential)
def token(self): if (self._credential and self._credential.expires > datetime.utcnow()): return self._credential.credential credential = Credential.lookup( self._db, self.source, None, None, self.refresh_credential ) return credential.credential
def test_create_authdata(self): credential = self.model.create_authdata(self.bob_patron) # There's now a persistent token associated with Bob's # patron account, and that's the token returned by create_authdata() bob_authdata = Credential.lookup( self._db, self.data_source, self.model.AUTHDATA_TOKEN_TYPE, self.bob_patron, None) eq_(credential.credential, bob_authdata.credential)
def patron_from_authdata_lookup(self, authdata): """Look up a patron by their persistent authdata token.""" credential = Credential.lookup_by_token( self._db, self.data_source, self.AUTHDATA_TOKEN_TYPE, authdata, allow_persistent_token=True ) if not credential: return None return credential.patron
def urn_to_label(self, urn): credential = Credential.lookup_by_token( self._db, self.data_source, self.VENDOR_ID_UUID_TOKEN_TYPE, urn, True) if not credential: return None patron = credential.patron uuid, label = self.uuid_and_label(credential.patron) return label
def test_authdata_lookup_success(self): now = datetime.datetime.utcnow() temp_token, ignore = Credential.temporary_token_create( self._db, self.data_source, self.model.TEMPORARY_TOKEN_TYPE, self.bob_patron, datetime.timedelta(seconds=60)) old_expires = temp_token.expires assert temp_token.expires > now urn, label = self.model.authdata_lookup(temp_token.credential) # There is now a UUID associated with Bob's patron account, # and that's the UUID returned by standard_lookup(). bob_uuid = Credential.lookup( self._db, self.data_source, self.model.VENDOR_ID_UUID_TOKEN_TYPE, self.bob_patron, None) eq_(urn, bob_uuid.credential) eq_("Card number 5", label) # Having been used once, the temporary token has been expired. assert temp_token.expires < now
def authdata_lookup(self, authdata): """Look up a patron by a temporary Adobe Vendor ID token. Return their Vendor ID UUID and their human-readable label. """ credential = Credential.lookup_by_temporary_token( self._db, self.data_source, self.TEMPORARY_TOKEN_TYPE, authdata) if not credential: return None, None return self.uuid_and_label(credential.patron)
def test_authdata_lookup_failure_wrong_token(self): # Bob has an authdata token. token, ignore = Credential.persistent_token_create( self._db, self.data_source, self.model.AUTHDATA_TOKEN_TYPE, self.bob_patron) # But we look up a different token and get nothing. urn, label = self.model.authdata_lookup("nosuchauthdata") eq_(None, urn) eq_(None, label)
def test_smuggled_authdata_credential_success(self): # Bob's client has created a persistent token to authenticate him. now = datetime.datetime.utcnow() token, ignore = Credential.persistent_token_create( self._db, self.data_source, self.model.AUTHDATA_TOKEN_TYPE, self.bob_patron ) # But Bob's client can't trigger the operation that will cause # Adobe to authenticate him via that token, so it passes in # the token credential as the 'username' and leaves the # password blank. urn, label = self.model.standard_lookup( dict(username=token.credential) ) # There is now an anonymized identifier associated with Bob's # patron account. internal = DataSource.lookup(self._db, DataSource.INTERNAL_PROCESSING) bob_anonymized_identifier = Credential.lookup( self._db, internal, AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER, self.bob_patron, None ) # That anonymized identifier is associated with a # DelegatedPatronIdentifier whose delegated_identifier is a # UUID. [bob_delegated_patron_identifier] = self._db.query( DelegatedPatronIdentifier).filter( DelegatedPatronIdentifier.patron_identifier ==bob_anonymized_identifier.credential ).all() # That UUID is the one returned by standard_lookup. eq_(urn, bob_delegated_patron_identifier.delegated_identifier) # A future attempt to authenticate with the token will succeed. urn, label = self.model.standard_lookup( dict(username=token.credential) ) eq_(urn, bob_delegated_patron_identifier.delegated_identifier)
def _adobe_patron_identifier(self, patron): _db = Session.object_session(patron) internal = DataSource.lookup(_db, DataSource.INTERNAL_PROCESSING) def refresh(credential): credential.credential = str(uuid.uuid1()) patron_identifier = Credential.lookup( _db, internal, AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER, patron, refresher_method=refresh, allow_persistent_token=True ) return patron_identifier.credential
def get_or_create_patron_identifier_credential(cls, patron): _db = Session.object_session(patron) def refresh(credential): credential.credential = str(uuid.uuid1()) data_source = DataSource.lookup(_db, DataSource.INTERNAL_PROCESSING) patron_identifier_credential = Credential.lookup( _db, data_source, AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER, patron, refresher_method=refresh, allow_persistent_token=True ) return patron_identifier_credential
def test_standard_lookup_success(self): urn, label = self.model.standard_lookup(dict(username="******", password="******")) # There is now a UUID associated with Bob's patron account, # and that's the UUID returned by standard_lookup(). bob_uuid = Credential.lookup( self._db, self.data_source, self.model.VENDOR_ID_UUID_TOKEN_TYPE, self.bob_patron, None) eq_("Card number 5", label) eq_(urn, bob_uuid.credential) assert urn.startswith("urn:uuid:0") assert urn.endswith('685b35c00f05')
def test_lookup_proquest_token_returns_token(self): # Arrange credential_manager = ProQuestCredentialManager() patron = self._patron() expected_token = "12345" Credential.temporary_token_create( self._db, self._data_source, ProQuestCredentialType.PROQUEST_JWT_TOKEN.value, patron, datetime.timedelta(hours=1), expected_token, ) # Act token = credential_manager.lookup_proquest_token(self._db, patron) # Assert assert True == isinstance(token, Credential) assert expected_token == token.credential
def save_proquest_token(self, db, patron, duration, token): """Save a ProQuest JWT bearer token for later use. :param db: Database session :type db: sqlalchemy.orm.session.Session :param patron: Patron object :type patron: core.model.patron.Patron :param duration: How long this token can be valid :type duration: datetime.timedelta :param token: ProQuest JWT bearer token :type token: str :return: Credential object containing a new ProQuest JWT bearer token :rtype: Optional[core.model.credential.Credential] """ if not is_session(db): raise ValueError('"db" argument must be a valid SQLAlchemy session') if not isinstance(patron, Patron): raise ValueError('"patron" argument must be an instance of Patron class') if not isinstance(duration, datetime.timedelta): raise ValueError( '"duration" argument must be an instance of datetime.timedelta class' ) if not isinstance(token, str) or not token: raise ValueError('"token" argument must be a non-empty string') self._logger.debug( "Started saving a ProQuest JWT bearer token {0}".format(token) ) data_source = DataSource.lookup( db, DataSourceConstants.PROQUEST, autocreate=True ) credential, is_new = Credential.temporary_token_create( db, data_source, ProQuestCredentialType.PROQUEST_JWT_TOKEN.value, patron, duration, token, ) self._logger.debug( "Finished saving a ProQuest JWT bearer token {0}: {1} (new = {2})".format( token, credential, is_new ) ) return credential
def test_process_patron(self): patron = self._patron() # This patron has old-style and new-style Credentials that link # them to Adobe account IDs (hopefully the same ID, though that # doesn't matter here. def set_value(credential): credential.value = "a credential" # Data source doesn't matter -- even if it's incorrect, a Credential # of the appropriate type will be deleted. data_source = DataSource.lookup(self._db, DataSource.OVERDRIVE) # Create two Credentials that will be deleted and one that will be # left alone. for type in (AdobeVendorIDModel.VENDOR_ID_UUID_TOKEN_TYPE, AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER, "Some other type" ): credential = Credential.lookup( self._db, data_source, type, patron, set_value, True ) eq_(3, len(patron.credentials)) # Run the patron through the script. script = AdobeAccountIDResetScript(self._db) # A dry run does nothing. script.delete = False script.process_patron(patron) self._db.commit() eq_(3, len(patron.credentials)) # Now try it for real. script.delete = True script.process_patron(patron) self._db.commit() # The two Adobe-related credentials are gone. The other one remains. [credential] = patron.credentials eq_("Some other type", credential.type)
def test_save_proquest_token_saves_token(self): # Arrange credential_manager = ProQuestCredentialManager() patron = self._patron() expected_token = "12345" # Act credential_manager.save_proquest_token( self._db, patron, datetime.timedelta(hours=1), expected_token ) token = Credential.lookup_by_patron( self._db, self._data_source.name, ProQuestCredentialType.PROQUEST_JWT_TOKEN.value, patron, ) # Assert assert expected_token == token.credential
def test_getter(self, _, credential_type, method_name, expected_result): # Arrange credential = Credential(credential=expected_result) with patch.object( Credential, "persistent_token_create") as persistent_token_create_mock: persistent_token_create_mock.return_value = (credential, True) method = getattr(self._factory, method_name) # Act result = method(self._db, self._patron) # Assert assert result == expected_result persistent_token_create_mock.assert_called_once_with( self._db, self._data_source, credential_type, self._patron, None)
def _create_token(self, db, patron, token, valid_till): """Creates a Credential object that ties the given patron to the given provider token. :param db: Database session :type db: sqlalchemy.orm.session.Session :param patron: Patron object :type patron: Patron :param token: Token containing SAML subject's metadata :type token: Dict :param valid_till: The time till which the SAML subject is valid :type valid_till: datetime.timedelta :return: Credential object :rtype: Credential """ data_source, ignore = self._get_token_data_source(db) return Credential.temporary_token_create(db, data_source, self.TOKEN_TYPE, patron, valid_till, token)
def lookup_proquest_token(self, db, patron): """Look up for a JWT bearer token used required to use ProQuest API. :param db: Database session :type db: sqlalchemy.orm.session.Session :param patron: Patron object :type patron: core.model.patron.Patron :return: Credential object containing the existing ProQuest JWT bearer token (if any) :rtype: Optional[core.model.credential.Credential] """ if not is_session(db): raise ValueError('"db" argument must be a valid SQLAlchemy session') if not isinstance(patron, Patron): raise ValueError('"patron" argument must be an instance of Patron class') self._logger.debug("Started looking up for a ProQuest JWT token") credential = Credential.lookup_by_patron( db, DataSourceConstants.PROQUEST, ProQuestCredentialType.PROQUEST_JWT_TOKEN.value, patron, allow_persistent_token=False, auto_create_datasource=True, ) self._logger.debug( "Finished looking up for a ProQuest JWT token: {0}".format(credential) ) if credential: return credential return None
class OPDSForDistributorsAPI(BaseCirculationAPI): NAME = "OPDS for Distributors" DESCRIPTION = _("Import books from a distributor that requires authentication to get the OPDS feed and download books.") SETTINGS = OPDSImporter.SETTINGS + [ { "key": ExternalIntegration.USERNAME, "label": _("Library's username or access key"), }, { "key": ExternalIntegration.PASSWORD, "label": _("Library's password or secret key"), } ] SUPPORTED_MEDIA_TYPES = [Representation.EPUB_MEDIA_TYPE] delivery_mechanism_to_internal_format = { (type, DeliveryMechanism.NO_DRM): type for type in SUPPORTED_MEDIA_TYPES } def __init__(self, _db, collection): self.collection_id = collection.id self.data_source_name = collection.external_integration.setting(Collection.DATA_SOURCE_NAME_SETTING).value self.username = collection.external_integration.username self.password = collection.external_integration.password self.feed_url = collection.external_account_id self.auth_url = None def _request_with_timeout(self, method, url, *args, **kwargs): """Wrapper around HTTP.request_with_timeout to be overridden for tests.""" return HTTP.request_with_timeout(method, url, *args, **kwargs) def _get_token(self, _db): # If this is the first time we're getting a token, we # need to find the authenticate url in the OPDS # authentication document. if not self.auth_url: response = self._request_with_timeout('GET', self.feed_url) if response.status_code != 401: # This feed doesn't require authentication, so # we need to find a link to the authentication document. feed = feedparser.parse(response.content) links = feed.get('feed', {}).get('links', []) auth_doc_links = [l for l in links if l['rel'] == "http://opds-spec.org/auth/document"] if not auth_doc_links: raise LibraryAuthorizationFailedException() auth_doc_link = auth_doc_links[0].get("href") response = self._request_with_timeout('GET', auth_doc_link) try: auth_doc = json.loads(response.content) except Exception, e: raise LibraryAuthorizationFailedException() auth_types = auth_doc.get('authentication', []) credentials_types = [t for t in auth_types if t['type'] == "http://opds-spec.org/auth/oauth/client_credentials"] if not credentials_types: raise LibraryAuthorizationFailedException() links = credentials_types[0].get('links', []) auth_links = [l for l in links if l.get("rel") == "authenticate"] if not auth_links: raise LibraryAuthorizationFailedException() self.auth_url = auth_links[0].get("href") def refresh(credential): headers = dict() auth_header = "Basic %s" % base64.b64encode("%s:%s" % (self.username, self.password)) headers['Authorization'] = auth_header headers['Content-Type'] = "application/x-www-form-urlencoded" body = dict(grant_type='client_credentials') token_response = self._request_with_timeout('POST', self.auth_url, data=body, headers=headers) token = json.loads(token_response.content) access_token = token.get("access_token") expires_in = token.get("expires_in") if not access_token or not expires_in: raise LibraryAuthorizationFailedException() credential.credential = access_token expires_in = expires_in credential.expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=expires_in) return Credential.lookup(_db, self.data_source_name, "OPDS For Distributors Bearer Token", patron=None, refresher_method=refresh, ).credential
def create_authdata(self, patron): credential, is_new = Credential.persistent_token_create( self._db, self.data_source, self.AUTHDATA_TOKEN_TYPE, patron) return credential
class OPDSForDistributorsAPI(BaseCirculationAPI, HasSelfTests): NAME = "OPDS for Distributors" DESCRIPTION = _( "Import books from a distributor that requires authentication to get the OPDS feed and download books." ) SETTINGS = OPDSImporter.BASE_SETTINGS + [ { "key": ExternalIntegration.USERNAME, "label": _("Library's username or access key"), "required": True, }, { "key": ExternalIntegration.PASSWORD, "label": _("Library's password or secret key"), "required": True, } ] # In OPDS For Distributors, all items are gated through the # BEARER_TOKEN access control scheme. # # If the default client supports a given media type when # combined with the BEARER_TOKEN scheme, then we should import # titles with that media type... SUPPORTED_MEDIA_TYPES = [ format for (format, drm) in DeliveryMechanism.default_client_can_fulfill_lookup if drm == (DeliveryMechanism.BEARER_TOKEN) and format is not None ] # ...and we should map requests for delivery of that media type to # the (type, BEARER_TOKEN) DeliveryMechanism. delivery_mechanism_to_internal_format = { (type, DeliveryMechanism.BEARER_TOKEN): type for type in SUPPORTED_MEDIA_TYPES } def __init__(self, _db, collection): self.collection_id = collection.id self.external_integration_id = collection.external_integration.id self.data_source_name = collection.external_integration.setting( Collection.DATA_SOURCE_NAME_SETTING).value self.username = collection.external_integration.username self.password = collection.external_integration.password self.feed_url = collection.external_account_id self.auth_url = None def external_integration(self, _db): return get_one(_db, ExternalIntegration, id=self.external_integration_id) def _run_self_tests(self, _db): """Try to get a token.""" yield self.run_test("Negotiate a fulfillment token", self._get_token, _db) def _request_with_timeout(self, method, url, *args, **kwargs): """Wrapper around HTTP.request_with_timeout to be overridden for tests.""" return HTTP.request_with_timeout(method, url, *args, **kwargs) def _get_token(self, _db): # If this is the first time we're getting a token, we # need to find the authenticate url in the OPDS # authentication document. if not self.auth_url: # Keep track of the most recent URL we retrieved for error # reporting purposes. current_url = self.feed_url response = self._request_with_timeout('GET', current_url) if response.status_code != 401: # This feed doesn't require authentication, so # we need to find a link to the authentication document. feed = feedparser.parse(response.content) links = feed.get('feed', {}).get('links', []) auth_doc_links = [ l for l in links if l['rel'] == "http://opds-spec.org/auth/document" ] if not auth_doc_links: raise LibraryAuthorizationFailedException( "No authentication document link found in %s" % current_url) current_url = auth_doc_links[0].get("href") response = self._request_with_timeout('GET', current_url) try: auth_doc = json.loads(response.content) except Exception, e: raise LibraryAuthorizationFailedException( "Could not load authentication document from %s" % current_url) auth_types = auth_doc.get('authentication', []) credentials_types = [ t for t in auth_types if t['type'] == "http://opds-spec.org/auth/oauth/client_credentials" ] if not credentials_types: raise LibraryAuthorizationFailedException( "Could not find any credential-based authentication mechanisms in %s" % current_url) links = credentials_types[0].get('links', []) auth_links = [l for l in links if l.get("rel") == "authenticate"] if not auth_links: raise LibraryAuthorizationFailedException( "Could not find any authentication links in %s" % current_url) self.auth_url = auth_links[0].get("href") def refresh(credential): headers = dict() auth_header = "Basic %s" % base64.b64encode( "%s:%s" % (self.username, self.password)) headers['Authorization'] = auth_header headers['Content-Type'] = "application/x-www-form-urlencoded" body = dict(grant_type='client_credentials') token_response = self._request_with_timeout('POST', self.auth_url, data=body, headers=headers) token = json.loads(token_response.content) access_token = token.get("access_token") expires_in = token.get("expires_in") if not access_token or not expires_in: raise LibraryAuthorizationFailedException( "Document retrieved from %s is not a bearer token: %s" % (self.auth_url, token_response.content)) credential.credential = access_token expires_in = expires_in # We'll avoid edge cases by assuming the token expires 75% # into its useful lifetime. credential.expires = datetime.datetime.utcnow( ) + datetime.timedelta(seconds=expires_in * 0.75) return Credential.lookup( _db, self.data_source_name, "OPDS For Distributors Bearer Token", patron=None, refresher_method=refresh, )
def _patron_credential_lookup(self, patron, refresh): return Credential.lookup(self._db, DataSource.ODILO, "OAuth Token", patron, refresh)
def credential_object(self, refresh): """Look up the Credential object that allows us to use the Odilo API. """ return Credential.lookup(self._db, DataSource.ODILO, None, None, refresh)
def create_authdata(self, patron): credential, is_new = Credential.temporary_token_create( self._db, self.data_source, self.TEMPORARY_TOKEN_TYPE, patron, self.temporary_token_duration) return credential