예제 #1
0
 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 __init__(self, integration, library=None):
     _db = Session.object_session(integration)
     if not library:
         raise CannotLoadConfiguration("Google Analytics can't be configured without a library.")
     url_setting = ConfigurationSetting.for_externalintegration(ExternalIntegration.URL, integration)
     self.url = url_setting.value or self.DEFAULT_URL
     self.tracking_id = ConfigurationSetting.for_library_and_externalintegration(
         _db, self.TRACKING_ID, library, integration,
     ).value
     if not self.tracking_id:
         raise CannotLoadConfiguration("Missing tracking id for library %s" % library.short_name)
예제 #3
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
예제 #4
0
    def save(self, db, setting_name, value):
        """Save the value as as a new configuration setting

        :param db: Database session
        :type db: sqlalchemy.orm.session.Session

        :param setting_name: Name of the library's configuration setting
        :type setting_name: string

        :param value: Value to be saved
        :type value: Any
        """
        integration = self._integration_owner.external_integration(db)
        ConfigurationSetting.for_externalintegration(
            setting_name,
            integration).value = value
 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
예제 #6
0
    def load(self, db, setting_name):
        """Loads and returns the library's configuration setting

        :param db: Database session
        :type db: sqlalchemy.orm.session.Session

        :param setting_name: Name of the library's configuration setting
        :type setting_name: string

        :return: Any
        """
        integration = self._integration_owner.external_integration(db)
        value = ConfigurationSetting.for_externalintegration(
            setting_name,
            integration).value

        return value
 def __init__(self, integration, library=None):
     _db = Session.object_session(integration)
     if not library:
         raise CannotLoadConfiguration(
             "Google Analytics can't be configured without a library.")
     url_setting = ConfigurationSetting.for_externalintegration(
         ExternalIntegration.URL, integration)
     self.url = url_setting.value or self.DEFAULT_URL
     self.tracking_id = ConfigurationSetting.for_library_and_externalintegration(
         _db,
         self.TRACKING_ID,
         library,
         integration,
     ).value
     if not self.tracking_id:
         raise CannotLoadConfiguration(
             "Missing tracking id for library %s" % library.short_name)
예제 #8
0
    def test_load_settings_correctly_loads_menu_values(self):
        # Arrange
        manager = create_autospec(spec=CirculationManager)
        manager._db = PropertyMock(return_value=self._db)
        controller = CollectionSettingsController(manager)

        # We'll be using affiliation_attributes configuration setting defined in the ProQuest integration.
        affiliation_attributes_key = (
            ProQuestOPDS2ImporterConfiguration.affiliation_attributes.key)
        expected_affiliation_attributes = [
            SAMLAttributeType.eduPersonPrincipalName.name,
            SAMLAttributeType.eduPersonScopedAffiliation.name,
        ]
        protocol_settings = [
            ProQuestOPDS2ImporterConfiguration.affiliation_attributes.
            to_settings()
        ]
        collection_settings = None
        collection = self._default_collection

        # We need to explicitly set the value of "affiliation_attributes" configuration setting.
        ConfigurationSetting.for_externalintegration(
            affiliation_attributes_key,
            collection.external_integration).value = json.dumps(
                expected_affiliation_attributes)

        # Act
        settings = controller.load_settings(protocol_settings, collection,
                                            collection_settings)

        # Assert
        assert True == (affiliation_attributes_key in settings)

        # We want to make sure that the result setting array contains a correct value in a list format.
        saved_affiliation_attributes = settings[affiliation_attributes_key]
        assert expected_affiliation_attributes == saved_affiliation_attributes
예제 #9
0
    def push(self,
             stage,
             url_for,
             catalog_url=None,
             do_get=HTTP.debuggable_get,
             do_post=HTTP.debuggable_post,
             key=None):
        """Attempt to register a library with a RemoteRegistry.

        NOTE: this method does a database commit (by calling
        _set_public_key) so that when the remote registry asks for the
        library's Authentication For OPDS document, the public key is
        found and included in that document.

        NOTE: This method is designed to be used in a
        controller. Other callers may use this method, but they must be
        able to render a ProblemDetail when there's a failure.

        NOTE: The application server must be running when this method
        is called, because part of the OPDS Directory Registration
        Protocol is the remote server retrieving the library's
        Authentication For OPDS document.

        :param stage: Either TESTING_STAGE or PRODUCTION_STAGE
        :param url_for: Flask url_for() or equivalent, used to generate URLs
            for the application server.
        :param do_get: Mockable method to make a GET request.
        :param do_post: Mockable method to make a POST request.
        :param key: Pass in an RsaKey object to use a specific public key
            rather than generating a new one.

        :return: A ProblemDetail if there was a problem; otherwise True.
        """
        # Assume that the registration will fail.
        #
        # TODO: If a registration has previously succeeded, failure to
        # re-register probably means a maintenance of the status quo,
        # not a change of success to failure. But we don't have any way
        # of being sure.
        self.status_field.value = self.FAILURE_STATUS

        if stage not in self.VALID_REGISTRATION_STAGES:
            return INVALID_INPUT.detailed(
                _("%r is not a valid registration stage") % stage)

        # Before we can start the registration protocol, we must fetch
        # the remote catalog's URL and extract the link to the
        # registration resource that kicks off the protocol.
        catalog_url = catalog_url or self.integration.url
        response = do_get(catalog_url)
        if isinstance(response, ProblemDetail):
            return response

        result = self._extract_catalog_information(response)
        if isinstance(result, ProblemDetail):
            return result
        register_url, vendor_id = result

        # Store the vendor id as a ConfigurationSetting on the integration
        # -- it'll be the same value for all libraries.
        if vendor_id:
            ConfigurationSetting.for_externalintegration(
                AuthdataUtility.VENDOR_ID_KEY,
                self.integration).value = vendor_id

        # Set a public key for the library.
        encryptor = self._set_public_key(key)
        if isinstance(encryptor, ProblemDetail):
            return encryptor

        # Build the document we'll be sending to the registration URL.
        payload = self._create_registration_payload(url_for, stage)
        if isinstance(payload, ProblemDetail):
            return payload

        # Send the document.
        response = self._send_registration_request(register_url, payload,
                                                   do_post)
        if isinstance(response, ProblemDetail):
            return response
        catalog = json.loads(response.content)

        # Process the result.
        return self._process_registration_result(catalog, encryptor, stage)
예제 #10
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)
예제 #11
0
    def test_push(self):
        # Test the other methods orchestrated by the push() method.

        class MockRegistry(RemoteRegistry):
            def fetch_catalog(self, catalog_url, do_get):
                # Pretend to fetch a root catalog and extract a
                # registration URL from it.
                self.fetch_catalog_called_with = (catalog_url, do_get)
                return "register_url", "vendor_id"

        class MockRegistration(Registration):
            def _create_registration_payload(self, url_for, stage):
                self.payload_ingredients = (url_for, stage)
                return dict(payload="this is it")

            def _create_registration_headers(self):
                self._create_registration_headers_called = True
                return dict(Header="Value")

            def _send_registration_request(self, register_url, headers,
                                           payload, do_post):
                self._send_registration_request_called_with = (
                    register_url,
                    headers,
                    payload,
                    do_post,
                )
                return MockRequestsResponse(200,
                                            content=json.dumps("you did it!"))

            def _process_registration_result(self, catalog, encryptor, stage):
                self._process_registration_result_called_with = (
                    catalog,
                    encryptor,
                    stage,
                )
                return "all done!"

        # If there is no preexisting key pair set up for the library,
        # registration fails. (This normally won't happen because the
        # key pair is set up when the LibraryAuthenticator is
        # initialized.)
        library = self._default_library
        registry = MockRegistry(self.integration)
        registration = MockRegistration(registry, library)
        stage = Registration.TESTING_STAGE
        url_for = object()
        catalog_url = "http://catalog/"
        do_get = object()
        do_post = object()

        def push():
            return registration.push(stage, url_for, catalog_url, do_get,
                                     do_post)

        result = push()
        expect = "Library %s has no key pair set." % library.short_name
        assert expect == result.detail

        # When a key pair is present, registration is kicked off, and
        # in this case it succeeds.
        key_pair_setting = ConfigurationSetting.for_library(
            Configuration.KEY_PAIR, library)
        public_key, private_key = Configuration.key_pair(key_pair_setting)
        result = push()
        assert "all done!" == result

        # But there were many steps towards this result.

        # First, MockRegistry.fetch_catalog() was called, in an attempt
        # to find the registration URL inside the root catalog.
        assert (catalog_url, do_get) == registry.fetch_catalog_called_with

        # fetch_catalog() returned a registration URL and
        # a vendor ID. The registration URL was used later on...
        #
        # The vendor ID was set as a ConfigurationSetting on
        # the ExternalIntegration associated with this registry.
        assert ("vendor_id" == ConfigurationSetting.for_externalintegration(
            AuthdataUtility.VENDOR_ID_KEY, self.integration).value)

        # _create_registration_payload was called to create the body
        # of the registration request.
        assert (url_for, stage) == registration.payload_ingredients

        # _create_registration_headers was called to create the headers
        # sent along with the request.
        assert True == registration._create_registration_headers_called

        # Then _send_registration_request was called, POSTing the
        # payload to "register_url", the registration URL we got earlier.
        results = registration._send_registration_request_called_with
        assert (
            "register_url",
            {
                "Header": "Value"
            },
            dict(payload="this is it"),
            do_post,
        ) == results

        # Finally, the return value of that method was loaded as JSON
        # and passed into _process_registration_result, along with
        # a cipher created from the private key. (That cipher would be used
        # to decrypt anything the foreign site signed using this site's
        # public key.)
        results = registration._process_registration_result_called_with
        message, cipher, actual_stage = results
        assert "you did it!" == message
        assert cipher._key.exportKey().decode("utf-8") == private_key
        assert actual_stage == stage

        # If a nonexistent stage is provided a ProblemDetail is the result.
        result = registration.push("no such stage", url_for, catalog_url,
                                   do_get, do_post)
        assert INVALID_INPUT.uri == result.uri
        assert "'no such stage' is not a valid registration stage" == result.detail

        # Now in reverse order, let's replace the mocked methods so
        # that they return ProblemDetail documents. This tests that if
        # there is a failure at any stage, the ProblemDetail is
        # propagated.

        # The push() function will no longer push anything, so rename it.
        cause_problem = push

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not process registration result")

        registration._process_registration_result = fail
        problem = cause_problem()
        assert "could not process registration result" == problem.detail

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not send registration request")

        registration._send_registration_request = fail
        problem = cause_problem()
        assert "could not send registration request" == problem.detail

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not create registration payload")

        registration._create_registration_payload = fail
        problem = cause_problem()
        assert "could not create registration payload" == problem.detail

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed("could not fetch catalog")

        registry.fetch_catalog = fail
        problem = cause_problem()
        assert "could not fetch catalog" == problem.detail
