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)
Exemple #2
0
    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)
Exemple #3
0
    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)
Exemple #6
0
    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)
Exemple #7
0
    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()
Exemple #9
0
 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
Exemple #10
0
    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:])
Exemple #13
0
                    _("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
        ]: