Ejemplo n.º 1
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)
Ejemplo n.º 2
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_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)
Ejemplo n.º 4
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 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)
Ejemplo n.º 7
0
def authenticated_client_from_request(_db, required=True):
    header = request.headers.get('Authorization')
    if header and 'bearer' in header.lower():
        shared_secret = base64.b64decode(header.split(' ')[1])
        client = IntegrationClient.authenticate(_db, shared_secret)
        if client:
            return client
    if not required and not header:
        # In the case that authentication is not required
        # (i.e. URN lookup) return None instead of an error.
        return None
    return INVALID_CREDENTIALS
Ejemplo n.º 8
0
def authenticated_client_from_request(_db, required=True):
    header = request.authorization
    if header:
        key, secret = header.username, header.password
        client = IntegrationClient.authenticate(_db, key, secret)
        if client:
            return client
    if not required and not header:
        # In the case that authentication is not required
        # (i.e. URN lookup) return None instead of an error.
        return None
    return INVALID_CREDENTIALS
def authenticated_client_from_request(_db, required=True):
    header = request.headers.get('Authorization')
    if header and 'bearer' in header.lower():
        shared_secret = base64.b64decode(header.split(' ')[1])
        client = IntegrationClient.authenticate(_db, shared_secret)
        if client:
            return client
    if not required and not header:
        # In the case that authentication is not required
        # (i.e. URN lookup) return None instead of an error.
        return None
    return INVALID_CREDENTIALS
Ejemplo n.º 10
0
    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)
Ejemplo n.º 11
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)
Ejemplo n.º 12
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)
Ejemplo n.º 13
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.key
        print "CLIENT SECRET: %s" % plaintext_secret
        self._db.commit()
Ejemplo n.º 14
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
Ejemplo n.º 15
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:])
Ejemplo n.º 18
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)))
    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)
Ejemplo n.º 20
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)
Ejemplo n.º 21
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:
Ejemplo n.º 22
0
    def register(self, do_get=HTTP.get_with_timeout):

        # 'url' points to a document containing a public key that
        # should be used to sign the secret.
        public_key_url = request.form.get('url')
        if not public_key_url:
            return NO_AUTH_URL

        log = logging.getLogger("Public key integration document (%s)" %
                                public_key_url)

        # 'jwt' is a JWT that proves the client making this request
        # controls the private key.
        #
        # For backwards compatibility purposes, it's okay not to
        # provide a JWT, but it may lead to situations where
        # the client doesn't know their shared secret and can't reset
        # it.
        jwt_token = request.form.get('jwt')

        try:
            response = do_get(public_key_url,
                              allowed_response_codes=['2xx', '3xx'])
        except Exception as e:
            log.error("Error retrieving URL", exc_info=e)
            return REMOTE_INTEGRATION_ERROR.detailed(
                _("Could not retrieve public key URL %(url)s",
                  url=public_key_url))

        content_type = None
        if response.headers:
            content_type = response.headers.get('Content-Type')

        if not (response.content and content_type == OPDS_2_MEDIA_TYPE):
            # There's no JSON to speak of.
            log.error("Could not find OPDS 2 document: %s/%s",
                      response.content, content_type)
            return INVALID_INTEGRATION_DOCUMENT.detailed(
                _("Not an integration document: %(doc)s",
                  doc=response.content))

        public_key_response = response.json()

        url = public_key_response.get('id')
        if not url:
            message = _(
                "The public key integration document is missing an id.")
            log.error(unicode(message))
            return INVALID_INTEGRATION_DOCUMENT.detailed(message)

        # Remove any library-specific URL elements.
        def base_url(full_url):
            scheme, netloc, path, parameters, query, fragment = urlparse.urlparse(
                full_url)
            return '%s://%s' % (scheme, netloc)

        client_url = base_url(url)
        base_public_key_url = base_url(public_key_url)
        if not client_url == base_public_key_url:
            log.error(
                "ID of OPDS 2 document (%s) doesn't match submitted URL (%s)",
                client_url, base_public_key_url)
            return INVALID_INTEGRATION_DOCUMENT.detailed(
                _("The public key integration document id (%(id)s) doesn't match submitted url %(url)s",
                  id=client_url,
                  url=base_public_key_url))

        public_key = public_key_response.get('public_key')
        if not (public_key and public_key.get('type') == 'RSA'
                and public_key.get('value')):
            message = _(
                "The public key integration document is missing an RSA public_key."
            )
            log.error(unicode(message))
            return INVALID_INTEGRATION_DOCUMENT.detailed(message)
        public_key_text = public_key.get('value')
        public_key = RSA.importKey(public_key_text)
        encryptor = PKCS1_OAEP.new(public_key)

        submitted_secret = None
        auth_header = request.headers.get('Authorization')
        if auth_header and isinstance(
                auth_header, basestring) and 'bearer' in auth_header.lower():
            token = auth_header.split(' ')[1]
            submitted_secret = base64.b64decode(token)

        if jwt_token:
            # In the new system, the 'token' must be a JWT whose
            # signature can be verified with the public key.
            try:
                parsed = jwt.decode(jwt_token,
                                    public_key_text,
                                    algorithm='RS256')
            except Exception, e:
                return INVALID_CREDENTIALS.detailed(
                    _("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()
        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()
Ejemplo n.º 24
0
                _("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 [
Ejemplo n.º 25
0
    def register(self, do_get=HTTP.get_with_timeout):

        # 'url' points to a document containing a public key that
        # should be used to sign the secret.
        public_key_url = request.form.get('url')
        if not public_key_url:
            return NO_AUTH_URL

        log = logging.getLogger(
            "Public key integration document (%s)" % public_key_url
        )

        # 'jwt' is a JWT that proves the client making this request
        # controls the private key.
        #
        # For backwards compatibility purposes, it's okay not to
        # provide a JWT, but it may lead to situations where
        # the client doesn't know their shared secret and can't reset
        # it.
        jwt_token = request.form.get('jwt')

        try:
            response = do_get(
                public_key_url, allowed_response_codes=['2xx', '3xx']
            )
        except Exception as e:
            log.error("Error retrieving URL", exc_info=e)
            return REMOTE_INTEGRATION_ERROR.detailed(
                _("Could not retrieve public key URL %(url)s",
                  url=public_key_url)
            )

        content_type = None
        if response.headers:
            content_type = response.headers.get('Content-Type')

        if not (response.content and content_type == OPDS_2_MEDIA_TYPE):
            # There's no JSON to speak of.
            log.error("Could not find OPDS 2 document: %s/%s",
                      response.content, content_type)
            return INVALID_INTEGRATION_DOCUMENT.detailed(
                _("Not an integration document: %(doc)s", doc=response.content)
            )

        public_key_response = response.json()

        url = public_key_response.get('id')
        if not url:
            message = _("The public key integration document is missing an id.")
            log.error(unicode(message))
            return INVALID_INTEGRATION_DOCUMENT.detailed(message)

        # Remove any library-specific URL elements.
        def base_url(full_url):
            scheme, netloc, path, parameters, query, fragment = urlparse.urlparse(full_url)
            return '%s://%s' % (scheme, netloc)

        client_url = base_url(url)
        base_public_key_url = base_url(public_key_url)
        if not client_url == base_public_key_url:
            log.error(
                "ID of OPDS 2 document (%s) doesn't match submitted URL (%s)",
                client_url, base_public_key_url
            )
            return INVALID_INTEGRATION_DOCUMENT.detailed(
                _("The public key integration document id (%(id)s) doesn't match submitted url %(url)s", id=client_url, url=base_public_key_url)
            )

        public_key = public_key_response.get('public_key')
        if not (public_key and public_key.get('type') == 'RSA' and public_key.get('value')):
            message = _("The public key integration document is missing an RSA public_key.")
            log.error(unicode(message))
            return INVALID_INTEGRATION_DOCUMENT.detailed(message)
        public_key_text = public_key.get('value')
        public_key = RSA.importKey(public_key_text)
        encryptor = PKCS1_OAEP.new(public_key)

        submitted_secret = None
        auth_header = request.headers.get('Authorization')
        if auth_header and isinstance(auth_header, basestring) and 'bearer' in auth_header.lower():
            token = auth_header.split(' ')[1]
            submitted_secret = base64.b64decode(token)

        if jwt_token:
            # In the new system, the 'token' must be a JWT whose
            # signature can be verified with the public key.
            try:
                parsed = jwt.decode(
                    jwt_token, public_key_text, algorithm='RS256'
                )
            except Exception, e:
                return INVALID_CREDENTIALS.detailed(
                    _("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()
Ejemplo n.º 26
0
            # 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)