예제 #12
0
    def from_config(cls, library, _db=None):
        """Initialize an AuthdataUtility from site configuration.

        :return: An AuthdataUtility if one is configured; otherwise None.

        :raise CannotLoadConfiguration: If an AuthdataUtility is
            incompletely configured.
        """
        _db = _db or Session.object_session(library)
        if not _db:
            raise ValueError(
                "No database connection provided and could not derive one from Library object!"
            )
        # Use a version of the library
        library = _db.merge(library, load=False)

        # Try to find an external integration with a configured Vendor ID.
        integrations = _db.query(ExternalIntegration).outerjoin(
            ExternalIntegration.libraries).filter(
                ExternalIntegration.protocol ==
                ExternalIntegration.OPDS_REGISTRATION,
                ExternalIntegration.goal == ExternalIntegration.DISCOVERY_GOAL,
                Library.id == library.id)

        integration = None
        for possible_integration in integrations:
            vendor_id = ConfigurationSetting.for_externalintegration(
                cls.VENDOR_ID_KEY, possible_integration).value
            if vendor_id:
                integration = possible_integration
                break

        library_uri = ConfigurationSetting.for_library(
            Configuration.WEBSITE_URL, library).value

        if not integration:
            return None

        vendor_id = integration.setting(cls.VENDOR_ID_KEY).value
        library_short_name = ConfigurationSetting.for_library_and_externalintegration(
            _db, ExternalIntegration.USERNAME, library, integration).value
        secret = ConfigurationSetting.for_library_and_externalintegration(
            _db, ExternalIntegration.PASSWORD, library, integration).value

        other_libraries = None
        adobe_integration = ExternalIntegration.lookup(
            _db,
            ExternalIntegration.ADOBE_VENDOR_ID,
            ExternalIntegration.DRM_GOAL,
            library=library)
        if adobe_integration:
            other_libraries = adobe_integration.setting(
                cls.OTHER_LIBRARIES_KEY).json_value
        other_libraries = other_libraries or dict()

        if (not vendor_id or not library_uri or not library_short_name
                or not secret):
            raise CannotLoadConfiguration(
                "Short Client Token configuration is incomplete. "
                "vendor_id, username, password and "
                "Library website_url must all be defined.")
        if '|' in library_short_name:
            raise CannotLoadConfiguration(
                "Library short name cannot contain the pipe character.")
        return cls(vendor_id, library_uri, library_short_name, secret,
                   other_libraries)
예제 #13
0
    def test_push(self):
        """Test the other methods orchestrated by the push() method.
        """
        class Mock(Registration):
            def _extract_catalog_information(self, response):
                self.initial_catalog_response = response
                return "register_url", "vendor_id"

            def _create_registration_payload(self, url_for, stage):
                self.payload_ingredients = (url_for, stage)
                return dict(payload="this is it")

            def _create_registration_headers(self):
                self._create_registration_headers_called = True
                return dict(Header="Value")

            def _send_registration_request(self, register_url, headers,
                                           payload, do_post):
                self._send_registration_request_called_with = (register_url,
                                                               headers,
                                                               payload,
                                                               do_post)
                return MockRequestsResponse(200,
                                            content=json.dumps("you did it!"))

            def _process_registration_result(self, catalog, encryptor, stage):
                self._process_registration_result_called_with = (catalog,
                                                                 encryptor,
                                                                 stage)
                return "all done!"

            def mock_do_get(self, url):
                self.do_get_called_with = url
                return "A fake catalog"

        # If there is no preexisting key pair set up for the library,
        # registration fails. (This normally won't happen because the
        # key pair is set up when the LibraryAuthenticator is
        # initialized.
        library = self._default_library
        registration = Mock(self.registry, library)
        stage = Registration.TESTING_STAGE
        url_for = object()
        catalog_url = "http://catalog/"
        do_post = object()

        def push():
            return registration.push(stage, url_for, catalog_url,
                                     registration.mock_do_get, do_post)

        result = push()
        expect = "Library %s has no key pair set." % library.short_name
        eq_(expect, result.detail)

        # When a key pair is present, registration is kicked off, and
        # in this case it succeeds.
        key_pair_setting = ConfigurationSetting.for_library(
            Configuration.KEY_PAIR, library)
        public_key, private_key = Configuration.key_pair(key_pair_setting)
        result = registration.push(stage, url_for, catalog_url,
                                   registration.mock_do_get, do_post)
        eq_("all done!", result)

        # But there were many steps towards this result.

        # First, do_get was called on the catalog URL.
        eq_(catalog_url, registration.do_get_called_with)

        # Then, the catalog was passed into _extract_catalog_information.
        eq_("A fake catalog", registration.initial_catalog_response)

        # _extract_catalog_information returned a registration URL and
        # a vendor ID. The registration URL was used later on...
        #
        # The vendor ID was set as a ConfigurationSetting on
        # the ExternalIntegration associated with this registry.
        eq_(
            "vendor_id",
            ConfigurationSetting.for_externalintegration(
                AuthdataUtility.VENDOR_ID_KEY, self.integration).value)

        # _create_registration_payload was called to create the body
        # of the registration request.
        eq_((url_for, stage), registration.payload_ingredients)

        # _create_registration_headers was called to create the headers
        # sent along with the request.
        eq_(True, registration._create_registration_headers_called)

        # Then _send_registration_request was called, POSTing the
        # payload to "register_url", the registration URL we got earlier.
        results = registration._send_registration_request_called_with
        eq_(("register_url", {
            "Header": "Value"
        }, dict(payload="this is it"), do_post), results)

        # Finally, the return value of that method was loaded as JSON
        # and passed into _process_registration_result, along with
        # a cipher created from the private key. (That cipher would be used
        # to decrypt anything the foreign site signed using this site's
        # public key.)
        results = registration._process_registration_result_called_with
        message, cipher, actual_stage = results
        eq_("you did it!", message)
        eq_(cipher._key.exportKey(), private_key)
        eq_(actual_stage, stage)

        # If a nonexistent stage is provided a ProblemDetail is the result.
        result = registration.push("no such stage", url_for, catalog_url,
                                   registration.mock_do_get, do_post)
        eq_(INVALID_INPUT.uri, result.uri)
        eq_("'no such stage' is not a valid registration stage", result.detail)

        # Now in reverse order, let's replace the mocked methods so
        # that they return ProblemDetail documents. This tests that if
        # there is a failure at any stage, the ProblemDetail is
        # propagated.

        # The push() function will no longer push anything, so rename it.
        cause_problem = push

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not process registration result")

        registration._process_registration_result = fail
        problem = cause_problem()
        eq_("could not process registration result", problem.detail)

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not send registration request")

        registration._send_registration_request = fail
        problem = cause_problem()
        eq_("could not send registration request", problem.detail)

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not create registration payload")

        registration._create_registration_payload = fail
        problem = cause_problem()
        eq_("could not create registration payload", problem.detail)

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not extract catalog information")

        registration._extract_catalog_information = fail
        problem = cause_problem()
        eq_("could not extract catalog information", problem.detail)
