コード例 #1
0
    def test_decode_short_client_token_from_another_library(self):
        # Here's the AuthdataUtility used by another library.
        foreign_authdata = AuthdataUtility(
            vendor_id = "The Vendor ID",
            library_uri = "http://your-library.org/",
            library_short_name = "you",
            secret = "Your library secret",
        )
        
        patron_identifier = "Patron identifier"
        vendor_id, token = foreign_authdata.encode_short_client_token(
            patron_identifier
        )
        
        # Because we know the other library's secret, we're able to
        # decode the authdata.
        decoded = self.authdata.decode_short_client_token(token)
        eq_(("http://your-library.org/", "Patron identifier"), decoded)

        # If our secret for a library doesn't match the other
        # library's short token signing key, we can't decode the
        # authdata.
        foreign_authdata.short_token_signing_key = 'A new secret'
        vendor_id, token = foreign_authdata.encode_short_client_token(
            patron_identifier
        )
        assert_raises_regexp(
            ValueError, "Invalid signature for",
            self.authdata.decode_short_client_token, token
        )
コード例 #2
0
    def test_decode_from_another_library(self):        

        # Here's the AuthdataUtility used by another library.
        foreign_authdata = AuthdataUtility(
            vendor_id = "The Vendor ID",
            library_uri = "http://your-library.org/",
            library_short_name = "you",
            secret = "Your library secret",
        )
        
        patron_identifier = "Patron identifier"
        vendor_id, authdata = foreign_authdata.encode(patron_identifier)

        # Because we know the other library's secret, we're able to
        # decode the authdata.
        decoded = self.authdata.decode(authdata)
        eq_(("http://your-library.org/", "Patron identifier"), decoded)

        # If our secret doesn't match the other library's secret,
        # we can't decode the authdata
        foreign_authdata.secret = 'A new secret'
        vendor_id, authdata = foreign_authdata.encode(patron_identifier)
        assert_raises_regexp(
            DecodeError, "Signature verification failed",
            self.authdata.decode, authdata
        )
コード例 #3
0
 def setup(self):
     super(TestAuthdataUtility, self).setup()
     self.authdata = AuthdataUtility(
         vendor_id = "The Vendor ID",
         library_uri = "http://my-library.org/",
         library_short_name = "MyLibrary",
         secret = "My library secret",
         other_libraries = {
             "http://your-library.org/": ("you", "Your library secret")
         },
     )
コード例 #4
0
    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)
コード例 #5
0
    def test_decode_from_unknown_library_fails(self):

        # Here's the AuthdataUtility used by a library we don't know
        # about.
        foreign_authdata = AuthdataUtility(
            vendor_id="The Vendor ID",
            library_uri="http://some-other-library.org/",
            library_short_name="SomeOther",
            secret="Some other library secret",
        )
        vendor_id, authdata = foreign_authdata.encode("A patron")
        # They can encode, but we cna't decode.
        assert_raises_regexp(
            DecodeError, "Unknown library: http://some-other-library.org/",
            self.authdata.decode, authdata)
コード例 #6
0
    def test_decode_two_part_short_client_token_uses_adobe_base64_encoding(self):

        # The base64 encoding of this signature has a plus sign in it.
        signature = 'LbU}66%\\-4zt>R>_)\n2Q'
        encoded_signature = AuthdataUtility.adobe_base64_encode(signature)

        # We replace the plus sign with a colon.
        assert ':' in encoded_signature
        assert '+' not in encoded_signature
        
        # Make sure that decode_two_part_short_client_token properly
        # reverses that change when decoding the 'password'.
        class MockAuthdataUtility(AuthdataUtility):
            def _decode_short_client_token(self, token, supposed_signature):
                eq_(supposed_signature, signature)
                self.test_code_ran = True

        utility =  MockAuthdataUtility(
            vendor_id = "The Vendor ID",
            library_uri = "http://your-library.org/",
            library_short_name = "you",
            secret = "Your library secret",
        )
        utility.test_code_ran = False
        utility.decode_two_part_short_client_token(
            "username", encoded_signature
        )

        # The code in _decode_short_client_token ran. Since there was no
        # test failure, it ran successfully.
        eq_(True, utility.test_code_ran)
