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)
示例#2
0
    def test_lookup_patron_affiliation_id(
        self, _, subject, expected_affiliation_id, affiliation_attributes=None
    ):
        # Arrange
        credential_manager = ProQuestCredentialManager()
        patron = self._patron()

        if subject:
            expected_token = json.dumps(subject, cls=SAMLSubjectJSONEncoder)

            data_source = DataSource.lookup(
                self._db,
                BaseSAMLAuthenticationProvider.TOKEN_DATA_SOURCE_NAME,
                autocreate=True,
            )
            Credential.temporary_token_create(
                self._db,
                data_source,
                BaseSAMLAuthenticationProvider.TOKEN_TYPE,
                patron,
                datetime.timedelta(hours=1),
                expected_token,
            )

        # Act
        if affiliation_attributes:
            token = credential_manager.lookup_patron_affiliation_id(
                self._db, patron, affiliation_attributes
            )
        else:
            token = credential_manager.lookup_patron_affiliation_id(self._db, patron)

        # Assert
        assert expected_affiliation_id == token
示例#3
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)
示例#4
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
示例#5
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)
示例#6
0
 def test_authdata_lookup_failure_wrong_token(self):
     temp_token, ignore = Credential.temporary_token_create(
         self._db, self.data_source, self.model.TEMPORARY_TOKEN_TYPE,
         self.bob_patron, datetime.timedelta(seconds=60))
     urn, label = self.model.authdata_lookup("nosuchauthdata")
     eq_(None, urn)
     eq_(None, label)
示例#7
0
    def _create_token(self, db, patron, subject, cm_session_lifetime=None):
        """Create a Credential object that ties the given patron to the
            given provider token.

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

        :param patron: Patron object
        :type patron: Patron

        :param subject: SAML subject
        :type subject: api.saml.metadata.model.SAMLSubject

        :param cm_session_lifetime: (Optional) Circulation Manager's session lifetime expressed in days
        :type cm_session_lifetime: Optional[int]

        :return: Credential object
        :rtype: Credential
        """
        session_lifetime = subject.valid_till

        if cm_session_lifetime:
            session_lifetime = datetime.timedelta(
                days=int(cm_session_lifetime))

        token = self._create_token_value(subject)
        data_source, ignore = self._get_token_data_source(db)

        return Credential.temporary_token_create(db, data_source,
                                                 self.TOKEN_TYPE, patron,
                                                 session_lifetime, token)
示例#8
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 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)
示例#10
0
    def _lookup_saml_token(self, db, patron):
        """Look up for a SAML token.

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

        :param patron: Patron object
        :type patron: core.model.patron.Patron

        :return: SAML subject (if any)
        :rtype: Optional[api.saml.metadata.Subject]
        """
        self._logger.debug("Started looking up for a SAML token")

        from api.authenticator import BaseSAMLAuthenticationProvider

        credential = Credential.lookup_by_patron(
            db,
            BaseSAMLAuthenticationProvider.TOKEN_DATA_SOURCE_NAME,
            BaseSAMLAuthenticationProvider.TOKEN_TYPE,
            patron,
            allow_persistent_token=False,
            auto_create_datasource=True,
        )

        self._logger.debug(
            "Finished looking up for a SAML token: {0}".format(credential)
        )

        return credential
    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
示例#12
0
    def authenticated_patron(self, db, token):
        """Go from a token to an authenticated Patron.

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

        :param token: The provider token extracted from the Authorization
            header. This is _not_ the bearer token found in
            the Authorization header; it's the provider-specific token
            embedded in that token.
        :type token: Dict

        :return: A Patron, if one can be authenticated. None, if the
            credentials do not authenticate any particular patron. A
            ProblemDetail if an error occurs.
        :rtype: Union[Patron, ProblemDetail]
        """
        data_source, ignore = self._get_token_data_source(db)
        credential = Credential.lookup_by_token(db, data_source,
                                                self.TOKEN_TYPE, token)
        if credential:
            return credential.patron

        # This token wasn't in our database, or was expired. The
        # patron will have to log in through the SAML provider again
        # to get a new token.
        return None
示例#13
0
    def test_migrate_adobe_id_success(self):
        from api.opds import CirculationManagerAnnotator
        patron = self._patron()

        # This patron has a Credential containing their Adobe ID
        data_source = DataSource.lookup(self._db, DataSource.ADOBE)
        adobe_id = Credential(
            patron=patron, data_source=data_source,
            type=AdobeVendorIDModel.VENDOR_ID_UUID_TOKEN_TYPE,
            credential="My Adobe ID"
        )

        # Run the migration.
        new_credential, delegated_identifier = self.authdata.migrate_adobe_id(patron)
        
        # The patron now has _two_ Credentials -- the old one
        # containing the Adobe ID, and a new one.
        eq_(set([new_credential, adobe_id]), set(patron.credentials))

        # The new credential contains an anonymized patron identifier
        # used solely to connect the patron to their Adobe ID.
        eq_(AuthdataUtility.ADOBE_ACCOUNT_ID_PATRON_IDENTIFIER,
            new_credential.type)

        # We can use that identifier to look up a DelegatedPatronIdentifier
        # 
        def explode():
            # This method won't be called because the
            # DelegatedPatronIdentifier already exists.
            raise Exception()
        identifier, is_new = DelegatedPatronIdentifier.get_one_or_create(
            self._db, self.authdata.library_uri, new_credential.credential,
            DelegatedPatronIdentifier.ADOBE_ACCOUNT_ID, explode
        )
        eq_(delegated_identifier, identifier)
        eq_(False, is_new)
        eq_("My Adobe ID", identifier.delegated_identifier)

        # An integration-level test:
        # AdobeVendorIDModel.to_delegated_patron_identifier_uuid works
        # now.
        model = AdobeVendorIDModel(self._default_library, None, None)
        uuid, label = model.to_delegated_patron_identifier_uuid(
            self.authdata.library_uri, new_credential.credential
        )
        eq_("My Adobe ID", uuid)
        eq_('Delegated account ID My Adobe ID', label)
        
        # If we run the migration again, nothing new happens.
        new_credential_2, delegated_identifier_2 = self.authdata.migrate_adobe_id(patron)
        eq_(new_credential, new_credential_2)
        eq_(delegated_identifier, delegated_identifier_2)
        eq_(2, len(patron.credentials))
        uuid, label = model.to_delegated_patron_identifier_uuid(
            self.authdata.library_uri, new_credential.credential
        )
        eq_("My Adobe ID", uuid)
        eq_('Delegated account ID My Adobe ID', label)
示例#14
0
    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)
示例#15
0
    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
示例#16
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)      
示例#17
0
 def patron_from_authdata_lookup(self, authdata):
     """Look up a patron by their persistent authdata token."""
     credential = Credential.lookup_by_token(
         self._db, self.data_source, self.AUTHDATA_TOKEN_TYPE, 
         authdata, allow_persistent_token=True
     )
     if not credential:
         return None
     return credential.patron
 def urn_to_label(self, urn):
     credential = Credential.lookup_by_token(
         self._db, self.data_source, self.VENDOR_ID_UUID_TOKEN_TYPE, 
         urn, True)
     if not credential:
         return None
     patron = credential.patron
     uuid, label = self.uuid_and_label(credential.patron)
     return label
示例#19
0
    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
 def authdata_lookup(self, authdata):
     """Look up a patron by a temporary Adobe Vendor ID token. Return their
     Vendor ID UUID and their human-readable label.
     """
     credential = Credential.lookup_by_temporary_token(
         self._db, self.data_source, self.TEMPORARY_TOKEN_TYPE, 
         authdata)
     if not credential:
         return None, None
     return self.uuid_and_label(credential.patron)
    def test_authdata_lookup_failure_wrong_token(self):
        # Bob has an authdata token.
        token, ignore = Credential.persistent_token_create(
            self._db, self.data_source, self.model.AUTHDATA_TOKEN_TYPE,
            self.bob_patron)

        # But we look up a different token and get nothing.
        urn, label = self.model.authdata_lookup("nosuchauthdata")
        eq_(None, urn)
        eq_(None, label)
示例#22
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)
示例#23
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
示例#24
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
示例#25
0
    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')
示例#26
0
    def test_lookup_proquest_token_returns_token(self):
        # Arrange
        credential_manager = ProQuestCredentialManager()
        patron = self._patron()
        expected_token = "12345"

        Credential.temporary_token_create(
            self._db,
            self._data_source,
            ProQuestCredentialType.PROQUEST_JWT_TOKEN.value,
            patron,
            datetime.timedelta(hours=1),
            expected_token,
        )

        # Act
        token = credential_manager.lookup_proquest_token(self._db, patron)

        # Assert
        assert True == isinstance(token, Credential)
        assert expected_token == token.credential
示例#27
0
    def save_proquest_token(self, db, patron, duration, token):
        """Save a ProQuest JWT bearer token for later use.

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

        :param patron: Patron object
        :type patron: core.model.patron.Patron

        :param duration: How long this token can be valid
        :type duration: datetime.timedelta

        :param token: ProQuest JWT bearer token
        :type token: str

        :return: Credential object containing a new ProQuest JWT bearer token
        :rtype: Optional[core.model.credential.Credential]
        """
        if not is_session(db):
            raise ValueError('"db" argument must be a valid SQLAlchemy session')
        if not isinstance(patron, Patron):
            raise ValueError('"patron" argument must be an instance of Patron class')
        if not isinstance(duration, datetime.timedelta):
            raise ValueError(
                '"duration" argument must be an instance of datetime.timedelta class'
            )
        if not isinstance(token, str) or not token:
            raise ValueError('"token" argument must be a non-empty string')

        self._logger.debug(
            "Started saving a ProQuest JWT bearer token {0}".format(token)
        )

        data_source = DataSource.lookup(
            db, DataSourceConstants.PROQUEST, autocreate=True
        )
        credential, is_new = Credential.temporary_token_create(
            db,
            data_source,
            ProQuestCredentialType.PROQUEST_JWT_TOKEN.value,
            patron,
            duration,
            token,
        )

        self._logger.debug(
            "Finished saving a ProQuest JWT bearer token {0}: {1} (new = {2})".format(
                token, credential, is_new
            )
        )

        return credential
示例#28
0
    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)
示例#29
0
    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)
示例#30
0
    def test_save_proquest_token_saves_token(self):
        # Arrange
        credential_manager = ProQuestCredentialManager()
        patron = self._patron()
        expected_token = "12345"

        # Act
        credential_manager.save_proquest_token(
            self._db, patron, datetime.timedelta(hours=1), expected_token
        )
        token = Credential.lookup_by_patron(
            self._db,
            self._data_source.name,
            ProQuestCredentialType.PROQUEST_JWT_TOKEN.value,
            patron,
        )

        # Assert
        assert expected_token == token.credential
    def test_getter(self, _, credential_type, method_name, expected_result):
        # Arrange
        credential = Credential(credential=expected_result)

        with patch.object(
                Credential,
                "persistent_token_create") as persistent_token_create_mock:
            persistent_token_create_mock.return_value = (credential, True)

            method = getattr(self._factory, method_name)

            # Act
            result = method(self._db, self._patron)

            # Assert
            assert result == expected_result
            persistent_token_create_mock.assert_called_once_with(
                self._db, self._data_source, credential_type, self._patron,
                None)
示例#32
0
    def _create_token(self, db, patron, token, valid_till):
        """Creates a Credential object that ties the given patron to the
        given provider token.

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

        :param patron: Patron object
        :type patron: Patron

        :param token: Token containing SAML subject's metadata
        :type token: Dict

        :param valid_till: The time till which the SAML subject is valid
        :type valid_till: datetime.timedelta

        :return: Credential object
        :rtype: Credential
        """
        data_source, ignore = self._get_token_data_source(db)
        return Credential.temporary_token_create(db, data_source,
                                                 self.TOKEN_TYPE, patron,
                                                 valid_till, token)
示例#33
0
    def lookup_proquest_token(self, db, patron):
        """Look up for a JWT bearer token used required to use ProQuest API.

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

        :param patron: Patron object
        :type patron: core.model.patron.Patron

        :return: Credential object containing the existing ProQuest JWT bearer token (if any)
        :rtype: Optional[core.model.credential.Credential]
        """
        if not is_session(db):
            raise ValueError('"db" argument must be a valid SQLAlchemy session')
        if not isinstance(patron, Patron):
            raise ValueError('"patron" argument must be an instance of Patron class')

        self._logger.debug("Started looking up for a ProQuest JWT token")

        credential = Credential.lookup_by_patron(
            db,
            DataSourceConstants.PROQUEST,
            ProQuestCredentialType.PROQUEST_JWT_TOKEN.value,
            patron,
            allow_persistent_token=False,
            auto_create_datasource=True,
        )

        self._logger.debug(
            "Finished looking up for a ProQuest JWT token: {0}".format(credential)
        )

        if credential:
            return credential

        return None
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
示例#35
0
 def create_authdata(self, patron):
     credential, is_new = Credential.persistent_token_create(
         self._db, self.data_source, self.AUTHDATA_TOKEN_TYPE, patron)
     return 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,
        )
示例#37
0
 def _patron_credential_lookup(self, patron, refresh):
     return Credential.lookup(self._db, DataSource.ODILO, "OAuth Token", patron, refresh)
示例#38
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)
 def create_authdata(self, patron):
     credential, is_new = Credential.temporary_token_create(
         self._db, self.data_source, self.TEMPORARY_TOKEN_TYPE,
         patron, self.temporary_token_duration)
     return credential