예제 #14
0
    def push(self, stage, url_for, catalog_url=None, do_get=HTTP.debuggable_get,
             do_post=HTTP.debuggable_post):
        """Attempt to register a library with a RemoteRegistry.

        NOTE: This method is designed to be used in a
        controller. Other callers may use this method, but they must be
        able to render a ProblemDetail when there's a failure.

        NOTE: The application server must be running when this method
        is called, because part of the OPDS Directory Registration
        Protocol is the remote server retrieving the library's
        Authentication For OPDS document.

        :param stage: Either TESTING_STAGE or PRODUCTION_STAGE
        :param url_for: Flask url_for() or equivalent, used to generate URLs
            for the application server.
        :param do_get: Mockable method to make a GET request.
        :param do_post: Mockable method to make a POST request.

        :return: A ProblemDetail if there was a problem; otherwise True.
        """
        # Assume that the registration will fail.
        #
        # TODO: If a registration has previously succeeded, failure to
        # re-register probably means a maintenance of the status quo,
        # not a change of success to failure. But we don't have any way
        # of being sure.
        self.status_field.value = self.FAILURE_STATUS

        if stage not in self.VALID_REGISTRATION_STAGES:
            return INVALID_INPUT.detailed(
                _("%r is not a valid registration stage") % stage
            )

        # Verify that a public/private key pair exists for this library.
        # This key pair is created during initialization of the
        # LibraryAuthenticator, so this should always be present.
        #
        # We can't just create the key pair here because the process
        # of pushing a registration involves the other site making a
        # request to the circulation manager. This means the key pair
        # needs to be committed to the database _before_ the push
        # attempt starts.
        key_pair = ConfigurationSetting.for_library(
            Configuration.KEY_PAIR, self.library).json_value
        if not key_pair:
            # TODO: We could create the key pair _here_. The database
            # session will be committed at the end of this request,
            # so the push attempt would succeed if repeated.
            return SHARED_SECRET_DECRYPTION_ERROR.detailed(
                _("Library %(library)s has no key pair set.",
                  library=self.library.short_name)
            )
        public_key, private_key = key_pair
        cipher = Configuration.cipher(private_key)

        # Before we can start the registration protocol, we must fetch
        # the remote catalog's URL and extract the link to the
        # registration resource that kicks off the protocol.
        catalog_url = catalog_url or self.integration.url
        response = do_get(catalog_url)
        if isinstance(response, ProblemDetail):
            return response

        result = self._extract_catalog_information(response)
        if isinstance(result, ProblemDetail):
            return result
        register_url, vendor_id = result

        # Store the vendor id as a ConfigurationSetting on the integration
        # -- it'll be the same value for all libraries.
        if vendor_id:
            ConfigurationSetting.for_externalintegration(
                AuthdataUtility.VENDOR_ID_KEY, self.integration
            ).value = vendor_id

        # Build the document we'll be sending to the registration URL.
        payload = self._create_registration_payload(url_for, stage)

        if isinstance(payload, ProblemDetail):
            return payload

        headers = self._create_registration_headers()
        if isinstance(headers, ProblemDetail):
            return headers

        # Send the document.
        response = self._send_registration_request(
            register_url, headers, payload, do_post
        )

        if isinstance(response, ProblemDetail):
            return response
        catalog = json.loads(response.content)

        # Process the result.
        return self._process_registration_result(catalog, cipher, stage)
