def test_fulfill(self): other_client, ignore = IntegrationClient.register(self._db, "http://other_library.org") loan, ignore = create(self._db, Loan, integration_client=other_client, license_pool=self.pool) assert_raises(CannotFulfill, self.shared_collection.fulfill, self.collection, self.client, loan, self.delivery_mechanism) loan.integration_client = self.client # If the API does not return content or a content link, the loan can't be fulfilled. assert_raises(CannotFulfill, self.shared_collection.fulfill, self.collection, self.client, loan, self.delivery_mechanism) eq_([(self.client, loan, self.delivery_mechanism)], self.api.fulfills) self.api.fulfillment = FulfillmentInfo( self.collection, self.pool.data_source.name, self.pool.identifier.type, self.pool.identifier.identifier, "http://content", "text/html", None, None, ) fulfillment = self.shared_collection.fulfill(self.collection, self.client, loan, self.delivery_mechanism) eq_([(self.client, loan, self.delivery_mechanism)], self.api.fulfills[1:]) eq_(self.delivery_mechanism, loan.fulfillment)
def test_fulfill(self): other_client, ignore = IntegrationClient.register( self._db, "http://other_library.org") loan, ignore = create(self._db, Loan, integration_client=other_client, license_pool=self.pool) assert_raises(CannotFulfill, self.shared_collection.fulfill, self.collection, self.client, loan, self.delivery_mechanism) loan.integration_client = self.client # If the API does not return content or a content link, the loan can't be fulfilled. assert_raises(CannotFulfill, self.shared_collection.fulfill, self.collection, self.client, loan, self.delivery_mechanism) eq_([(self.client, loan, self.delivery_mechanism)], self.api.fulfills) self.api.fulfillment = FulfillmentInfo( self.collection, self.pool.data_source.name, self.pool.identifier.type, self.pool.identifier.identifier, "http://content", "text/html", None, None, ) fulfillment = self.shared_collection.fulfill(self.collection, self.client, loan, self.delivery_mechanism) eq_([(self.client, loan, self.delivery_mechanism)], self.api.fulfills[1:]) eq_(self.delivery_mechanism, loan.fulfillment)
def test_borrow(self): # This client is registered, but isn't one of the allowed URLs for the collection # (maybe it was registered for a different shared collection). other_client, ignore = IntegrationClient.register( self._db, "http://other_library.org") # Trying to borrow raises an exception. assert_raises(AuthorizationFailedException, self.shared_collection.borrow, self.collection, other_client, self.pool) # A client that's registered with the collection can borrow. self.shared_collection.borrow(self.collection, self.client, self.pool) eq_([(self.client, self.pool)], self.api.checkouts) # If the client's checking out an existing hold, the hold must be for that client. hold, ignore = create(self._db, Hold, integration_client=other_client, license_pool=self.pool) assert_raises(CannotLoan, self.shared_collection.borrow, self.collection, self.client, self.pool, hold=hold) hold.integration_client = self.client self.shared_collection.borrow(self.collection, self.client, self.pool, hold=hold) eq_([(self.client, self.pool)], self.api.checkouts[1:])
def test_revoke_loan(self): other_client, ignore = IntegrationClient.register(self._db, "http://other_library.org") loan, ignore = create(self._db, Loan, integration_client=other_client, license_pool=self.pool) assert_raises(NotCheckedOut, self.shared_collection.revoke_loan, self.collection, self.client, loan) loan.integration_client = self.client self.shared_collection.revoke_loan(self.collection, self.client, loan) eq_([(self.client, loan)], self.api.returns)
def test_revoke_hold(self): other_client, ignore = IntegrationClient.register(self._db, "http://other_library.org") hold, ignore = create(self._db, Hold, integration_client=other_client, license_pool=self.pool) assert_raises(CannotReleaseHold, self.shared_collection.revoke_hold, self.collection, self.client, hold) hold.integration_client = self.client self.shared_collection.revoke_hold(self.collection, self.client, hold) eq_([(self.client, hold)], self.api.released_holds)
def test_revoke_loan(self): other_client, ignore = IntegrationClient.register( self._db, "http://other_library.org") loan, ignore = create(self._db, Loan, integration_client=other_client, license_pool=self.pool) assert_raises(NotCheckedOut, self.shared_collection.revoke_loan, self.collection, self.client, loan) loan.integration_client = self.client self.shared_collection.revoke_loan(self.collection, self.client, loan) eq_([(self.client, loan)], self.api.returns)
def test_revoke_hold(self): other_client, ignore = IntegrationClient.register( self._db, "http://other_library.org") hold, ignore = create(self._db, Hold, integration_client=other_client, license_pool=self.pool) assert_raises(CannotReleaseHold, self.shared_collection.revoke_hold, self.collection, self.client, hold) hold.integration_client = self.client self.shared_collection.revoke_hold(self.collection, self.client, hold) eq_([(self.client, hold)], self.api.released_holds)
def run(self, url): if not url: ValueError("No url provided. Could not create IntegrationClient.") url = " ".join(url) print "Creating IntegrationClient for '%s'" % url client, plaintext_secret = IntegrationClient.register(self._db, url) print client print ("RECORD THE FOLLOWING AUTHENTICATION DETAILS. " "The client secret cannot be recovered.") print "-" * 40 print "CLIENT KEY: %s" % client.key print "CLIENT SECRET: %s" % plaintext_secret self._db.commit()
def setup(self): super(TestSharedCollectionAPI, self).setup() self.collection = self._collection(protocol="Mock") self.shared_collection = SharedCollectionAPI(self._db, api_map={"Mock": MockAPI}) self.api = self.shared_collection.api(self.collection) ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, self.collection.external_integration).value = json.dumps( ["http://library.org"]) self.client, ignore = IntegrationClient.register( self._db, "http://library.org") edition, self.pool = self._edition(with_license_pool=True, collection=self.collection) [self.delivery_mechanism] = self.pool.delivery_mechanisms
def run(self, url): if not url: ValueError("No url provided. Could not create IntegrationClient.") url = " ".join(url) print "Creating IntegrationClient for '%s'" % url client, plaintext_secret = IntegrationClient.register(self._db, url) print client print( "RECORD THE FOLLOWING AUTHENTICATION DETAILS. " "The client secret cannot be recovered.") print "-" * 40 print "CLIENT KEY: %s" % client print "CLIENT SECRET: %s" % plaintext_secret
def setup(self): super(TestSharedCollectionAPI, self).setup() self.collection = self._collection(protocol="Mock") self.shared_collection = SharedCollectionAPI( self._db, api_map = { "Mock" : MockAPI } ) self.api = self.shared_collection.api(self.collection) ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, self.collection.external_integration ).value = json.dumps(["http://library.org"]) self.client, ignore = IntegrationClient.register(self._db, "http://library.org") edition, self.pool = self._edition( with_license_pool=True, collection=self.collection ) [self.delivery_mechanism] = self.pool.delivery_mechanisms
def test_borrow(self): # This client is registered, but isn't one of the allowed URLs for the collection # (maybe it was registered for a different shared collection). other_client, ignore = IntegrationClient.register(self._db, "http://other_library.org") # Trying to borrow raises an exception. assert_raises(AuthorizationFailedException, self.shared_collection.borrow, self.collection, other_client, self.pool) # A client that's registered with the collection can borrow. self.shared_collection.borrow(self.collection, self.client, self.pool) eq_([(self.client, self.pool)], self.api.checkouts) # If the client's checking out an existing hold, the hold must be for that client. hold, ignore = create(self._db, Hold, integration_client=other_client, license_pool=self.pool) assert_raises(CannotLoan, self.shared_collection.borrow, self.collection, self.client, self.pool, hold=hold) hold.integration_client = self.client self.shared_collection.borrow(self.collection, self.client, self.pool, hold=hold) eq_([(self.client, self.pool)], self.api.checkouts[1:])
_("Error decoding JWT: %(message)s", message=e.message)) # The ability to create a valid JWT indicates control over # the server, so it's not necessary to know the current # secret to set a new secret. client, is_new = IntegrationClient.for_url(self._db, url) client.randomize_secret() else: # If no JWT is provided, then we use the old logic. The first # time registration happens, no special authentication # is required apart from the ability to decode the secret. # # On subsequent attempts, the old secret must be provided to # create a new secret. try: client, is_new = IntegrationClient.register( self._db, url, submitted_secret=submitted_secret) except ValueError as e: log.error("Error in IntegrationClient.register", exc_info=e) return INVALID_CREDENTIALS.detailed(e.message) # Now that we have an IntegrationClient with a shared # secret, encrypt the shared secret with the provided public key # and send it back. encrypted_secret = encryptor.encrypt(str(client.shared_secret)) shared_secret = base64.b64encode(encrypted_secret) auth_data = dict(id=url, metadata=dict(shared_secret=shared_secret)) content = json.dumps(auth_data) headers = {'Content-Type': OPDS_2_MEDIA_TYPE} status_code = 200 if is_new:
library_url=start_url)) public_key = auth_document.get("public_key") if not public_key or not public_key.get("type") == "RSA" or not public_key.get("value"): raise RemoteInitiatedServerError( _("Authentication document at %(auth_document_url)s did not contain an RSA public key.", auth_document_url=auth_document_url), _("Remote authentication document")) public_key = public_key.get("value") encryptor = Configuration.cipher(public_key) normalized_url = IntegrationClient.normalize_url(start_url) client = get_one(self._db, IntegrationClient, url=normalized_url) if not client: client, ignore = IntegrationClient.register(self._db, start_url) shared_secret = client.shared_secret encrypted_secret = encryptor.encrypt(str(shared_secret)) return dict(metadata=dict(shared_secret=base64.b64encode(encrypted_secret))) def check_client_authorization(self, collection, client): """Verify that an IntegrationClient is whitelisted for access to the collection.""" external_library_urls = ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, collection.external_integration ).json_value if client.url not in [IntegrationClient.normalize_url(url) for url in external_library_urls]: raise AuthorizationFailedException() def borrow(self, collection, client, pool, hold=None): api = self.api(collection)
def register(self, collection, auth_document_url, do_get=HTTP.get_with_timeout): """Register a library on an external circulation manager for access to this collection. The library's auth document url must be whitelisted in the collection's settings.""" if not auth_document_url: raise InvalidInputException( _("An authentication document URL is required to register a library.") ) auth_response = do_get(auth_document_url, allowed_response_codes=["2xx", "3xx"]) try: auth_document = json.loads(auth_response.content) except ValueError as e: raise RemoteInitiatedServerError( _( "Authentication document at %(auth_document_url)s was not valid JSON.", auth_document_url=auth_document_url, ), _("Remote authentication document"), ) links = auth_document.get("links") start_url = None for link in links: if link.get("rel") == "start": start_url = link.get("href") break if not start_url: raise RemoteInitiatedServerError( _( "Authentication document at %(auth_document_url)s did not contain a start link.", auth_document_url=auth_document_url, ), _("Remote authentication document"), ) external_library_urls = ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, collection.external_integration, ).json_value if not external_library_urls or start_url not in external_library_urls: raise AuthorizationFailedException( _( "Your library's URL is not one of the allowed URLs for this collection. Ask the collection administrator to add %(library_url)s to the list of allowed URLs.", library_url=start_url, ) ) public_key = auth_document.get("public_key") if ( not public_key or not public_key.get("type") == "RSA" or not public_key.get("value") ): raise RemoteInitiatedServerError( _( "Authentication document at %(auth_document_url)s did not contain an RSA public key.", auth_document_url=auth_document_url, ), _("Remote authentication document"), ) public_key = public_key.get("value") encryptor = Configuration.cipher(public_key) normalized_url = IntegrationClient.normalize_url(start_url) client = get_one(self._db, IntegrationClient, url=normalized_url) if not client: client, ignore = IntegrationClient.register(self._db, start_url) shared_secret = client.shared_secret.encode("utf-8") encrypted_secret = encryptor.encrypt(shared_secret) return dict(metadata=dict(shared_secret=base64.b64encode(encrypted_secret)))
# The ability to create a valid JWT indicates control over # the server, so it's not necessary to know the current # secret to set a new secret. client, is_new = IntegrationClient.for_url(self._db, url) client.randomize_secret() else: # If no JWT is provided, then we use the old logic. The first # time registration happens, no special authentication # is required apart from the ability to decode the secret. # # On subsequent attempts, the old secret must be provided to # create a new secret. try: client, is_new = IntegrationClient.register( self._db, url, submitted_secret=submitted_secret ) except ValueError as e: log.error("Error in IntegrationClient.register", exc_info=e) return INVALID_CREDENTIALS.detailed(e.message) # Now that we have an IntegrationClient with a shared # secret, encrypt the shared secret with the provided public key # and send it back. encrypted_secret = encryptor.encrypt(str(client.shared_secret)) shared_secret = base64.b64encode(encrypted_secret) auth_data = dict( id=url, metadata=dict(shared_secret=shared_secret) ) content = json.dumps(auth_data)
public_key = auth_document.get("public_key") if not public_key or not public_key.get( "type") == "RSA" or not public_key.get("value"): raise RemoteInitiatedServerError( _("Authentication document at %(auth_document_url)s did not contain an RSA public key.", auth_document_url=auth_document_url), _("Remote authentication document")) public_key = public_key.get("value") public_key = RSA.importKey(public_key) encryptor = PKCS1_OAEP.new(public_key) normalized_url = IntegrationClient.normalize_url(start_url) client = get_one(self._db, IntegrationClient, url=normalized_url) if not client: client, ignore = IntegrationClient.register(self._db, start_url) shared_secret = client.shared_secret encrypted_secret = encryptor.encrypt(str(shared_secret)) return dict(metadata=dict( shared_secret=base64.b64encode(encrypted_secret))) def check_client_authorization(self, collection, client): """Verify that an IntegrationClient is whitelisted for access to the collection.""" external_library_urls = ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, collection.external_integration).json_value if client.url not in [ IntegrationClient.normalize_url(url) for url in external_library_urls ]: