Beispiel #1
0
    def test_uuid_and_label_creates_delegatedpatronid_from_credential(self):
       
        # This patron once used the old system to create an Adobe
        # account ID which was stored in a Credential. For whatever
        # reason, the migration script did not give them a
        # DelegatedPatronIdentifier.
        adobe = self.data_source
        def set_value(credential):
            credential.credential = "A dummy value"
        old_style_credential = Credential.lookup(
            self._db, adobe, self.model.VENDOR_ID_UUID_TOKEN_TYPE,
            self.bob_patron, set_value, True
        )

        # Now uuid_and_label works.
        uuid, label = self.model.uuid_and_label(self.bob_patron)
        eq_("A dummy value", uuid)
        eq_("Delegated account ID A dummy value", label)

        # There is now an anonymized identifier associated with Bob's
        # patron account.
        internal = DataSource.lookup(self._db, DataSource.INTERNAL_PROCESSING)
        bob_anonymized_identifier = Credential.lookup(
            self._db, internal,
            AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER,
            self.bob_patron, None
        )

        # That anonymized identifier is associated with a
        # DelegatedPatronIdentifier whose delegated_identifier is
        # taken from the old-style Credential.
        [bob_delegated_patron_identifier] = self._db.query(
            DelegatedPatronIdentifier).filter(
                DelegatedPatronIdentifier.patron_identifier
                ==bob_anonymized_identifier.credential
            ).all()
        eq_("A dummy value",
            bob_delegated_patron_identifier.delegated_identifier)

        # If the DelegatedPatronIdentifier and the Credential
        # have different values, the DelegatedPatronIdentifier wins.
        old_style_credential.credential = "A different value."
        uuid, label = self.model.uuid_and_label(self.bob_patron)
        eq_("A dummy value", uuid)
        
        # We can even delete the old-style Credential, and
        # uuid_and_label will still give the value that was stored in
        # it.
        self._db.delete(old_style_credential)
        self._db.commit()
        uuid, label = self.model.uuid_and_label(self.bob_patron)
        eq_("A dummy value", uuid)
Beispiel #2
0
 def get_patron_credential(self, patron, pin):
     """Create an OAuth token for the given patron."""
     def refresh(credential):
         return self.refresh_patron_access_token(
             credential, patron, pin)
     return Credential.lookup(
         self._db, DataSource.OVERDRIVE, "OAuth Token", patron, refresh)
Beispiel #3
0
    def patron_remote_identifier(self, patron):
        """Locate the identifier for the given Patron's account on the
        RBdigital side, creating a new RBdigital account if necessary.

        The identifier is cached in a persistent Credential object.

        :return: The remote identifier for this patron, taken from
        the corresponding Credential.
        """
        def refresher(credential):
            remote_identifier = self.patron_remote_identifier_lookup(patron)
            if not remote_identifier:
                remote_identifier = self.create_patron(patron)
            credential.credential = remote_identifier
            credential.expires = None

        _db = Session.object_session(patron)
        credential = Credential.lookup(
            _db,
            DataSource.RB_DIGITAL,
            Credential.IDENTIFIER_FROM_REMOTE_SERVICE,
            patron,
            refresher_method=refresher,
            allow_persistent_token=True)
        if not credential.credential:
            refresher(credential)
        return credential.credential
    def test_authdata_token_credential_lookup_success(self):

        # Create an authdata token Credential for Bob.
        now = datetime.datetime.utcnow()
        token, ignore = Credential.persistent_token_create(
            self._db, self.data_source, self.model.AUTHDATA_TOKEN_TYPE,
            self.bob_patron)

        # The token is persistent.
        eq_(None, token.expires)

        # Use that token to perform a lookup of Bob's Adobe Vendor ID
        # UUID.
        urn, label = self.model.authdata_lookup(token.credential)

        # There is now an anonymized identifier associated with Bob's
        # patron account.
        internal = DataSource.lookup(self._db, DataSource.INTERNAL_PROCESSING)
        bob_anonymized_identifier = Credential.lookup(
            self._db, internal,
            AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER,
            self.bob_patron, None)

        # That anonymized identifier is associated with a
        # DelegatedPatronIdentifier whose delegated_identifier is a
        # UUID.
        [bob_delegated_patron_identifier
         ] = self._db.query(DelegatedPatronIdentifier).filter(
             DelegatedPatronIdentifier.patron_identifier ==
             bob_anonymized_identifier.credential).all()

        # That UUID is the one returned by authdata_lookup.
        eq_(urn, bob_delegated_patron_identifier.delegated_identifier)
Beispiel #5
0
    def test_username_password_lookup_success(self):
        urn, label = self.model.standard_lookup(self.credentials)

        # There is now an anonymized identifier associated with Bob's
        # patron account.
        internal = DataSource.lookup(self._db, DataSource.INTERNAL_PROCESSING)
        bob_anonymized_identifier = Credential.lookup(
            self._db, internal,
            AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER,
            self.bob_patron, None
        )

        # That anonymized identifier is associated with a
        # DelegatedPatronIdentifier whose delegated_identifier is a
        # UUID.
        [bob_delegated_patron_identifier] = self._db.query(
            DelegatedPatronIdentifier).filter(
                DelegatedPatronIdentifier.patron_identifier
                ==bob_anonymized_identifier.credential
            ).all()

        eq_("Delegated account ID %s" % urn, label)
        eq_(urn, bob_delegated_patron_identifier.delegated_identifier)
        assert urn.startswith("urn:uuid:0")
        assert urn.endswith('685b35c00f05')
    def uuid_and_label(self, patron):
        """Create or retrieve a Vendor ID UUID and human-readable Vendor ID
        label for the given patron.
        """
        if not patron:
            return None, None

        def generate_uuid(credential):
            # This is the first time a credential has ever been
            # created for this patron. Set the value of the 
            # credential to a new UUID.
            print "GENERATING NEW UUID"
            credential.credential = self.uuid()

        credential = Credential.lookup(
            self._db, self.data_source, self.VENDOR_ID_UUID_TOKEN_TYPE,
            patron, generate_uuid, True)

        identifier = patron.authorization_identifier          
        if not identifier:
            # Maybe this should be an error, but even though the lack
            # of an authorization identifier is a problem, the problem
            # should manifest when the patron tries to actually use
            # their credential.
            return "Unknown card number.", "Unknown card number"
        return credential.credential, "Card number " + identifier
 def get_patron_credential(self, patron, pin):
     """Create an OAuth token for the given patron."""
     def refresh(credential):
         return self.refresh_patron_access_token(
             credential, patron, pin)
     return Credential.lookup(
         self._db, DataSource.OVERDRIVE, "OAuth Token", patron, refresh)
Beispiel #8
0
    def test_create_authdata(self):
        credential = self.model.create_authdata(self.bob_patron)

        # There's now a persistent token associated with Bob's
        # patron account, and that's the token returned by create_authdata()
        bob_authdata = Credential.lookup(
            self._db, self.data_source, self.model.AUTHDATA_TOKEN_TYPE,
            self.bob_patron, None)
        eq_(credential.credential, bob_authdata.credential)      
    def test_create_authdata(self):
        credential = self.model.create_authdata(self.bob_patron)

        # There's now a temporary token associated with Bob's
        # patron account, and that's the token returned by create_authdata()
        bob_authdata = Credential.lookup(
            self._db, self.data_source, self.model.TEMPORARY_TOKEN_TYPE,
            self.bob_patron, None)
        eq_(credential.credential, bob_authdata.credential)
    def token(self):
        if (self._credential and
            self._credential.expires > datetime.utcnow()):
            return self._credential.credential

        credential = Credential.lookup(
            self._db, self.source, None, None, self.refresh_credential
        )
        return credential.credential
Beispiel #11
0
    def _adobe_patron_identifier(self, patron):
        _db = Session.object_session(patron)
        internal = DataSource.lookup(_db, DataSource.INTERNAL_PROCESSING)

        def refresh(credential):
            credential.credential = str(uuid.uuid1())
        patron_identifier = Credential.lookup(
            _db, internal, AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER, patron,
            refresher_method=refresh, allow_persistent_token=True
        )
        return patron_identifier.credential
Beispiel #12
0
 def get_or_create_patron_identifier_credential(cls, patron):
     _db = Session.object_session(patron)
     def refresh(credential):
         credential.credential = str(uuid.uuid1())
     data_source = DataSource.lookup(_db, DataSource.INTERNAL_PROCESSING)
     patron_identifier_credential = Credential.lookup(
         _db, data_source,
         AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER,
         patron, refresher_method=refresh, allow_persistent_token=True
     )
     return patron_identifier_credential
    def test_standard_lookup_success(self):
        urn, label = self.model.standard_lookup(dict(username="******", password="******"))

        # There is now a UUID associated with Bob's patron account,
        # and that's the UUID returned by standard_lookup().
        bob_uuid = Credential.lookup(
            self._db, self.data_source, self.model.VENDOR_ID_UUID_TOKEN_TYPE,
            self.bob_patron, None)
        eq_("Card number 5", label)
        eq_(urn, bob_uuid.credential)
        assert urn.startswith("urn:uuid:0")
        assert urn.endswith('685b35c00f05')
    def test_process_patron(self):
        patron = self._patron()

        # This patron has old-style and new-style Credentials that link
        # them to Adobe account IDs (hopefully the same ID, though that
        # doesn't matter here.
        def set_value(credential):
            credential.value = "a credential"

        # Data source doesn't matter -- even if it's incorrect, a Credential
        # of the appropriate type will be deleted.
        data_source = DataSource.lookup(self._db, DataSource.OVERDRIVE)

        # Create two Credentials that will be deleted and one that will be
        # left alone.
        for type in (AdobeVendorIDModel.VENDOR_ID_UUID_TOKEN_TYPE,
                     AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER,
                     "Some other type"
        ):

            credential = Credential.lookup(
                self._db, data_source, type, patron,
                set_value, True
            )

        eq_(3, len(patron.credentials))

        # Run the patron through the script.
        script = AdobeAccountIDResetScript(self._db)

        # A dry run does nothing.
        script.delete = False
        script.process_patron(patron)
        self._db.commit()
        eq_(3, len(patron.credentials))

        # Now try it for real.
        script.delete = True
        script.process_patron(patron)
        self._db.commit()

        # The two Adobe-related credentials are gone. The other one remains.
        [credential] = patron.credentials
        eq_("Some other type", credential.type)
    def test_process_patron(self):
        patron = self._patron()

        # This patron has old-style and new-style Credentials that link
        # them to Adobe account IDs (hopefully the same ID, though that
        # doesn't matter here.
        def set_value(credential):
            credential.value = "a credential"

        # Data source doesn't matter -- even if it's incorrect, a Credential
        # of the appropriate type will be deleted.
        data_source = DataSource.lookup(self._db, DataSource.OVERDRIVE)

        # Create two Credentials that will be deleted and one that will be
        # left alone.
        for type in (AdobeVendorIDModel.VENDOR_ID_UUID_TOKEN_TYPE,
                     AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER,
                     "Some other type"
        ):

            credential = Credential.lookup(
                self._db, data_source, type, patron,
                set_value, True
            )

        eq_(3, len(patron.credentials))

        # Run the patron through the script.
        script = AdobeAccountIDResetScript(self._db)

        # A dry run does nothing.
        script.delete = False
        script.process_patron(patron)
        self._db.commit()
        eq_(3, len(patron.credentials))

        # Now try it for real.
        script.delete = True
        script.process_patron(patron)
        self._db.commit()

        # The two Adobe-related credentials are gone. The other one remains.
        [credential] = patron.credentials
        eq_("Some other type", credential.type)
    def test_authdata_lookup_success(self):
        now = datetime.datetime.utcnow()
        temp_token, ignore = Credential.temporary_token_create(
            self._db, self.data_source, self.model.TEMPORARY_TOKEN_TYPE,
            self.bob_patron, datetime.timedelta(seconds=60))
        old_expires = temp_token.expires
        assert temp_token.expires > now
        urn, label = self.model.authdata_lookup(temp_token.credential)

        # There is now a UUID associated with Bob's patron account,
        # and that's the UUID returned by standard_lookup().
        bob_uuid = Credential.lookup(
            self._db, self.data_source, self.model.VENDOR_ID_UUID_TOKEN_TYPE,
            self.bob_patron, None)
        eq_(urn, bob_uuid.credential)
        eq_("Card number 5", label)

        # Having been used once, the temporary token has been expired.
        assert temp_token.expires < now