예제 #15
0
    def push(self,
             stage,
             url_for,
             catalog_url=None,
             do_get=HTTP.debuggable_get,
             do_post=HTTP.debuggable_post):
        """Attempt to register a library with a RemoteRegistry.

        NOTE: This method is designed to be used in a
        controller. Other callers may use this method, but they must be
        able to render a ProblemDetail when there's a failure.

        NOTE: The application server must be running when this method
        is called, because part of the OPDS Directory Registration
        Protocol is the remote server retrieving the library's
        Authentication For OPDS document.

        :param stage: Either TESTING_STAGE or PRODUCTION_STAGE
        :param url_for: Flask url_for() or equivalent, used to generate URLs
            for the application server.
        :param do_get: Mockable method to make a GET request.
        :param do_post: Mockable method to make a POST request.

        :return: A ProblemDetail if there was a problem; otherwise True.
        """
        # Assume that the registration will fail.
        #
        # TODO: If a registration has previously succeeded, failure to
        # re-register probably means a maintenance of the status quo,
        # not a change of success to failure. But we don't have any way
        # of being sure.
        self.status_field.value = self.FAILURE_STATUS

        if stage not in self.VALID_REGISTRATION_STAGES:
            return INVALID_INPUT.detailed(
                _("%r is not a valid registration stage") % stage)

        # Verify that a public/private key pair exists for this library.
        # This key pair is created during initialization of the
        # LibraryAuthenticator, so this should always be present.
        #
        # We can't just create the key pair here because the process
        # of pushing a registration involves the other site making a
        # request to the circulation manager. This means the key pair
        # needs to be committed to the database _before_ the push
        # attempt starts.
        key_pair = ConfigurationSetting.for_library(Configuration.KEY_PAIR,
                                                    self.library).json_value
        if not key_pair:
            # TODO: We could create the key pair _here_. The database
            # session will be committed at the end of this request,
            # so the push attempt would succeed if repeated.
            return SHARED_SECRET_DECRYPTION_ERROR.detailed(
                _("Library %(library)s has no key pair set.",
                  library=self.library.short_name))
        public_key, private_key = key_pair
        cipher = Configuration.cipher(private_key)

        # Before we can start the registration protocol, we must fetch
        # the remote catalog's URL and extract the link to the
        # registration resource that kicks off the protocol.
        catalog_url = catalog_url or self.integration.url
        response = do_get(catalog_url)
        if isinstance(response, ProblemDetail):
            return response

        result = self._extract_catalog_information(response)
        if isinstance(result, ProblemDetail):
            return result
        register_url, vendor_id = result

        # Store the vendor id as a ConfigurationSetting on the integration
        # -- it'll be the same value for all libraries.
        if vendor_id:
            ConfigurationSetting.for_externalintegration(
                AuthdataUtility.VENDOR_ID_KEY,
                self.integration).value = vendor_id

        # Build the document we'll be sending to the registration URL.
        payload = self._create_registration_payload(url_for, stage)

        if isinstance(payload, ProblemDetail):
            return payload

        headers = self._create_registration_headers()
        if isinstance(headers, ProblemDetail):
            return headers

        # Send the document.
        response = self._send_registration_request(register_url, headers,
                                                   payload, do_post)

        if isinstance(response, ProblemDetail):
            return response
        catalog = json.loads(response.content)

        # Process the result.
        return self._process_registration_result(catalog, cipher, stage)
예제 #16
0
    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)))
예제 #17
0
        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"))
예제 #18
0
    def from_config(cls, library: Library, _db=None):
        """Initialize an AuthdataUtility from site configuration.

        The library must be successfully registered with a discovery
        integration in order for that integration to be a candidate
        to provide configuration for the AuthdataUtility.

        :return: An AuthdataUtility if one is configured; otherwise None.

        :raise CannotLoadConfiguration: If an AuthdataUtility is
            incompletely configured.
        """
        _db = _db or Session.object_session(library)
        if not _db:
            raise ValueError(
                "No database connection provided and could not derive one from Library object!"
            )
        # Use a version of the library
        library = _db.merge(library, load=False)

        # Try to find an external integration with a configured Vendor ID.
        integrations = (_db.query(ExternalIntegration).outerjoin(
            ExternalIntegration.libraries).filter(
                ExternalIntegration.protocol ==
                ExternalIntegration.OPDS_REGISTRATION,
                ExternalIntegration.goal == ExternalIntegration.DISCOVERY_GOAL,
                Library.id == library.id,
            ))

        for possible_integration in integrations:
            vendor_id = ConfigurationSetting.for_externalintegration(
                cls.VENDOR_ID_KEY, possible_integration).value
            registration_status = (
                ConfigurationSetting.for_library_and_externalintegration(
                    _db,
                    RegistrationConstants.LIBRARY_REGISTRATION_STATUS,
                    library,
                    possible_integration,
                ).value)
            if (vendor_id and registration_status
                    == RegistrationConstants.SUCCESS_STATUS):
                integration = possible_integration
                break
        else:
            return None

        library_uri = ConfigurationSetting.for_library(
            Configuration.WEBSITE_URL, library).value

        vendor_id = integration.setting(cls.VENDOR_ID_KEY).value
        library_short_name = ConfigurationSetting.for_library_and_externalintegration(
            _db, ExternalIntegration.USERNAME, library, integration).value
        secret = ConfigurationSetting.for_library_and_externalintegration(
            _db, ExternalIntegration.PASSWORD, library, integration).value

        other_libraries = None
        adobe_integration = ExternalIntegration.lookup(
            _db,
            ExternalIntegration.ADOBE_VENDOR_ID,
            ExternalIntegration.DRM_GOAL,
            library=library,
        )
        if adobe_integration:
            other_libraries = adobe_integration.setting(
                cls.OTHER_LIBRARIES_KEY).json_value
        other_libraries = other_libraries or dict()

        if not vendor_id or not library_uri or not library_short_name or not secret:
            raise CannotLoadConfiguration(
                "Short Client Token configuration is incomplete. "
                "vendor_id (%s), username (%s), password (%s) and "
                "Library website_url (%s) must all be defined." %
                (vendor_id, library_uri, library_short_name, secret))
        if "|" in library_short_name:
            raise CannotLoadConfiguration(
                "Library short name cannot contain the pipe character.")
        return cls(vendor_id, library_uri, library_short_name, secret,
                   other_libraries)