コード例 #7
0
    def test_short_client_token_encode_known_value(self):
        """Verify that the encoding algorithm gives a known value on known
        input.
        """
        value = self.authdata._encode_short_client_token(
            "a library", "a patron identifier", 1234.5
        )

        # Note the colon characters that replaced the plus signs in
        # what would otherwise be normal base64 text. Similarly for
        # the semicolon which replaced the slash, and the at sign which
        # replaced the equals sign.
        eq_('a library|1234.5|a patron identifier|YoNGn7f38mF531KSWJ;o1H0Z3chbC:uTE:t7pAwqYxM@',
            value
        )

        # Dissect the known value to show how it works.
        token, signature = value.rsplit("|", 1)

        # Signature is base64-encoded in a custom way that avoids
        # triggering an Adobe bug ; token is not.
        signature = AuthdataUtility.adobe_base64_decode(signature)

        # The token comes from the library name, the patron identifier,
        # and the time of creation.
        eq_("a library|1234.5|a patron identifier", token)

        # The signature comes from signing the token with the
        # secret associated with this library.
        expect_signature = self.authdata.short_token_signer.sign(
            token, self.authdata.short_token_signing_key
        )
        eq_(expect_signature, signature)
コード例 #8
0
    def test_adobe_base64_encode_decode(self):
        """Test our special variant of base64 encoding designed to avoid
        triggering an Adobe bug.
        """
        value = "!\tFN6~'Es52?X!#)Z*_S"

        encoded = AuthdataUtility.adobe_base64_encode(value)
        eq_('IQlGTjZ:J0VzNTI;WCEjKVoqX1M@', encoded)

        # This is like normal base64 encoding, but with a colon
        # replacing the plus character, a semicolon replacing the
        # slash, an at sign replacing the equal sign and the final
        # newline stripped.
        eq_(
            encoded.replace(":", "+").replace(";", "/").replace("@", "=") +
            "\n", base64.encodestring(value))

        # We can reverse the encoding to get the original value.
        eq_(value, AuthdataUtility.adobe_base64_decode(encoded))
コード例 #9
0
    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)
コード例 #10
0
    def test_adobe_id_tags_when_vendor_id_configured(self):
        """When vendor ID delegation is configured, adobe_id_tags()
        returns a list containing a single tag. The tag contains
        the information necessary to get an Adobe ID and a link to the local
        DRM Device Management Protocol endpoint.
        """
        self.initialize_adobe(self._default_library)
        patron_identifier = "patron identifier"
        [element] = self.annotator.adobe_id_tags(patron_identifier)
        eq_('{http://librarysimplified.org/terms/drm}licensor', element.tag)

        key = '{http://librarysimplified.org/terms/drm}vendor'
        eq_(self.adobe_vendor_id.username, element.attrib[key])

        [token, device_management_link] = element.getchildren()

        eq_('{http://librarysimplified.org/terms/drm}clientToken', token.tag)
        # token.text is a token which we can decode, since we know
        # the secret.
        token = token.text
        authdata = AuthdataUtility.from_config(self._default_library)
        decoded = authdata.decode_short_client_token(token)
        expected_url = ConfigurationSetting.for_library(
            Configuration.WEBSITE_URL, self._default_library).value
        eq_((expected_url, patron_identifier), decoded)

        eq_("link", device_management_link.tag)
        eq_("http://librarysimplified.org/terms/drm/rel/devices",
            device_management_link.attrib['rel'])
        expect_url = self.annotator.url_for(
            'adobe_drm_devices',
            library_short_name=self._default_library.short_name,
            _external=True)
        eq_(expect_url, device_management_link.attrib['href'])

        # If we call adobe_id_tags again we'll get a distinct tag
        # object that renders to the same XML.
        [same_tag] = self.annotator.adobe_id_tags(patron_identifier)
        assert same_tag is not element
        eq_(etree.tostring(element), etree.tostring(same_tag))
コード例 #11
0
    def test_from_config(self):
        library = self._default_library
        library2 = self._library()
        self.initialize_adobe(library, [library2])
        library_url = library.setting(Configuration.WEBSITE_URL).value
        library2_url = library2.setting(Configuration.WEBSITE_URL).value
        
        utility = AuthdataUtility.from_config(library)

        registry = ExternalIntegration.lookup(
            self._db, ExternalIntegration.OPDS_REGISTRATION,
            ExternalIntegration.DISCOVERY_GOAL, library=library
        )
        eq_(library.short_name + "token",
            ConfigurationSetting.for_library_and_externalintegration(
                self._db, ExternalIntegration.USERNAME, library, registry).value)
        eq_(library.short_name + " token secret",
            ConfigurationSetting.for_library_and_externalintegration(
                self._db, ExternalIntegration.PASSWORD, library, registry).value)

        eq_(self.TEST_VENDOR_ID, utility.vendor_id)
        eq_(library_url, utility.library_uri)
        eq_(
            {library2_url : "%s token secret" % library2.short_name,
             library_url : "%s token secret" % library.short_name},
            utility.secrets_by_library_uri
        )

        eq_(
            {"%sTOKEN" % library.short_name.upper() : library_url,
             "%sTOKEN" % library2.short_name.upper() : library2_url },
            utility.library_uris_by_short_name
        )

        # If an integration is set up but incomplete, from_config
        # raises CannotLoadConfiguration.
        setting = ConfigurationSetting.for_library_and_externalintegration(
            self._db, ExternalIntegration.USERNAME, library, registry)
        old_short_name = setting.value
        setting.value = None
        assert_raises(
            CannotLoadConfiguration, AuthdataUtility.from_config,
            library
        )
        setting.value = old_short_name

        setting = library.setting(Configuration.WEBSITE_URL)
        old_value = setting.value
        setting.value = None
        assert_raises(
            CannotLoadConfiguration, AuthdataUtility.from_config, library
        )
        setting.value = old_value

        setting = ConfigurationSetting.for_library_and_externalintegration(
            self._db, ExternalIntegration.PASSWORD, library, registry)
        old_secret = setting.value
        setting.value = None
        assert_raises(
            CannotLoadConfiguration, AuthdataUtility.from_config, library
        )
        setting.value = old_secret

        # If other libraries are not configured, that's fine. We'll
        # only have a configuration for ourselves.
        self.adobe_vendor_id.set_setting(
            AuthdataUtility.OTHER_LIBRARIES_KEY, None
        )
        authdata = AuthdataUtility.from_config(library)
        eq_({library_url : "%s token secret" % library.short_name},
            authdata.secrets_by_library_uri)
        eq_({"%sTOKEN" % library.short_name.upper(): library_url},
            authdata.library_uris_by_short_name)

        # Short library names are case-insensitive. If the
        # configuration has the same library short name twice, you
        # can't create an AuthdataUtility.
        self.adobe_vendor_id.set_setting(
            AuthdataUtility.OTHER_LIBRARIES_KEY,
            json.dumps({
                "http://a/" : ("a", "secret1"),
                "http://b/" : ("A", "secret2"),
            })
        )
        assert_raises(ValueError, AuthdataUtility.from_config, library)

        # If there is no Adobe Vendor ID integration set up,
        # from_config() returns None.
        self._db.delete(registry)
        eq_(None, AuthdataUtility.from_config(library))