Beispiel #17
0
    def test_smuggled_authdata_credential_success(self):
        # Bob's client has created a persistent token to authenticate him.
        now = datetime.datetime.utcnow()
        token, ignore = Credential.persistent_token_create(
            self._db, self.data_source, self.model.AUTHDATA_TOKEN_TYPE,
            self.bob_patron
        )

        # But Bob's client can't trigger the operation that will cause
        # Adobe to authenticate him via that token, so it passes in
        # the token credential as the 'username' and leaves the
        # password blank.
        urn, label = self.model.standard_lookup(
            dict(username=token.credential)
        )

        # There is now an anonymized identifier associated with Bob's
        # patron account.
        internal = DataSource.lookup(self._db, DataSource.INTERNAL_PROCESSING)
        bob_anonymized_identifier = Credential.lookup(
            self._db, internal,
            AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER,
            self.bob_patron, None
        )

        # That anonymized identifier is associated with a
        # DelegatedPatronIdentifier whose delegated_identifier is a
        # UUID.
        [bob_delegated_patron_identifier] = self._db.query(
            DelegatedPatronIdentifier).filter(
                DelegatedPatronIdentifier.patron_identifier
                ==bob_anonymized_identifier.credential
            ).all()

        # That UUID is the one returned by standard_lookup.
        eq_(urn, bob_delegated_patron_identifier.delegated_identifier)

        # A future attempt to authenticate with the token will succeed.
        urn, label = self.model.standard_lookup(
            dict(username=token.credential)
        )
        eq_(urn, bob_delegated_patron_identifier.delegated_identifier)
class OPDSForDistributorsAPI(BaseCirculationAPI):
    NAME = "OPDS for Distributors"
    DESCRIPTION = _("Import books from a distributor that requires authentication to get the OPDS feed and download books.")

    SETTINGS = OPDSImporter.SETTINGS + [
        {
            "key": ExternalIntegration.USERNAME,
            "label": _("Library's username or access key"),
        },
        {
            "key": ExternalIntegration.PASSWORD,
            "label": _("Library's password or secret key"),
        }
    ]

    SUPPORTED_MEDIA_TYPES = [Representation.EPUB_MEDIA_TYPE]

    delivery_mechanism_to_internal_format = {
        (type, DeliveryMechanism.NO_DRM): type for type in SUPPORTED_MEDIA_TYPES
    }

    def __init__(self, _db, collection):
        self.collection_id = collection.id
        self.data_source_name = collection.external_integration.setting(Collection.DATA_SOURCE_NAME_SETTING).value
        self.username = collection.external_integration.username
        self.password = collection.external_integration.password
        self.feed_url = collection.external_account_id
        self.auth_url = None

    def _request_with_timeout(self, method, url, *args, **kwargs):
        """Wrapper around HTTP.request_with_timeout to be overridden for tests."""
        return HTTP.request_with_timeout(method, url, *args, **kwargs)

    def _get_token(self, _db):
        # If this is the first time we're getting a token, we
        # need to find the authenticate url in the OPDS
        # authentication document.
        if not self.auth_url:
            response = self._request_with_timeout('GET', self.feed_url)

            if response.status_code != 401:
                # This feed doesn't require authentication, so
                # we need to find a link to the authentication document.
                feed = feedparser.parse(response.content)
                links = feed.get('feed', {}).get('links', [])
                auth_doc_links = [l for l in links if l['rel'] == "http://opds-spec.org/auth/document"]
                if not auth_doc_links:
                    raise LibraryAuthorizationFailedException()
                auth_doc_link = auth_doc_links[0].get("href")

                response = self._request_with_timeout('GET', auth_doc_link)

            try:
                auth_doc = json.loads(response.content)
            except Exception, e:
                raise LibraryAuthorizationFailedException()
            auth_types = auth_doc.get('authentication', [])
            credentials_types = [t for t in auth_types if t['type'] == "http://opds-spec.org/auth/oauth/client_credentials"]
            if not credentials_types:
                raise LibraryAuthorizationFailedException()

            links = credentials_types[0].get('links', [])
            auth_links = [l for l in links if l.get("rel") == "authenticate"]
            if not auth_links:
                raise LibraryAuthorizationFailedException()
            self.auth_url = auth_links[0].get("href")

        def refresh(credential):
            headers = dict()
            auth_header = "Basic %s" % base64.b64encode("%s:%s" % (self.username, self.password))
            headers['Authorization'] = auth_header
            headers['Content-Type'] = "application/x-www-form-urlencoded"
            body = dict(grant_type='client_credentials')
            token_response = self._request_with_timeout('POST', self.auth_url, data=body, headers=headers)
            token = json.loads(token_response.content)
            access_token = token.get("access_token")
            expires_in = token.get("expires_in")
            if not access_token or not expires_in:
                raise LibraryAuthorizationFailedException()
            credential.credential = access_token
            expires_in = expires_in
            credential.expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=expires_in)
        return Credential.lookup(_db, self.data_source_name,
                                 "OPDS For Distributors Bearer Token",
                                 patron=None,
                                 refresher_method=refresh,
                                 ).credential