예제 #19
0
    def test_push(self):
        """Test the other methods orchestrated by the push() method.
        """
        class Mock(Registration):
            def _extract_catalog_information(self, response):
                self.initial_catalog_response = response
                return "register_url", "vendor_id"

            def _set_public_key(self, key):
                self._set_public_key_called_with = key
                return "an encryptor"

            def _create_registration_payload(self, url_for, stage):
                self.payload_ingredients = (url_for, stage)
                return dict(payload="this is it")

            def _send_registration_request(self, register_url, payload,
                                           do_post):
                self._send_registration_request_called_with = (register_url,
                                                               payload,
                                                               do_post)
                return MockRequestsResponse(200,
                                            content=json.dumps("you did it!"))

            def _process_registration_result(self, catalog, encryptor, stage):
                self._process_registration_result_called_with = (catalog,
                                                                 encryptor,
                                                                 stage)
                return "all done!"

            def mock_do_get(self, url):
                self.do_get_called_with = url
                return "A fake catalog"

        # First of all, test success.
        registration = Mock(self.registry, self._default_library)
        stage = Registration.TESTING_STAGE
        url_for = object()
        catalog_url = "http://catalog/"
        do_post = object()
        key = object()
        result = registration.push(stage, url_for, catalog_url,
                                   registration.mock_do_get, do_post, key)

        # Ultimately the push succeeded.
        eq_("all done!", result)

        # But there were many steps towards this result.

        # First, do_get was called on the catalog URL.
        eq_(catalog_url, registration.do_get_called_with)

        # Then, the catalog was passed into _extract_catalog_information.
        eq_("A fake catalog", registration.initial_catalog_response)

        # _extract_catalog_information returned a registration URL and
        # a vendor ID. The registration URL was used later on...
        #
        # The vendor ID was set as a ConfigurationSetting on
        # the ExternalIntegration associated with this registry.
        eq_(
            "vendor_id",
            ConfigurationSetting.for_externalintegration(
                AuthdataUtility.VENDOR_ID_KEY, self.integration).value)

        # _set_public_key() was called to create an encryptor object.
        # It returned an encryptor (here mocked as the string "an encryptor")
        # to be used later.
        eq_(key, registration._set_public_key_called_with)

        # _create_registration_payload was called to create the body
        # of the registration request.
        eq_((url_for, stage), registration.payload_ingredients)

        # Then _send_registration_request was called, POSTing the
        # payload to "register_url", the registration URL we got earlier.
        results = registration._send_registration_request_called_with
        eq_(("register_url", dict(payload="this is it"), do_post), results)

        # Finally, the return value of that method was loaded as JSON
        # and passed into _process_registration_result, along with
        # the encryptor obtained from _set_public_key()
        results = registration._process_registration_result_called_with
        eq_(("you did it!", "an encryptor", stage), results)

        # If a nonexistent stage is provided a ProblemDetail is the result.
        result = registration.push("no such stage", url_for, catalog_url,
                                   registration.mock_do_get, do_post, key)
        eq_(INVALID_INPUT.uri, result.uri)
        eq_("'no such stage' is not a valid registration stage", result.detail)

        # Now in reverse order, let's replace the mocked methods so
        # that they return ProblemDetail documents. This tests that if
        # there is a failure at any stage, the ProblemDetail is
        # propagated.
        def cause_problem():
            """Try the same method call that worked before; it won't work
            anymore.
            """
            return registration.push(stage, url_for, catalog_url,
                                     registration.mock_do_get, do_post, key)

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not process registration result")

        registration._process_registration_result = fail
        problem = cause_problem()
        eq_("could not process registration result", problem.detail)

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not send registration request")

        registration._send_registration_request = fail
        problem = cause_problem()
        eq_("could not send registration request", problem.detail)

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not create registration payload")

        registration._create_registration_payload = fail
        problem = cause_problem()
        eq_("could not create registration payload", problem.detail)

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed("could not set public key")

        registration._set_public_key = fail
        problem = cause_problem()
        eq_("could not set public key", problem.detail)

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not extract catalog information")

        registration._extract_catalog_information = fail
        problem = cause_problem()
        eq_("could not extract catalog information", problem.detail)