コード例 #12
0
class TestAuthdataUtility(VendorIDTest):

    def setup(self):
        super(TestAuthdataUtility, self).setup()
        self.authdata = AuthdataUtility(
            vendor_id = "The Vendor ID",
            library_uri = "http://my-library.org/",
            library_short_name = "MyLibrary",
            secret = "My library secret",
            other_libraries = {
                "http://your-library.org/": ("you", "Your library secret")
            },
        )

    def test_from_config(self):
        library = self._default_library
        library2 = self._library()
        self.initialize_adobe(library, [library2])
        library_url = library.setting(Configuration.WEBSITE_URL).value
        library2_url = library2.setting(Configuration.WEBSITE_URL).value
        
        utility = AuthdataUtility.from_config(library)

        registry = ExternalIntegration.lookup(
            self._db, ExternalIntegration.OPDS_REGISTRATION,
            ExternalIntegration.DISCOVERY_GOAL, library=library
        )
        eq_(library.short_name + "token",
            ConfigurationSetting.for_library_and_externalintegration(
                self._db, ExternalIntegration.USERNAME, library, registry).value)
        eq_(library.short_name + " token secret",
            ConfigurationSetting.for_library_and_externalintegration(
                self._db, ExternalIntegration.PASSWORD, library, registry).value)

        eq_(self.TEST_VENDOR_ID, utility.vendor_id)
        eq_(library_url, utility.library_uri)
        eq_(
            {library2_url : "%s token secret" % library2.short_name,
             library_url : "%s token secret" % library.short_name},
            utility.secrets_by_library_uri
        )

        eq_(
            {"%sTOKEN" % library.short_name.upper() : library_url,
             "%sTOKEN" % library2.short_name.upper() : library2_url },
            utility.library_uris_by_short_name
        )

        # If an integration is set up but incomplete, from_config
        # raises CannotLoadConfiguration.
        setting = ConfigurationSetting.for_library_and_externalintegration(
            self._db, ExternalIntegration.USERNAME, library, registry)
        old_short_name = setting.value
        setting.value = None
        assert_raises(
            CannotLoadConfiguration, AuthdataUtility.from_config,
            library
        )
        setting.value = old_short_name

        setting = library.setting(Configuration.WEBSITE_URL)
        old_value = setting.value
        setting.value = None
        assert_raises(
            CannotLoadConfiguration, AuthdataUtility.from_config, library
        )
        setting.value = old_value

        setting = ConfigurationSetting.for_library_and_externalintegration(
            self._db, ExternalIntegration.PASSWORD, library, registry)
        old_secret = setting.value
        setting.value = None
        assert_raises(
            CannotLoadConfiguration, AuthdataUtility.from_config, library
        )
        setting.value = old_secret

        # If other libraries are not configured, that's fine. We'll
        # only have a configuration for ourselves.
        self.adobe_vendor_id.set_setting(
            AuthdataUtility.OTHER_LIBRARIES_KEY, None
        )
        authdata = AuthdataUtility.from_config(library)
        eq_({library_url : "%s token secret" % library.short_name},
            authdata.secrets_by_library_uri)
        eq_({"%sTOKEN" % library.short_name.upper(): library_url},
            authdata.library_uris_by_short_name)

        # Short library names are case-insensitive. If the
        # configuration has the same library short name twice, you
        # can't create an AuthdataUtility.
        self.adobe_vendor_id.set_setting(
            AuthdataUtility.OTHER_LIBRARIES_KEY,
            json.dumps({
                "http://a/" : ("a", "secret1"),
                "http://b/" : ("A", "secret2"),
            })
        )
        assert_raises(ValueError, AuthdataUtility.from_config, library)

        # If there is no Adobe Vendor ID integration set up,
        # from_config() returns None.
        self._db.delete(registry)
        eq_(None, AuthdataUtility.from_config(library))
            
    def test_decode_round_trip(self):        
        patron_identifier = "Patron identifier"
        vendor_id, authdata = self.authdata.encode(patron_identifier)
        eq_("The Vendor ID", vendor_id)
        
        # We can decode the authdata with our secret.
        decoded = self.authdata.decode(authdata)
        eq_(("http://my-library.org/", "Patron identifier"), decoded)

    def test_decode_round_trip_with_intermediate_mischief(self):        
        patron_identifier = "Patron identifier"
        vendor_id, authdata = self.authdata.encode(patron_identifier)
        eq_("The Vendor ID", vendor_id)

        # A mischievious party in the middle decodes our authdata
        # without telling us.
        authdata = base64.decodestring(authdata)
        
        # But it still works.
        decoded = self.authdata.decode(authdata)
        eq_(("http://my-library.org/", "Patron identifier"), decoded)
        
    def test_encode(self):
        """Test that _encode gives a known value with known input."""
        patron_identifier = "Patron identifier"
        now = datetime.datetime(2016, 1, 1, 12, 0, 0)
        expires = datetime.datetime(2018, 1, 1, 12, 0, 0)
        authdata = self.authdata._encode(
            self.authdata.library_uri, patron_identifier, now, expires
        )
        eq_(
            base64.encodestring('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbXktbGlicmFyeS5vcmcvIiwiaWF0IjoxNDUxNjQ5NjAwLjAsInN1YiI6IlBhdHJvbiBpZGVudGlmaWVyIiwiZXhwIjoxNTE0ODA4MDAwLjB9.n7VRVv3gIyLmNxTzNRTEfCdjoky0T0a1Jhehcag1oQw'),
            authdata
        )

    def test_decode_from_another_library(self):        

        # Here's the AuthdataUtility used by another library.
        foreign_authdata = AuthdataUtility(
            vendor_id = "The Vendor ID",
            library_uri = "http://your-library.org/",
            library_short_name = "you",
            secret = "Your library secret",
        )
        
        patron_identifier = "Patron identifier"
        vendor_id, authdata = foreign_authdata.encode(patron_identifier)

        # Because we know the other library's secret, we're able to
        # decode the authdata.
        decoded = self.authdata.decode(authdata)
        eq_(("http://your-library.org/", "Patron identifier"), decoded)

        # If our secret doesn't match the other library's secret,
        # we can't decode the authdata
        foreign_authdata.secret = 'A new secret'
        vendor_id, authdata = foreign_authdata.encode(patron_identifier)
        assert_raises_regexp(
            DecodeError, "Signature verification failed",
            self.authdata.decode, authdata
        )
        
    def test_decode_from_unknown_library_fails(self):

        # Here's the AuthdataUtility used by a library we don't know
        # about.
        foreign_authdata = AuthdataUtility(
            vendor_id = "The Vendor ID",
            library_uri = "http://some-other-library.org/",
            library_short_name = "SomeOther",
            secret = "Some other library secret",
        )
        vendor_id, authdata = foreign_authdata.encode("A patron")
        # They can encode, but we cna't decode.
        assert_raises_regexp(
            DecodeError, "Unknown library: http://some-other-library.org/",
            self.authdata.decode, authdata
        )

    def test_cannot_decode_token_from_future(self):
        future = datetime.datetime.utcnow() + datetime.timedelta(days=365)
        authdata = self.authdata._encode(
            "Patron identifier", iat=future
        )        
        assert_raises(
            InvalidIssuedAtError, self.authdata.decode, authdata
        )
        
    def test_cannot_decode_expired_token(self):
        expires = datetime.datetime(2016, 1, 1, 12, 0, 0)
        authdata = self.authdata._encode(
            "Patron identifier", exp=expires
        )
        assert_raises(
            ExpiredSignatureError, self.authdata.decode, authdata
        )
        
    def test_cannot_encode_null_patron_identifier(self):
        assert_raises_regexp(
            ValueError, "No patron identifier specified",
            self.authdata.encode, None
        )
        
    def test_cannot_decode_null_patron_identifier(self):

        authdata = self.authdata._encode(
            self.authdata.library_uri, None, 
        )
        assert_raises_regexp(
            DecodeError, "No subject specified",
            self.authdata.decode, authdata
        )

    def test_short_client_token_round_trip(self):
        """Encoding a token and immediately decoding it gives the expected
        result.
        """
        vendor_id, token = self.authdata.encode_short_client_token("a patron")
        eq_(self.authdata.vendor_id, vendor_id)

        library_uri, patron = self.authdata.decode_short_client_token(token)
        eq_(self.authdata.library_uri, library_uri)
        eq_("a patron", patron)

    def test_short_client_token_encode_known_value(self):
        """Verify that the encoding algorithm gives a known value on known
        input.
        """
        value = self.authdata._encode_short_client_token(
            "a library", "a patron identifier", 1234.5
        )

        # Note the colon characters that replaced the plus signs in
        # what would otherwise be normal base64 text. Similarly for
        # the semicolon which replaced the slash, and the at sign which
        # replaced the equals sign.
        eq_('a library|1234.5|a patron identifier|YoNGn7f38mF531KSWJ;o1H0Z3chbC:uTE:t7pAwqYxM@',
            value
        )

        # Dissect the known value to show how it works.
        token, signature = value.rsplit("|", 1)

        # Signature is base64-encoded in a custom way that avoids
        # triggering an Adobe bug ; token is not.
        signature = AuthdataUtility.adobe_base64_decode(signature)

        # The token comes from the library name, the patron identifier,
        # and the time of creation.
        eq_("a library|1234.5|a patron identifier", token)

        # The signature comes from signing the token with the
        # secret associated with this library.
        expect_signature = self.authdata.short_token_signer.sign(
            token, self.authdata.short_token_signing_key
        )
        eq_(expect_signature, signature)

    def test_decode_short_client_token_from_another_library(self):
        # Here's the AuthdataUtility used by another library.
        foreign_authdata = AuthdataUtility(
            vendor_id = "The Vendor ID",
            library_uri = "http://your-library.org/",
            library_short_name = "you",
            secret = "Your library secret",
        )
        
        patron_identifier = "Patron identifier"
        vendor_id, token = foreign_authdata.encode_short_client_token(
            patron_identifier
        )
        
        # Because we know the other library's secret, we're able to
        # decode the authdata.
        decoded = self.authdata.decode_short_client_token(token)
        eq_(("http://your-library.org/", "Patron identifier"), decoded)

        # If our secret for a library doesn't match the other
        # library's short token signing key, we can't decode the
        # authdata.
        foreign_authdata.short_token_signing_key = 'A new secret'
        vendor_id, token = foreign_authdata.encode_short_client_token(
            patron_identifier
        )
        assert_raises_regexp(
            ValueError, "Invalid signature for",
            self.authdata.decode_short_client_token, token
        )

    def test_decode_client_token_errors(self):
        """Test various token errors"""
        m = self.authdata._decode_short_client_token

        # A token has to contain at least two pipe characters.
        assert_raises_regexp(
            ValueError, "Invalid client token",
            m, "foo|", "signature"
        )
        
        # The expiration time must be numeric.
        assert_raises_regexp(
            ValueError, 'Expiration time "a time" is not numeric',
            m, "library|a time|patron", "signature"
        )

        # The patron identifier must not be blank.
        assert_raises_regexp(
            ValueError, 'Token library|1234| has empty patron identifier',
            m, "library|1234|", "signature"
        )
        
        # The library must be a known one.
        assert_raises_regexp(
            ValueError,
            'I don\'t know how to handle tokens from library "LIBRARY"',
            m, "library|1234|patron", "signature"
        )

        # We must have the shared secret for the given library.
        self.authdata.library_uris_by_short_name['LIBRARY'] = 'http://a-library.com/'
        assert_raises_regexp(
            ValueError,
            'I don\'t know the secret for library http://a-library.com/',
            m, "library|1234|patron", "signature"
        )

        # The token must not have expired.
        assert_raises_regexp(
            ValueError,
            'Token mylibrary|1234|patron expired at 1970-01-01 00:20:34',
            m, "mylibrary|1234|patron", "signature"
        )

        # Finally, the signature must be valid.
        assert_raises_regexp(
            ValueError, 'Invalid signature for',
            m, "mylibrary|99999999999|patron", "signature"
        )

    def test_adobe_base64_encode_decode(self):
        """Test our special variant of base64 encoding designed to avoid
        triggering an Adobe bug.
        """
        value = "!\tFN6~'Es52?X!#)Z*_S"
        
        encoded = AuthdataUtility.adobe_base64_encode(value)
        eq_('IQlGTjZ:J0VzNTI;WCEjKVoqX1M@', encoded)

        # This is like normal base64 encoding, but with a colon
        # replacing the plus character, a semicolon replacing the
        # slash, an at sign replacing the equal sign and the final
        # newline stripped.
        eq_(
            encoded.replace(":", "+").replace(";", "/").replace("@", "=") + "\n",
            base64.encodestring(value)
        )

        # We can reverse the encoding to get the original value.
        eq_(value, AuthdataUtility.adobe_base64_decode(encoded))

    def test__encode_short_client_token_uses_adobe_base64_encoding(self):
        class MockSigner(object):
            def sign(self, value, key):
                """Always return the same signature, crafted to contain a 
                plus sign, a slash and an equal sign when base64-encoded.
                """
                return "!\tFN6~'Es52?X!#)Z*_S"
        self.authdata.short_token_signer = MockSigner()
        token = self.authdata._encode_short_client_token("lib", "1234", 0)

        # The signature part of the token has been encoded with our
        # custom encoding, not vanilla base64.
        eq_('lib|0|1234|IQlGTjZ:J0VzNTI;WCEjKVoqX1M@', token)
        
    def test_decode_two_part_short_client_token_uses_adobe_base64_encoding(self):

        # The base64 encoding of this signature has a plus sign in it.
        signature = 'LbU}66%\\-4zt>R>_)\n2Q'
        encoded_signature = AuthdataUtility.adobe_base64_encode(signature)

        # We replace the plus sign with a colon.
        assert ':' in encoded_signature
        assert '+' not in encoded_signature
        
        # Make sure that decode_two_part_short_client_token properly
        # reverses that change when decoding the 'password'.
        class MockAuthdataUtility(AuthdataUtility):
            def _decode_short_client_token(self, token, supposed_signature):
                eq_(supposed_signature, signature)
                self.test_code_ran = True

        utility =  MockAuthdataUtility(
            vendor_id = "The Vendor ID",
            library_uri = "http://your-library.org/",
            library_short_name = "you",
            secret = "Your library secret",
        )
        utility.test_code_ran = False
        utility.decode_two_part_short_client_token(
            "username", encoded_signature
        )

        # The code in _decode_short_client_token ran. Since there was no
        # test failure, it ran successfully.
        eq_(True, utility.test_code_ran)

        
    # Tests of code that is used only in a migration script.  This can
    # be deleted once
    # 20161102-adobe-id-is-delegated-patron-identifier.py is run on
    # all affected instances.
    def test_migrate_adobe_id_noop(self):
        patron = self._patron()
        self.authdata.migrate_adobe_id(patron)

        # Since the patron has no adobe ID, nothing happens.
        eq_([], patron.credentials)
        eq_([], self._db.query(DelegatedPatronIdentifier).all())

    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)
Adobe ID. This makes sure that they don't suddenly change Adobe IDs
when they start using a client that employs the new JWT-based authdata
system.
"""

import os
import sys
from pdb import set_trace
bin_dir = os.path.split(__file__)[0]
package_dir = os.path.join(bin_dir, "..")
sys.path.append(os.path.abspath(package_dir))
from core.model import (production_session, Patron)
from api.adobe_vendor_id import AuthdataUtility

_db = production_session()
authdata = AuthdataUtility.from_config()
if not authdata:
    print "Adobe IDs not configured, doing nothing."

count = 0
qu = _db.query(Patron)
print "Processing %d patrons." % qu.count()
for patron in qu:
    credential, delegated_identifier = authdata.migrate_adobe_id(patron)
    count += 1
    if not (count % 100):
        print count
        _db.commit()
    if credential is None or delegated_identifier is None:
        # This patron did not have an Adobe ID in the first place.
        # Do nothing.