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 update_client_url(self):
        """Updates the URL of a IntegrationClient"""
        client = authenticated_client_from_request(self._db)
        if isinstance(client, ProblemDetail):
            return client

        url = request.args.get('client_url')
        if not url:
            return INVALID_INPUT.detailed("No 'client_url' provided")

        client.url = IntegrationClient.normalize_url(urllib.unquote(url))

        return make_response("", HTTP_OK)
Exemple #3
0
    def test_register(self):
        # An auth document URL is required to register.
        assert_raises(InvalidInputException, self.shared_collection.register,
                      self.collection, None)

        # If the url doesn't return a valid auth document, there's an exception.
        auth_response = "not json"

        def do_get(*args, **kwargs):
            return MockRequestsResponse(200, content=auth_response)

        assert_raises(RemoteInitiatedServerError,
                      self.shared_collection.register,
                      self.collection,
                      "http://library.org/auth",
                      do_get=do_get)

        # The auth document also must have a link to the library's catalog.
        auth_response = json.dumps({"links": []})
        assert_raises(RemoteInitiatedServerError,
                      self.shared_collection.register,
                      self.collection,
                      "http://library.org/auth",
                      do_get=do_get)

        # If no external library URLs are configured, no one can register.
        auth_response = json.dumps(
            {"links": [{
                "href": "http://library.org",
                "rel": "start"
            }]})
        ConfigurationSetting.for_externalintegration(
            BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS,
            self.collection.external_integration).value = None
        assert_raises(AuthorizationFailedException,
                      self.shared_collection.register,
                      self.collection,
                      "http://library.org/auth",
                      do_get=do_get)

        # If the library's URL isn't in the configuration, it can't register.
        auth_response = json.dumps({
            "links": [{
                "href": "http://differentlibrary.org",
                "rel": "start"
            }]
        })
        ConfigurationSetting.for_externalintegration(
            BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS,
            self.collection.external_integration).value = json.dumps(
                ["http://library.org"])
        assert_raises(AuthorizationFailedException,
                      self.shared_collection.register,
                      self.collection,
                      "http://differentlibrary.org/auth",
                      do_get=do_get)

        # Or if the public key is missing from the auth document.
        auth_response = json.dumps(
            {"links": [{
                "href": "http://library.org",
                "rel": "start"
            }]})
        assert_raises(RemoteInitiatedServerError,
                      self.shared_collection.register,
                      self.collection,
                      "http://library.org/auth",
                      do_get=do_get)

        auth_response = json.dumps({
            "public_key": {
                "type": "not RSA",
                "value": "123"
            },
            "links": [{
                "href": "http://library.org",
                "rel": "start"
            }]
        })
        assert_raises(RemoteInitiatedServerError,
                      self.shared_collection.register,
                      self.collection,
                      "http://library.org/auth",
                      do_get=do_get)

        auth_response = json.dumps({
            "public_key": {
                "type": "RSA"
            },
            "links": [{
                "href": "http://library.org",
                "rel": "start"
            }]
        })
        assert_raises(RemoteInitiatedServerError,
                      self.shared_collection.register,
                      self.collection,
                      "http://library.org/auth",
                      do_get=do_get)

        # Here's an auth document with a valid key.
        key = RSA.generate(2048)
        public_key = key.publickey().exportKey()
        encryptor = PKCS1_OAEP.new(key)
        auth_response = json.dumps({
            "public_key": {
                "type": "RSA",
                "value": public_key
            },
            "links": [{
                "href": "http://library.org",
                "rel": "start"
            }]
        })
        response = self.shared_collection.register(self.collection,
                                                   "http://library.org/auth",
                                                   do_get=do_get)

        # An IntegrationClient has been created.
        client = get_one(
            self._db,
            IntegrationClient,
            url=IntegrationClient.normalize_url("http://library.org/"))
        decrypted_secret = encryptor.decrypt(
            base64.b64decode(
                response.get("metadata", {}).get("shared_secret")))
        eq_(client.shared_secret, decrypted_secret)
                _("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")
        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 [
    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)))
    def test_register(self):
        # An auth document URL is required to register.
        assert_raises(InvalidInputException, self.shared_collection.register,
                      self.collection, None)

        # If the url doesn't return a valid auth document, there's an exception.
        auth_response = "not json"
        def do_get(*args, **kwargs):
            return MockRequestsResponse(200, content=auth_response)
        assert_raises(RemoteInitiatedServerError, self.shared_collection.register,
                      self.collection, "http://library.org/auth", do_get=do_get)

        # The auth document also must have a link to the library's catalog.
        auth_response = json.dumps({"links": []})
        assert_raises(RemoteInitiatedServerError, self.shared_collection.register,
                      self.collection, "http://library.org/auth", do_get=do_get)

        # If no external library URLs are configured, no one can register.
        auth_response = json.dumps({"links": [{"href": "http://library.org", "rel": "start"}]})
        ConfigurationSetting.for_externalintegration(
            BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, self.collection.external_integration
        ).value = None
        assert_raises(AuthorizationFailedException, self.shared_collection.register,
                      self.collection, "http://library.org/auth", do_get=do_get)

        # If the library's URL isn't in the configuration, it can't register.
        auth_response = json.dumps({"links": [{"href": "http://differentlibrary.org", "rel": "start"}]})
        ConfigurationSetting.for_externalintegration(
            BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, self.collection.external_integration
        ).value = json.dumps(["http://library.org"])
        assert_raises(AuthorizationFailedException, self.shared_collection.register,
                      self.collection, "http://differentlibrary.org/auth", do_get=do_get)

        # Or if the public key is missing from the auth document.
        auth_response = json.dumps({"links": [{"href": "http://library.org", "rel": "start"}]})
        assert_raises(RemoteInitiatedServerError, self.shared_collection.register,
                      self.collection, "http://library.org/auth", do_get=do_get)

        auth_response = json.dumps({"public_key": { "type": "not RSA", "value": "123" },
                                    "links": [{"href": "http://library.org", "rel": "start"}]})
        assert_raises(RemoteInitiatedServerError, self.shared_collection.register,
                      self.collection, "http://library.org/auth", do_get=do_get)

        auth_response = json.dumps({"public_key": { "type": "RSA" },
                                    "links": [{"href": "http://library.org", "rel": "start"}]})
        assert_raises(RemoteInitiatedServerError, self.shared_collection.register,
                      self.collection, "http://library.org/auth", do_get=do_get)


        # Here's an auth document with a valid key.
        key = RSA.generate(2048)
        public_key = key.publickey().exportKey()
        encryptor = PKCS1_OAEP.new(key)
        auth_response = json.dumps({"public_key": { "type": "RSA", "value": public_key },
                                    "links": [{"href": "http://library.org", "rel": "start"}]})
        response = self.shared_collection.register(self.collection, "http://library.org/auth", do_get=do_get)

        # An IntegrationClient has been created.
        client = get_one(self._db, IntegrationClient, url=IntegrationClient.normalize_url("http://library.org/"))
        decrypted_secret = encryptor.decrypt(base64.b64decode(response.get("metadata", {}).get("shared_secret")))
        eq_(client.shared_secret, decrypted_secret)
        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
        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()