예제 #20
0
    def test_push(self):
        """Test the other methods orchestrated by the push() method.
        """

        class Mock(Registration):

            def _extract_catalog_information(self, response):
                self.initial_catalog_response = response
                return "register_url", "vendor_id"

            def _create_registration_payload(self, url_for, stage):
                self.payload_ingredients = (url_for, stage)
                return dict(payload="this is it")

            def _create_registration_headers(self):
                self._create_registration_headers_called = True
                return dict(Header="Value")

            def _send_registration_request(
                    self, register_url, headers, payload, do_post
            ):
                self._send_registration_request_called_with = (
                    register_url, headers, payload, do_post
                )
                return MockRequestsResponse(
                    200, content=json.dumps("you did it!")
                )

            def _process_registration_result(self, catalog, encryptor, stage):
                self._process_registration_result_called_with = (
                    catalog, encryptor, stage
                )
                return "all done!"

            def mock_do_get(self, url):
                self.do_get_called_with = url
                return "A fake catalog"

        # If there is no preexisting key pair set up for the library,
        # registration fails. (This normally won't happen because the
        # key pair is set up when the LibraryAuthenticator is
        # initialized.
        library = self._default_library
        registration = Mock(self.registry, library)
        stage = Registration.TESTING_STAGE
        url_for = object()
        catalog_url = "http://catalog/"
        do_post = object()
        def push():
            return registration.push(
                stage, url_for, catalog_url, registration.mock_do_get, do_post
            )

        result = push()
        expect = "Library %s has no key pair set." % library.short_name
        eq_(expect, result.detail)

        # When a key pair is present, registration is kicked off, and
        # in this case it succeeds.
        key_pair_setting = ConfigurationSetting.for_library(
            Configuration.KEY_PAIR, library
        )
        public_key, private_key = Configuration.key_pair(key_pair_setting)
        result = registration.push(
            stage, url_for, catalog_url, registration.mock_do_get, do_post
        )
        eq_("all done!", result)

        # But there were many steps towards this result.

        # First, do_get was called on the catalog URL.
        eq_(catalog_url, registration.do_get_called_with)

        # Then, the catalog was passed into _extract_catalog_information.
        eq_("A fake catalog", registration.initial_catalog_response)

        # _extract_catalog_information returned a registration URL and
        # a vendor ID. The registration URL was used later on...
        #
        # The vendor ID was set as a ConfigurationSetting on
        # the ExternalIntegration associated with this registry.
        eq_(
            "vendor_id",
            ConfigurationSetting.for_externalintegration(
                AuthdataUtility.VENDOR_ID_KEY, self.integration
            ).value
        )

        # _create_registration_payload was called to create the body
        # of the registration request.
        eq_((url_for, stage), registration.payload_ingredients)

        # _create_registration_headers was called to create the headers
        # sent along with the request.
        eq_(True, registration._create_registration_headers_called)

        # Then _send_registration_request was called, POSTing the
        # payload to "register_url", the registration URL we got earlier.
        results = registration._send_registration_request_called_with
        eq_(
            ("register_url", {"Header": "Value"}, dict(payload="this is it"),
             do_post),
            results
        )

        # Finally, the return value of that method was loaded as JSON
        # and passed into _process_registration_result, along with
        # a cipher created from the private key. (That cipher would be used
        # to decrypt anything the foreign site signed using this site's
        # public key.)
        results = registration._process_registration_result_called_with
        message, cipher, actual_stage = results
        eq_("you did it!", message)
        eq_(cipher._key.exportKey(), private_key)
        eq_(actual_stage, stage)

        # If a nonexistent stage is provided a ProblemDetail is the result.
        result = registration.push(
            "no such stage", url_for, catalog_url, registration.mock_do_get,
            do_post
        )
        eq_(INVALID_INPUT.uri, result.uri)
        eq_("'no such stage' is not a valid registration stage",
            result.detail)

        # Now in reverse order, let's replace the mocked methods so
        # that they return ProblemDetail documents. This tests that if
        # there is a failure at any stage, the ProblemDetail is
        # propagated.

        # The push() function will no longer push anything, so rename it.
        cause_problem = push

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not process registration result"
            )
        registration._process_registration_result = fail
        problem = cause_problem()
        eq_("could not process registration result", problem.detail)

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not send registration request"
            )
        registration._send_registration_request = fail
        problem = cause_problem()
        eq_("could not send registration request", problem.detail)

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not create registration payload"
            )
        registration._create_registration_payload = fail
        problem = cause_problem()
        eq_("could not create registration payload", problem.detail)

        def fail(*args, **kwargs):
            return INVALID_REGISTRATION.detailed(
                "could not extract catalog information"
            )
        registration._extract_catalog_information = fail
        problem = cause_problem()
        eq_("could not extract catalog information", problem.detail)
        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")
    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)