def setup(self): super(TestVendorIDModel, self).setup() # This library is going to act as the Vendor ID server. self.vendor_id_library = self._default_library # This library can create Short Client Tokens that the Vendor # ID server will recognize. self.short_client_token_library = self._library( short_name="shortclienttoken" ) # Initialize the Adobe-specific ExternalIntegrations for both # libraries. self.initialize_adobe( self.vendor_id_library, [self.short_client_token_library] ) # Set up a simple authentication provider that validates # one specific patron. integration = self._external_integration(self._str) provider = SimpleAuthenticationProvider integration.setting(provider.TEST_IDENTIFIER).value = "validpatron" integration.setting(provider.TEST_PASSWORD).value = "password" self.authenticator = SimpleAuthenticationProvider( self._default_library, integration ) self.model = AdobeVendorIDModel( self._default_library, self.authenticator, self.TEST_NODE_VALUE) self.data_source = DataSource.lookup(self._db, DataSource.ADOBE) self.bob_patron = self.authenticator.authenticated_patron( self._db, dict(username="******", password="******"))
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)
def setup(self): super(TestVendorIDModel, self).setup() self.authenticator = DummyMilleniumPatronAPI() self.model = AdobeVendorIDModel(self._db, self.authenticator, self.TEST_NODE_VALUE) self.data_source = DataSource.lookup(self._db, DataSource.ADOBE) # Normally this test patron doesn't have an authorization identifier. # Let's make sure there is one so it'll show up as the label. self.bob_patron = self.authenticator.authenticated_patron( self._db, dict(username="******", password="******")) self.bob_patron.authorization_identifier = "5"
class TestVendorIDModel(VendorIDTest): TEST_NODE_VALUE = 114740953091845 credentials = dict(username="******", password="******") def setup(self): super(TestVendorIDModel, self).setup() # This library is going to act as the Vendor ID server. self.vendor_id_library = self._default_library # This library can create Short Client Tokens that the Vendor # ID server will recognize. self.short_client_token_library = self._library( short_name="shortclienttoken" ) # Initialize the Adobe-specific ExternalIntegrations for both # libraries. self.initialize_adobe( self.vendor_id_library, [self.short_client_token_library] ) # Set up a simple authentication provider that validates # one specific patron. integration = self._external_integration(self._str) provider = SimpleAuthenticationProvider integration.setting(provider.TEST_IDENTIFIER).value = "validpatron" integration.setting(provider.TEST_PASSWORD).value = "password" self.authenticator = SimpleAuthenticationProvider( self._default_library, integration ) self.model = AdobeVendorIDModel( self._default_library, self.authenticator, self.TEST_NODE_VALUE) self.data_source = DataSource.lookup(self._db, DataSource.ADOBE) self.bob_patron = self.authenticator.authenticated_patron( self._db, dict(username="******", password="******")) def test_uuid(self): u = self.model.uuid() # All UUIDs need to start with a 0 and end with the same node # value. assert u.startswith('urn:uuid:0') assert u.endswith('685b35c00f05') def test_uuid_and_label_respects_existing_id(self): uuid, label = self.model.uuid_and_label(self.bob_patron) uuid2, label2 = self.model.uuid_and_label(self.bob_patron) eq_(uuid, uuid2) eq_(label, label2) 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) 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_to_delegated_patron_identifier_uuid(self): foreign_uri = "http://your-library/" foreign_identifier = "foreign ID" # Pass in nothing and you get nothing. eq_((None, None), self.model.to_delegated_patron_identifier_uuid(foreign_uri, None) ) eq_((None, None), self.model.to_delegated_patron_identifier_uuid( None, foreign_identifier ) ) # Pass in a URI and identifier and you get a UUID and a label. uuid, label = self.model.to_delegated_patron_identifier_uuid( foreign_uri, foreign_identifier ) # We can't test a specific value for the UUID but we can test the label. eq_("Delegated account ID " + uuid, label) # And we can verify that a DelegatedPatronIdentifier was # created for the URI+identifier, and that it contains the # UUID. [dpi] = self._db.query(DelegatedPatronIdentifier).filter( DelegatedPatronIdentifier.library_uri==foreign_uri).filter( DelegatedPatronIdentifier.patron_identifier==foreign_identifier ).all() eq_(uuid, dpi.delegated_identifier) def test_authdata_lookup_delegated_patron_identifier_success(self): """Test that one library can perform an authdata lookup on a JWT generated by a different library. """ # Here's a library that's not a Vendor ID server, but which # can generate a JWT for one of its patrons. sct_library = self.short_client_token_library utility = AuthdataUtility.from_config(sct_library) vendor_id, jwt = utility.encode("Foreign patron") # Here's an AuthdataUtility for the library that _is_ # a Vendor ID server. vendor_id_utility = AuthdataUtility.from_config(self.vendor_id_library) # The Vendor ID library knows the secret it shares with the # other library -- initialize_adobe() took care of that. sct_library_uri = sct_library.setting(Configuration.WEBSITE_URL).value eq_("%s token secret" % sct_library.short_name, vendor_id_utility.secrets_by_library_uri[sct_library_uri] ) # Because this library shares the other library's secret, # it can decode a JWT issued by the other library, and # issue an Adobe ID (UUID). uuid, label = self.model.authdata_lookup(jwt) # We get the same result if we smuggle the JWT into # a username/password lookup as the username. uuid2, label2 = self.model.standard_lookup(dict(username=jwt)) eq_(uuid2, uuid) eq_(label2, label) # The UUID corresponds to a DelegatedPatronIdentifier, # associated with the foreign library and the patron # identifier that library encoded in its JWT. [dpi] = self._db.query(DelegatedPatronIdentifier).filter( DelegatedPatronIdentifier.library_uri==sct_library_uri).filter( DelegatedPatronIdentifier.patron_identifier=="Foreign patron" ).all() eq_(uuid, dpi.delegated_identifier) eq_("Delegated account ID %s" % uuid, label) def test_short_client_token_lookup_delegated_patron_identifier_success(self): """Test that one library can perform an authdata lookup on a short client token generated by a different library. """ # Here's a library that's not a Vendor ID server, but which can # generate a Short Client Token for one of its patrons. sct_library = self.short_client_token_library utility = AuthdataUtility.from_config(sct_library) vendor_id, short_client_token = utility.encode_short_client_token( "Foreign patron" ) # Here's an AuthdataUtility for the library that _is_ # a Vendor ID server. vendor_id_utility = AuthdataUtility.from_config(self.vendor_id_library) # The Vendor ID library knows the secret it shares with the # other library -- initialize_adobe() took care of that. sct_library_url = sct_library.setting(Configuration.WEBSITE_URL).value eq_("%s token secret" % sct_library.short_name, vendor_id_utility.secrets_by_library_uri[sct_library_url] ) # Because the Vendor ID library shares the Short Client Token # library's secret, it can decode a short client token issued # by that library, and issue an Adobe ID (UUID). token, signature = short_client_token.rsplit("|", 1) uuid, label = self.model.short_client_token_lookup( token, signature ) # The UUID corresponds to a DelegatedPatronIdentifier, # associated with the foreign library and the patron # identifier that library encoded in its JWT. [dpi] = self._db.query(DelegatedPatronIdentifier).filter( DelegatedPatronIdentifier.library_uri==sct_library_url).filter( DelegatedPatronIdentifier.patron_identifier=="Foreign patron" ).all() eq_(uuid, dpi.delegated_identifier) eq_("Delegated account ID %s" % uuid, label) # We get the same UUID and label by passing the token and # signature to standard_lookup as username and password. # (That's because standard_lookup calls short_client_token_lookup # behind the scenes.) credentials = dict(username=token, password=signature) new_uuid, new_label = self.model.standard_lookup(credentials) eq_(new_uuid, uuid) eq_(new_label, label) def test_short_client_token_lookup_delegated_patron_identifier_failure(self): uuid, label = self.model.short_client_token_lookup( "bad token", "bad signature" ) eq_(None, uuid) eq_(None, label) 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 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) 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) def test_authdata_lookup_failure_no_token(self): urn, label = self.model.authdata_lookup("nosuchauthdata") eq_(None, urn) eq_(None, label) 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) def test_urn_to_label_success(self): urn, label = self.model.standard_lookup(self.credentials) label2 = self.model.urn_to_label(urn) eq_(label, label2) eq_("Delegated account ID %s" % urn, label)
class TestVendorIDModel(DatabaseTest): TEST_NODE_VALUE = 114740953091845 def setup(self): super(TestVendorIDModel, self).setup() self.authenticator = DummyMilleniumPatronAPI() self.model = AdobeVendorIDModel(self._db, self.authenticator, self.TEST_NODE_VALUE) self.data_source = DataSource.lookup(self._db, DataSource.ADOBE) # Normally this test patron doesn't have an authorization identifier. # Let's make sure there is one so it'll show up as the label. self.bob_patron = self.authenticator.authenticated_patron( self._db, dict(username="******", password="******")) self.bob_patron.authorization_identifier = "5" def test_uuid(self): u = self.model.uuid() # All UUIDs need to start with a 0 and end with the same node # value. assert u.startswith('urn:uuid:0') assert u.endswith('685b35c00f05') def test_uuid_and_label_respects_existing_id(self): uuid, label = self.model.uuid_and_label(self.bob_patron) uuid2, label2 = self.model.uuid_and_label(self.bob_patron) eq_(uuid, uuid2) eq_(label, label2) 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 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_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 test_authdata_lookup_failure_no_token(self): urn, label = self.model.authdata_lookup("nosuchauthdata") eq_(None, urn) eq_(None, label) 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) def test_urn_to_label_success(self): urn, label = self.model.standard_lookup(dict(username="******", password="******")) label2 = self.model.urn_to_label(urn) eq_(label, label2) eq_("Card number 5", label) def test_urn_to_label_failure_no_active_credential(self): label = self.model.urn_to_label("bad urn") eq_(None, label) def test_urn_to_label_failure_incorrect_urn(self): urn, label = self.model.standard_lookup(dict(username="******", password="******")) label = self.model.urn_to_label("bad urn") eq_(None, label)