class OPDSForDistributorsAPI(BaseCirculationAPI, HasSelfTests):
    NAME = "OPDS for Distributors"
    DESCRIPTION = _(
        "Import books from a distributor that requires authentication to get the OPDS feed and download books."
    )

    SETTINGS = OPDSImporter.BASE_SETTINGS + [
        {
            "key": ExternalIntegration.USERNAME,
            "label": _("Library's username or access key"),
            "required": True,
        }, {
            "key": ExternalIntegration.PASSWORD,
            "label": _("Library's password or secret key"),
            "required": True,
        }
    ]

    # In OPDS For Distributors, all items are gated through the
    # BEARER_TOKEN access control scheme.
    #
    # If the default client supports a given media type when
    # combined with the BEARER_TOKEN scheme, then we should import
    # titles with that media type...
    SUPPORTED_MEDIA_TYPES = [
        format for (format,
                    drm) in DeliveryMechanism.default_client_can_fulfill_lookup
        if drm == (DeliveryMechanism.BEARER_TOKEN) and format is not None
    ]

    # ...and we should map requests for delivery of that media type to
    # the (type, BEARER_TOKEN) DeliveryMechanism.
    delivery_mechanism_to_internal_format = {
        (type, DeliveryMechanism.BEARER_TOKEN): type
        for type in SUPPORTED_MEDIA_TYPES
    }

    def __init__(self, _db, collection):
        self.collection_id = collection.id
        self.external_integration_id = collection.external_integration.id
        self.data_source_name = collection.external_integration.setting(
            Collection.DATA_SOURCE_NAME_SETTING).value
        self.username = collection.external_integration.username
        self.password = collection.external_integration.password
        self.feed_url = collection.external_account_id
        self.auth_url = None

    def external_integration(self, _db):
        return get_one(_db,
                       ExternalIntegration,
                       id=self.external_integration_id)

    def _run_self_tests(self, _db):
        """Try to get a token."""
        yield self.run_test("Negotiate a fulfillment token", self._get_token,
                            _db)

    def _request_with_timeout(self, method, url, *args, **kwargs):
        """Wrapper around HTTP.request_with_timeout to be overridden for tests."""
        return HTTP.request_with_timeout(method, url, *args, **kwargs)

    def _get_token(self, _db):
        # If this is the first time we're getting a token, we
        # need to find the authenticate url in the OPDS
        # authentication document.
        if not self.auth_url:
            # Keep track of the most recent URL we retrieved for error
            # reporting purposes.
            current_url = self.feed_url
            response = self._request_with_timeout('GET', current_url)

            if response.status_code != 401:
                # This feed doesn't require authentication, so
                # we need to find a link to the authentication document.
                feed = feedparser.parse(response.content)
                links = feed.get('feed', {}).get('links', [])
                auth_doc_links = [
                    l for l in links
                    if l['rel'] == "http://opds-spec.org/auth/document"
                ]
                if not auth_doc_links:
                    raise LibraryAuthorizationFailedException(
                        "No authentication document link found in %s" %
                        current_url)
                current_url = auth_doc_links[0].get("href")

                response = self._request_with_timeout('GET', current_url)

            try:
                auth_doc = json.loads(response.content)
            except Exception, e:
                raise LibraryAuthorizationFailedException(
                    "Could not load authentication document from %s" %
                    current_url)
            auth_types = auth_doc.get('authentication', [])
            credentials_types = [
                t for t in auth_types if t['type'] ==
                "http://opds-spec.org/auth/oauth/client_credentials"
            ]
            if not credentials_types:
                raise LibraryAuthorizationFailedException(
                    "Could not find any credential-based authentication mechanisms in %s"
                    % current_url)

            links = credentials_types[0].get('links', [])
            auth_links = [l for l in links if l.get("rel") == "authenticate"]
            if not auth_links:
                raise LibraryAuthorizationFailedException(
                    "Could not find any authentication links in %s" %
                    current_url)
            self.auth_url = auth_links[0].get("href")

        def refresh(credential):
            headers = dict()
            auth_header = "Basic %s" % base64.b64encode(
                "%s:%s" % (self.username, self.password))
            headers['Authorization'] = auth_header
            headers['Content-Type'] = "application/x-www-form-urlencoded"
            body = dict(grant_type='client_credentials')
            token_response = self._request_with_timeout('POST',
                                                        self.auth_url,
                                                        data=body,
                                                        headers=headers)
            token = json.loads(token_response.content)
            access_token = token.get("access_token")
            expires_in = token.get("expires_in")
            if not access_token or not expires_in:
                raise LibraryAuthorizationFailedException(
                    "Document retrieved from %s is not a bearer token: %s" %
                    (self.auth_url, token_response.content))
            credential.credential = access_token
            expires_in = expires_in
            # We'll avoid edge cases by assuming the token expires 75%
            # into its useful lifetime.
            credential.expires = datetime.datetime.utcnow(
            ) + datetime.timedelta(seconds=expires_in * 0.75)

        return Credential.lookup(
            _db,
            self.data_source_name,
            "OPDS For Distributors Bearer Token",
            patron=None,
            refresher_method=refresh,
        )
Beispiel #20
0
 def _patron_credential_lookup(self, patron, refresh):
     return Credential.lookup(self._db, DataSource.ODILO, "OAuth Token", patron, refresh)
Beispiel #21
0
 def credential_object(self, refresh):
     """Look up the Credential object that allows us to use
     the Odilo API.
     """
     return Credential.lookup(self._db, DataSource.ODILO, None, None,
                              refresh)