← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wgrant/launchpad/devel-add-read-ff into lp:launchpad

 

William Grant has proposed merging lp:~wgrant/launchpad/devel-add-read-ff into lp:launchpad.

Commit message:
Add feature flag to read GPG keys from gpgservice.

Requested reviews:
  William Grant (wgrant): code

For more details, see:
https://code.launchpad.net/~wgrant/launchpad/devel-add-read-ff/+merge/289603

Polished version of https://code.launchpad.net/~thomir/launchpad/devel-add-read-ff/+merge/288989
-- 
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg	2016-03-14 19:40:14 +0000
+++ database/schema/security.cfg	2016-03-21 05:51:58 +0000
@@ -975,6 +975,7 @@
 public.livefs                                 = SELECT
 public.livefsbuild                            = SELECT, UPDATE
 public.livefsfile                             = SELECT
+public.openididentifier                       = SELECT
 public.packageset                             = SELECT
 public.packagesetgroup                        = SELECT
 public.packagesetinclusion                    = SELECT
@@ -1056,6 +1057,7 @@
 public.job                                      = SELECT, INSERT, UPDATE
 public.libraryfilealias                         = SELECT
 public.libraryfilecontent                       = SELECT
+public.openididentifier                         = SELECT
 public.packagecopyjob                           = SELECT
 public.packageset                               = SELECT, INSERT
 public.packagesetgroup                          = SELECT, INSERT
@@ -1113,6 +1115,7 @@
 public.libraryfilecontent                     = SELECT, INSERT
 public.message                                = SELECT, INSERT, UPDATE
 public.messagechunk                           = SELECT, INSERT, UPDATE
+public.openididentifier                       = SELECT
 public.packagecopyjob                         = SELECT, UPDATE
 public.packagediff                            = SELECT, INSERT
 public.packageset                             = SELECT, INSERT
@@ -1216,6 +1219,7 @@
 public.logintoken                       = SELECT, INSERT, UPDATE
 public.message                          = SELECT, INSERT, UPDATE
 public.milestone                        = SELECT, INSERT, UPDATE
+public.openididentifier                 = SELECT
 public.packageupload                    = SELECT, INSERT, UPDATE
 public.packageuploadbuild               = SELECT, INSERT, UPDATE
 public.packageuploadcustom              = SELECT, INSERT, UPDATE
@@ -1390,6 +1394,7 @@
 public.messagechunk                     = SELECT, INSERT
 public.milestone                        = SELECT
 public.milestonetag                     = SELECT
+public.openididentifier                 = SELECT
 public.packagecopyjob                   = SELECT, INSERT
 public.packagediff                      = SELECT, INSERT, UPDATE, DELETE
 public.packageset                       = SELECT
@@ -1502,6 +1507,7 @@
 public.messagechunk                     = SELECT, INSERT
 public.milestone                        = SELECT
 public.milestonetag                     = SELECT
+public.openididentifier                 = SELECT
 public.packagecopyjob                   = SELECT, INSERT, UPDATE
 public.packagediff                      = SELECT, UPDATE
 public.packageset                       = SELECT
@@ -1804,6 +1810,7 @@
 public.messagechunk                     = SELECT, INSERT
 public.milestone                        = SELECT
 public.milestonetag                     = SELECT, INSERT, DELETE
+public.openididentifier                 = SELECT
 public.packageset                       = SELECT
 public.packagesetgroup                  = SELECT
 public.packagesetinclusion              = SELECT
@@ -1922,6 +1929,7 @@
 public.libraryfilecontent               = SELECT, INSERT
 public.message                          = SELECT, INSERT
 public.messagechunk                     = SELECT, INSERT
+public.openididentifier                 = SELECT
 public.person                           = SELECT
 public.product                          = SELECT
 public.productseries                    = SELECT

=== modified file 'lib/lp/registry/browser/tests/test_gpgkey.py'
--- lib/lp/registry/browser/tests/test_gpgkey.py	2016-02-10 00:51:55 +0000
+++ lib/lp/registry/browser/tests/test_gpgkey.py	2016-03-21 05:51:58 +0000
@@ -6,28 +6,39 @@
 __metaclass__ = type
 
 from testtools.matchers import (
+    Contains,
+    Equals,
+    HasLength,
     Not,
     Raises,
     raises,
     )
+from zope.component import getUtility
 
+from lp.registry.interfaces.person import IPersonSet
+from lp.registry.interfaces.gpg import IGPGKeySet
 from lp.services.features.testing import FeatureFixture
 from lp.services.gpg.interfaces import (
+    GPG_DATABASE_READONLY_FEATURE_FLAG,
+    GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG,
+    GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG,
+    GPGKeyAlgorithm,
     GPGReadOnly,
-    GPG_DATABASE_READONLY_FEATURE_FLAG,
     )
+from lp.services.verification.interfaces.authtoken import LoginTokenType
+from lp.services.verification.interfaces.logintoken import ILoginTokenSet
 from lp.services.webapp import canonical_url
 from lp.testing import (
     login_person,
     TestCaseWithFactory,
     )
-from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.layers import LaunchpadFunctionalLayer
 from lp.testing.views import create_initialized_view
 
 
 class TestCanonicalUrl(TestCaseWithFactory):
 
-    layer = DatabaseFunctionalLayer
+    layer = LaunchpadFunctionalLayer
 
     def test_canonical_url(self):
         # The canonical URL of a GPG key is ???
@@ -41,7 +52,7 @@
 
 class TestPersonGPGView(TestCaseWithFactory):
 
-    layer = DatabaseFunctionalLayer
+    layer = LaunchpadFunctionalLayer
 
     def test_edit_pgp_keys_login_redirect(self):
         """+editpgpkeys should redirect to force you to re-authenticate."""
@@ -60,8 +71,9 @@
         }))
         person = self.factory.makePerson()
         login_person(person)
-        view = create_initialized_view(person, "+editpgpkeys", principal=person,
-                                       method='POST', have_fresh_login=True)
+        view = create_initialized_view(
+            person, "+editpgpkeys", principal=person, method='POST',
+            have_fresh_login=True)
         self.assertThat(view.render, raises(GPGReadOnly))
 
     def test_gpgkeys_GET_readonly_with_feature_flag_set(self):
@@ -70,6 +82,98 @@
         }))
         person = self.factory.makePerson()
         login_person(person)
-        view = create_initialized_view(person, "+editpgpkeys", principal=person,
-                                       method='GET', have_fresh_login=True)
+        view = create_initialized_view(
+            person, "+editpgpkeys", principal=person, method='GET',
+            have_fresh_login=True)
         self.assertThat(view.render, Not(Raises()))
+
+
+class GPGKeySetTests(TestCaseWithFactory):
+
+    layer = LaunchpadFunctionalLayer
+
+    def test_can_add_keys_for_test(self):
+        # IGPGKeySet.new _only_ writes to the launchpad database, so this test
+        # only works if the read_from_gpgservice FF is *not* set. Once this is
+        # the default codepath this test should be deleted.
+        self.useFixture(FeatureFixture({
+            GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG: None,
+        }))
+        keyset = getUtility(IGPGKeySet)
+        person = self.factory.makePerson()
+        fingerprint = "DEADBEEF12345678DEADBEEF12345678DEADBEEF"
+        keyset.new(person.id, "F0A432C2", fingerprint, 4096,
+                   GPGKeyAlgorithm.R, True, True)
+
+        keys = keyset.getGPGKeysForPerson(person)
+
+        self.assertThat(keys, HasLength(1))
+        self.assertThat(keys[0].fingerprint, Equals(fingerprint))
+
+    def test_sampledata_contains_gpgkeys(self):
+        keyset = getUtility(IGPGKeySet)
+        personset = getUtility(IPersonSet)
+        foobar = personset.getByName('name16')
+        keys = keyset.getGPGKeysForPerson(foobar)
+
+        self.assertThat(keys, HasLength(1))
+        self.assertThat(keys[0].keyid, Equals('12345678'))
+        self.assertThat(keys[0].fingerprint,
+                        Equals('ABCDEF0123456789ABCDDCBA0000111112345678'))
+
+    def test_can_retrieve_keys_by_fingerprint(self):
+        keyset = getUtility(IGPGKeySet)
+        person = self.factory.makePerson()
+        key = self.factory.makeGPGKey(person)
+
+        retrieved_key = keyset.getByFingerprint(key.fingerprint)
+
+        self.assertThat(retrieved_key.owner.name, Equals(person.name))
+        self.assertThat(retrieved_key.fingerprint, Equals(key.fingerprint))
+
+    def test_getGPGKeysForPerson_retrieves_active_keys(self):
+        keyset = getUtility(IGPGKeySet)
+        person = self.factory.makePerson()
+        key = self.factory.makeGPGKey(person)
+
+        keys = keyset.getGPGKeysForPerson(person)
+
+        self.assertThat(keys, HasLength(1))
+        self.assertThat(keys, Contains(key))
+
+    def test_getGPGKeysForPerson_retrieves_inactive_keys(self):
+        keyset = getUtility(IGPGKeySet)
+        person = self.factory.makePerson()
+        key = self.factory.makeGPGKey(person)
+        keyset.deactivate(key)
+
+        active_keys = keyset.getGPGKeysForPerson(person)
+        inactive_keys = keyset.getGPGKeysForPerson(person, active=False)
+
+        self.assertThat(active_keys, HasLength(0))
+        self.assertThat(inactive_keys, HasLength(1))
+        self.assertThat(inactive_keys, Contains(key))
+
+    def test_getGPGKeysForPerson_excludes_keys_with_logintoken(self):
+        keyset = getUtility(IGPGKeySet)
+        email = 'foo@xxxxxxx'
+        person = self.factory.makePerson(email)
+        key = self.factory.makeGPGKey(person)
+        keyset.deactivate(key)
+        getUtility(ILoginTokenSet).new(
+            person, email, email, LoginTokenType.VALIDATEGPG, key.fingerprint)
+
+        inactive_keys = keyset.getGPGKeysForPerson(person, active=False)
+        self.assertThat(inactive_keys, HasLength(0))
+
+
+class GPGKeySetWithGPGServiceTests(GPGKeySetTests):
+
+    """A copy of GPGKeySetTests, but with gpgservice used."""
+
+    def setUp(self):
+        super(GPGKeySetWithGPGServiceTests, self).setUp()
+        self.useFixture(FeatureFixture({
+            GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG: True,
+            GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG: True,
+        }))

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2016-02-28 19:13:17 +0000
+++ lib/lp/registry/configure.zcml	2016-03-21 05:51:58 +0000
@@ -1284,6 +1284,11 @@
             permission="launchpad.Edit"
             set_attributes="active can_encrypt"/>
     </class>
+    <class
+        class="lp.registry.model.gpgkey.GPGServiceKey">
+        <allow
+            interface="lp.registry.interfaces.gpg.IGPGKey"/>
+    </class>
 
     <!-- GPGKeySet -->
 

=== removed file 'lib/lp/registry/doc/gpgkey.txt'
--- lib/lp/registry/doc/gpgkey.txt	2016-03-01 12:46:32 +0000
+++ lib/lp/registry/doc/gpgkey.txt	1970-01-01 00:00:00 +0000
@@ -1,33 +0,0 @@
-= GPG Keys =
-
-Launchpad models GPG keys in a GPGKey class.
-
-    >>> from zope.component import getUtility
-    >>> from lp.registry.interfaces.gpg import IGPGKeySet
-    >>> from lp.registry.interfaces.person import IPersonSet
-    >>> from lp.services.gpg.interfaces import GPGKeyAlgorithm
-    >>> personset = getUtility(IPersonSet)
-    >>> foobar = personset.getByName('name16')
-    >>> gpgkeyset = getUtility(IGPGKeySet)
-    >>> keys = gpgkeyset.getGPGKeysForPerson(foobar)
-    >>> [key.keyid for key in keys]
-    [u'12345678']
-
-Adding new keys is pretty easy:
-
-    >>> name12 = personset.getByName('name12')
-    >>> gpgkeyset.new(name12.id, u'DEADBEEF',
-    ...     'DEADBEEF12345678DEADBEEF12345678DEADBEEF', 1024,
-    ...     GPGKeyAlgorithm.LITTLE_G)
-    <GPGKey...>
-    >>> gpgkeyset.new(name12.id, u'DEADBEED',
-    ...     'DEADBEED12345678DEADBEED12345678DEADBEED', 2048,
-    ...     GPGKeyAlgorithm.G)
-    <GPGKey...>
-
-As is pulling it based on the key ID:
-
-    >>> key = gpgkeyset.getByFingerprint(
-    ...     'DEADBEEF12345678DEADBEEF12345678DEADBEEF')
-    >>> key.owner.name
-    u'name12'

=== modified file 'lib/lp/registry/interfaces/gpg.py'
--- lib/lp/registry/interfaces/gpg.py	2016-03-14 19:40:14 +0000
+++ lib/lp/registry/interfaces/gpg.py	2016-03-21 05:51:58 +0000
@@ -39,7 +39,6 @@
 
     export_as_webservice_entry('gpg_key')
 
-    id = Int(title=_("Database id"), required=True, readonly=True)
     keysize = Int(title=_("Keysize"), required=True)
     algorithm = Choice(title=_("Algorithm"), required=True,
             vocabulary='GpgAlgorithm')
@@ -84,6 +83,9 @@
         inactive ones.
         """
 
+    def getOwnerIdForPerson(person):
+        """return an owner id string suitable for sending to gpgservice."""
+
     def getByFingerprints(fingerprints):
         """Get multiple OpenPGP keys by their fingerprints."""
 

=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py	2016-01-26 15:47:37 +0000
+++ lib/lp/registry/interfaces/person.py	2016-03-21 05:51:58 +0000
@@ -775,10 +775,10 @@
     inactivesignatures = Attribute("Retrieve own Inactive CoC Signatures.")
     signedcocs = Attribute("List of Signed Code Of Conduct")
     gpg_keys = exported(
-        CollectionField(
+        doNotSnapshot(CollectionField(
             title=_("List of valid OpenPGP keys ordered by ID"),
             readonly=False, required=False,
-            value_type=Reference(schema=IGPGKey)))
+            value_type=Reference(schema=IGPGKey))))
     pending_gpg_keys = CollectionField(
         title=_("Set of fingerprints pending confirmation"),
         readonly=False, required=False,

=== modified file 'lib/lp/registry/model/gpgkey.py'
--- lib/lp/registry/model/gpgkey.py	2016-03-16 00:32:05 +0000
+++ lib/lp/registry/model/gpgkey.py	2016-03-21 05:51:58 +0000
@@ -8,7 +8,6 @@
     BoolCol,
     ForeignKey,
     IntCol,
-    SQLObjectNotFound,
     StringCol,
     )
 from zope.component import getUtility
@@ -18,6 +17,8 @@
     IGPGKey,
     IGPGKeySet,
     )
+from lp.registry.interfaces.person import IPersonSet
+from lp.services.config import config
 from lp.services.database.enumcol import EnumCol
 from lp.services.database.interfaces import IStore
 from lp.services.database.sqlbase import (
@@ -27,12 +28,14 @@
 from lp.services.features import getFeatureFlag
 from lp.services.gpg.interfaces import (
     GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG,
+    GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG,
     GPGKeyAlgorithm,
     IGPGClient,
     IGPGHandler,
     )
 from lp.services.openid.interfaces.openid import IOpenIDPersistentIdentity
 from lp.services.openid.model.openididentifier import OpenIdIdentifier
+from lp.services.verification.interfaces.logintoken import ILoginTokenSet
 
 
 @implementer(IGPGKey)
@@ -65,6 +68,58 @@
         return '%s%s/%s' % (self.keysize, self.algorithm.title, self.keyid)
 
 
+@implementer(IGPGKey)
+class GPGServiceKey:
+
+    def __init__(self, key_data):
+        self._key_data = key_data
+
+    @property
+    def active(self):
+        return self._key_data['enabled']
+
+    @property
+    def keysize(self):
+        return self._key_data['size']
+
+    @property
+    def algorithm(self):
+        return GPGKeyAlgorithm.items[self._key_data['algorithm']]
+
+    @property
+    def keyid(self):
+        return self._key_data['id']
+
+    @property
+    def fingerprint(self):
+        return self._key_data['fingerprint']
+
+    @property
+    def displayname(self):
+        return '%s%s/%s' % (self.keysize, self.algorithm.title, self.keyid)
+
+    @property
+    def keyserverURL(self):
+        return getUtility(
+            IGPGHandler).getURLForKeyInServer(self.fingerprint, public=True)
+
+    @property
+    def can_encrypt(self):
+        return self._key_data['can_encrypt']
+
+    @property
+    def owner(self):
+        return getUtility(IPersonSet).getByOpenIDIdentifier(
+            self._key_data['owner'])
+
+    @property
+    def ownerID(self):
+        return self.owner.id
+
+    def __eq__(self, other):
+        return self.fingerprint == other.fingerprint
+
+
 @implementer(IGPGKeySet)
 class GPGKeySet:
 
@@ -79,8 +134,13 @@
     def activate(self, requester, key, can_encrypt):
         """See `IGPGKeySet`."""
         fingerprint = key.fingerprint
-        lp_key = self.getByFingerprint(fingerprint)
+        # XXX: This is a little ugly - we can't use getByFingerprint
+        # here since if the READ_FROM_GPGSERVICE FF is set we'll get a
+        # GPGServiceKey object instead of a GPGKey object, and we need
+        # to change the database representation in all cases.
+        lp_key = GPGKey.selectOneBy(fingerprint=fingerprint)
         if lp_key:
+            assert lp_key.owner == requester
             is_new = False
             # Then the key already exists, so let's reactivate it.
             lp_key.active = True
@@ -95,50 +155,116 @@
                 ownerID, keyid, fingerprint, keysize, algorithm,
                 can_encrypt=can_encrypt)
         if getFeatureFlag(GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG):
+            # XXX: Further to the comment above, if READ_FROM_GPGSERVICE FF is
+            # set then we need to duplicate the block above but reading from
+            # the gpgservice instead of the database:
             client = getUtility(IGPGClient)
-            openid_identifier = self.getOwnerIdForPerson(lp_key.owner)
-            client.addKeyForOwner(openid_identifier, key.fingerprint)
+            owner_id = self.getOwnerIdForPerson(requester)
+            # Users with more than one openid identifier may be re-activating
+            # a key that was previously deactivated with their non-default
+            # openid identifier. If that's the case, use the same openid
+            # identifier rather than the default one - this happens even if the
+            # read FF is not set:
+            key_data = client.getKeyByFingerprint(fingerprint)
+            if key_data:
+                owner_id = key_data['owner']
+            allowed_owner_ids = self._getAllOwnerIdsForPerson(requester)
+            assert owner_id in allowed_owner_ids
+            gpgservice_key = GPGServiceKey(
+                client.addKeyForOwner(owner_id, key.fingerprint))
+            if getFeatureFlag(GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG):
+                is_new = key_data is None
+                lp_key = gpgservice_key
         return lp_key, is_new
 
     def deactivate(self, key):
-        key.active = False
+        # key could be a GPGServiceKey, which doesn't allow us to set it's
+        # active attribute. Retrieve it by fingerprint:
+        lp_key = GPGKey.selectOneBy(fingerprint=key.fingerprint)
+        lp_key.active = False
         if getFeatureFlag(GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG):
+            # Users with more than one openid identifier may be
+            # deactivating a key that is associated with their
+            # non-default openid identifier.  If that's the case, use
+            # the same openid identifier rather than the default one:
             client = getUtility(IGPGClient)
-            openid_identifier = self.getOwnerIdForPerson(key.owner)
+            key_data = client.getKeyByFingerprint(key.fingerprint)
+            if not key_data:
+                # We get here if we're asked to deactivate a key that was never
+                # activated. This should probably never happen.
+                return
+            openid_identifier = key_data['owner']
             client.disableKeyForOwner(openid_identifier, key.fingerprint)
 
     def getByFingerprint(self, fingerprint, default=None):
         """See `IGPGKeySet`"""
-        result = GPGKey.selectOneBy(fingerprint=fingerprint)
-        if result is None:
-            return default
-        return result
+        if getFeatureFlag(GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG):
+            key_data = getUtility(IGPGClient).getKeyByFingerprint(fingerprint)
+            return GPGServiceKey(key_data) if key_data else default
+        else:
+            result = GPGKey.selectOneBy(fingerprint=fingerprint)
+            if result is None:
+                return default
+            return result
 
     def getByFingerprints(self, fingerprints):
         """See `IGPGKeySet`"""
-        return IStore(GPGKey).find(
-            GPGKey, GPGKey.fingerprint.is_in(fingerprints))
+        fingerprints = list(fingerprints)
+        if getFeatureFlag(GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG):
+            client = getUtility(IGPGClient)
+            return [
+                GPGServiceKey(key_data)
+                for key_data in client.getKeysByFingerprints(fingerprints)]
+        else:
+            return list(IStore(GPGKey).find(
+                GPGKey, GPGKey.fingerprint.is_in(fingerprints)))
 
     def getGPGKeysForPerson(self, owner, active=True):
-        if active is False:
-            query = """
-                active = false
-                AND fingerprint NOT IN
-                    (SELECT fingerprint FROM LoginToken
-                     WHERE fingerprint IS NOT NULL
-                           AND requester = %s
-                           AND date_consumed is NULL
-                    )
-                """ % sqlvalues(owner.id)
+        if getFeatureFlag(GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG):
+            client = getUtility(IGPGClient)
+            owner_ids = self._getAllOwnerIdsForPerson(owner)
+            if not owner_ids:
+                return []
+            gpg_keys = []
+            for owner_id in owner_ids:
+                key_data_list = client.getKeysForOwner(owner_id)['keys']
+                gpg_keys.extend([
+                    GPGServiceKey(d) for d in key_data_list
+                    if d['enabled'] == active])
+            if active is False:
+                login_tokens = getUtility(ILoginTokenSet).getPendingGPGKeys(
+                    owner.id)
+                token_fingerprints = [t.fingerprint for t in login_tokens]
+                return [
+                    k for k in gpg_keys
+                    if k.fingerprint not in token_fingerprints]
+            return gpg_keys
         else:
-            query = 'active=true'
-
-        query += ' AND owner=%s' % sqlvalues(owner.id)
-
-        return list(GPGKey.select(query, orderBy='id'))
+            if active is False:
+                query = """
+                    active = false
+                    AND fingerprint NOT IN
+                        (SELECT fingerprint FROM LoginToken
+                         WHERE fingerprint IS NOT NULL
+                               AND requester = %s
+                               AND date_consumed is NULL
+                        )
+                    """ % sqlvalues(owner.id)
+            else:
+                query = 'active=true'
+            query += ' AND owner=%s' % sqlvalues(owner.id)
+            return list(GPGKey.select(query, orderBy='id'))
 
     def getOwnerIdForPerson(self, owner):
         """See IGPGKeySet."""
         url = IOpenIDPersistentIdentity(owner).openid_identity_url
         assert url is not None
         return url
+
+    def _getAllOwnerIdsForPerson(self, owner):
+        identifiers = IStore(OpenIdIdentifier).find(
+            OpenIdIdentifier, account=owner.account)
+        openid_provider_root = config.launchpad.openid_provider_root
+        return [
+            openid_provider_root + '+id/' + i.identifier.encode('ascii')
+            for i in identifiers]

=== modified file 'lib/lp/registry/stories/gpg-coc/gpg-with-gpgservice-ff.txt'
--- lib/lp/registry/stories/gpg-coc/gpg-with-gpgservice-ff.txt	2016-02-22 23:45:24 +0000
+++ lib/lp/registry/stories/gpg-coc/gpg-with-gpgservice-ff.txt	2016-03-21 05:51:58 +0000
@@ -7,9 +7,13 @@
      tests will be deleted.
 
     >>> from lp.services.features.testing import FeatureFixture
-    >>> from lp.services.gpg.interfaces import GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG
+    >>> from lp.services.gpg.interfaces import (
+    ...   GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG,
+    ...   GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG,
+    ...   )
     >>> feature_fixture = FeatureFixture(
-    ...   {GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG: True})
+    ...   {GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG: True,
+    ...   GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG: True})
     >>> feature_fixture.setUp()
 
 == Setup ==

=== modified file 'lib/lp/services/gpg/configure.zcml'
--- lib/lp/services/gpg/configure.zcml	2016-02-18 03:35:43 +0000
+++ lib/lp/services/gpg/configure.zcml	2016-03-21 05:51:58 +0000
@@ -24,7 +24,7 @@
     </class>
 
     <securedutility
-        class="lp.services.gpg.handler.GPGClient"
+        class="lp.services.gpg.handler.LPGPGClient"
         provides="lp.services.gpg.interfaces.IGPGClient">
         <allow interface="lp.services.gpg.interfaces.IGPGClient" />
     </securedutility>

=== modified file 'lib/lp/services/gpg/handler.py'
--- lib/lp/services/gpg/handler.py	2016-03-18 00:48:59 +0000
+++ lib/lp/services/gpg/handler.py	2016-03-21 05:51:58 +0000
@@ -11,7 +11,6 @@
     ]
 
 import atexit
-import base64
 import httplib
 import os
 import shutil
@@ -22,12 +21,10 @@
 import tempfile
 import urllib
 import urllib2
-from urlparse import urljoin
 
 import gpgme
+from gpgservice_client import GPGClient
 from lazr.restful.utils import get_current_browser_request
-import requests
-from requests.status_codes import codes as http_codes
 from zope.interface import implementer
 
 from lp.app.validators.email import valid_email
@@ -39,7 +36,6 @@
     GPGKeyNotFoundError,
     GPGKeyRevoked,
     GPGKeyTemporarilyNotFoundError,
-    GPGServiceException,
     GPGUploadFailure,
     GPGVerificationError,
     IGPGClient,
@@ -51,7 +47,6 @@
     SecretGPGKeyImportDetected,
     valid_fingerprint,
     )
-from lp.services.openid.model.openididentifier import OpenIdIdentifier
 from lp.services.timeline.requesttimeline import get_request_timeline
 from lp.services.timeout import (
     TimeoutError,
@@ -640,132 +635,12 @@
     return fingerprint
 
 
-def sanitize_fingerprint_or_raise(fingerprint):
-    """Check the sanity of 'fingerprint'.
-
-    If 'fingerprint' is a valid fingerprint, the sanitised version will be
-    returned (see sanitize_fingerprint).
-
-    Otherwise, a ValueError will be raised.
-    """
-    sane_fingerprint = sanitize_fingerprint(fingerprint)
-    if sane_fingerprint is None:
-        raise ValueError("Invalid fingerprint: %r." % fingerprint)
-    return sane_fingerprint
-
-
 @implementer(IGPGClient)
-class GPGClient:
+class LPGPGClient(GPGClient):
     """See IGPGClient."""
 
-    def __init__(self):
-        self.write_hooks = set()
-
-    def getKeysForOwner(self, owner_id):
-        """See IGPGClient."""
-        path = '/users/%s/keys' % self._encode_owner_id(owner_id)
-        resp = self._request('get', path)
-        if resp.status_code != http_codes['OK']:
-            self.raise_for_error(resp)
-        return resp.json()
-
-    def addKeyForOwner(self, owner_id, fingerprint):
-        """See IGPGClient."""
-        fingerprint = sanitize_fingerprint_or_raise(fingerprint)
-        path = '/users/%s/keys' % self._encode_owner_id(owner_id)
-        data = dict(fingerprint=fingerprint)
-        resp = self._request('post', path, data)
-        if resp.status_code == http_codes['CREATED']:
-            self._notify_writes()
-        # status 200 (OK) is returned if the key was already enabled:
-        elif resp.status_code != http_codes['OK']:
-            self.raise_for_error(resp)
-
-    def disableKeyForOwner(self, owner_id, fingerprint):
-        """See IGPGClient."""
-        fingerprint = sanitize_fingerprint_or_raise(fingerprint)
-        path = '/users/%s/keys/%s' % (self._encode_owner_id(owner_id), fingerprint)
-        resp = self._request('delete', path)
-        if resp.status_code == http_codes['OK']:
-            self._notify_writes()
-        else:
-            self.raise_for_error(resp)
-
-    def getKeyByFingerprint(self, fingerprint):
-        fingerprint = sanitize_fingerprint_or_raise(fingerprint)
-        path = '/keys/%s' % fingerprint
-        resp = self._request('get', path)
-        if resp.status_code == http_codes['OK']:
-            return resp.json()
-        elif resp.status_code == http_codes['NOT_FOUND']:
-            return None
-        else:
-            self.raise_for_error(resp)
-
-    def registerWriteHook(self, hook_callable):
-        """See IGPGClient."""
-        if not callable(hook_callable):
-            raise TypeError("'hook_callable' parameter must be a callable.")
-        self.write_hooks.add(hook_callable)
-
-    def unregisterWriteHook(self, hook_callable):
-        """See IGPGClient."""
-        if hook_callable not in self.write_hooks:
-            raise ValueError("%r not registered.")
-        self.write_hooks.remove(hook_callable)
-
-    def addKeyForTest(self, owner_id, keyid, fingerprint, keysize, algorithm, enabled,
-                      can_encrypt):
-        """See IGPGClient."""
-        document = {'keys': [{
-            'owner': owner_id,
-            'id': keyid,
-            'fingerprint': fingerprint,
-            'size': keysize,
-            'algorithm': algorithm,
-            'enabled': enabled,
-            'can_encrypt': can_encrypt}]}
-        path = '/test/add_keys'
-        resp = self._request('post', path, document)
-        if resp.status_code == http_codes['NOT_FOUND']:
-            raise RuntimeError(
-                "gpgservice was not configured with test endpoints enabled.")
-        elif resp.status_code != http_codes['OK']:
-            self.raise_for_error(resp)
-
-    def _notify_writes(self):
-        errors = []
-        for hook in self.write_hooks:
-            try:
-                hook()
-            except Exception as e:
-                errors.append(str(e))
-        if errors:
-            raise Exception("The operation succeeded, but one or more write"
-                            " hooks failed: %s" % ', '.join(errors))
-
-    def _encode_owner_id(self, owner_id):
-        return base64.b64encode(owner_id, altchars='-_')
-
-    def raise_for_error(self, response):
-        """Raise GPGServiceException based on what's in 'response'."""
-        if response.headers['Content-Type'] == 'application/json':
-            message = response.json()['status']
-        else:
-            message = "Unhandled service error. HTTP Status: %d HTTP Body: %s" % (
-                response.status_code, response.content)
-        raise GPGServiceException(message)
-
-    @property
-    def timeout(self):
-        # Perhaps this should be from config?
+    def get_endpoint(self):
+        return "http://{}".format(config.gpgservice.api_endpoint)
+
+    def get_timeout(self):
         return 30.0
-
-    @property
-    def endpoint(self):
-        return "http://{}".format(config.gpgservice.api_endpoint)
-
-    def _request(self, method, path, data=None, **kwargs):
-        response = getattr(requests, method)(
-            urljoin(self.endpoint, path), json=data, timeout=self.timeout, **kwargs)
-        return response

=== modified file 'lib/lp/services/gpg/interfaces.py'
--- lib/lp/services/gpg/interfaces.py	2016-03-16 00:32:46 +0000
+++ lib/lp/services/gpg/interfaces.py	2016-03-21 05:51:58 +0000
@@ -3,6 +3,7 @@
 
 __all__ = [
     'GPG_DATABASE_READONLY_FEATURE_FLAG',
+    'GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG',
     'GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG',
     'GPGKeyAlgorithm',
     'GPGKeyDoesNotExistOnServer',
@@ -28,6 +29,7 @@
 import httplib
 import re
 
+from gpgservice_client import GPGServiceException
 from lazr.enum import (
     DBEnumeratedType,
     DBItem,
@@ -52,6 +54,7 @@
 
 GPG_DATABASE_READONLY_FEATURE_FLAG = u"gpg.database_read_only"
 GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG = u"gpg.write_to_gpgservice"
+GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG = u"gpg.read_from_gpgservice"
 
 
 def valid_fingerprint(fingerprint):
@@ -425,15 +428,6 @@
     comment = Attribute("The comment portion of this user ID")
 
 
-class GPGServiceException(Exception):
-
-    """Raised when we get an error from the gpgservice.
-
-    More specific errors for commonly encountered errors may be added once we
-    actually integrate gpgservice with the rest of launchpad.
-    """
-
-
 class IGPGClient(Interface):
 
     """A client for querying a gpgservice instance."""
@@ -467,6 +461,13 @@
         :raises ValueError: if the fingerprint isn't valid.
         """
 
+    def getKeysByFingerprints(fingerprints):
+        """Bulk retrieve GPG keys by a list of fingerprints.
+
+        :param fingerprints: A list of fingerprints to retrieve.
+        :returns: A list of keys that were found.
+        """
+
     def registerWriteHook(hook_callable):
         """Register a write hook.
 

=== modified file 'lib/lp/services/gpg/tests/test_gpghandler.py'
--- lib/lp/services/gpg/tests/test_gpghandler.py	2016-03-18 00:48:59 +0000
+++ lib/lp/services/gpg/tests/test_gpghandler.py	2016-03-21 05:51:58 +0000
@@ -16,6 +16,11 @@
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
+from lp.registry.interfaces.gpg import IGPGKeySet
+from lp.registry.interfaces.person import IPersonSet
+from lp.services.database.constants import THIRTY_DAYS_AGO
+from lp.services.database.interfaces import IMasterStore
+from lp.services.features.testing import FeatureFixture
 from lp.services.gpg.interfaces import (
     GPGKeyDoesNotExistOnServer,
     GPGKeyTemporarilyNotFoundError,
@@ -24,6 +29,7 @@
     IGPGHandler,
     )
 from lp.services.log.logger import BufferLogger
+from lp.services.openid.model.openididentifier import OpenIdIdentifier
 from lp.services.timeout import (
     get_default_timeout_function,
     set_default_timeout_function,
@@ -277,7 +283,9 @@
 
     def test_get_key_for_user_with_sampledata(self):
         client = getUtility(IGPGClient)
-        data = client.getKeysForOwner('name16_oid')
+        person = getUtility(IPersonSet).getByName('name16')
+        openid_id = getUtility(IGPGKeySet).getOwnerIdForPerson(person)
+        data = client.getKeysForOwner(openid_id)
         self.assertThat(data, ContainsDict({'keys': HasLength(1)}))
 
     def test_get_key_for_unknown_user(self):
@@ -339,7 +347,8 @@
     def test_adding_invalid_fingerprint_raises_ValueError(self):
         client = getUtility(IGPGClient)
         self.assertThat(
-            lambda: client.addKeyForOwner(self.get_random_owner_id_string(), ''),
+            lambda: client.addKeyForOwner(
+                self.get_random_owner_id_string(), ''),
             raises(ValueError("Invalid fingerprint: ''.")))
 
     def test_adding_duplicate_fingerprint_raises_GPGServiceException(self):
@@ -351,7 +360,8 @@
         client.addKeyForOwner(user_one, fingerprint)
         self.assertThat(
             lambda: client.addKeyForOwner(user_two, fingerprint),
-            raises(GPGServiceException("Error: Fingerprint already in database.")))
+            raises(GPGServiceException(
+                "Error: Fingerprint already in database.")))
 
     def test_disabling_active_key(self):
         self.useFixture(KeyServerTac())
@@ -382,7 +392,8 @@
     def test_disabling_invalid_fingerprint_raises_ValueError(self):
         client = getUtility(IGPGClient)
         self.assertThat(
-            lambda: client.disableKeyForOwner(self.get_random_owner_id_string(), ''),
+            lambda: client.disableKeyForOwner(
+                self.get_random_owner_id_string(), ''),
             raises(ValueError("Invalid fingerprint: ''."))
         )
 
@@ -409,15 +420,76 @@
                         raises(ValueError))
 
     def test_can_add_IGPGKey_to_test_enabled_gpgservice(self):
+        self.useFixture(
+            FeatureFixture({'gpg.write_to_gpgservice': True}))
         client = getUtility(IGPGClient)
         person = self.factory.makePerson()
+        # With the feature flag enabled, the following creates a
+        # gpg key on the gpgservice.
         gpgkey = self.factory.makeGPGKey(person)
-        user = self.get_random_owner_id_string()
-        client.addKeyForTest(user, gpgkey.keyid, gpgkey.fingerprint,
-                             gpgkey.keysize, gpgkey.algorithm.name,
-                             gpgkey.active, gpgkey.can_encrypt)
-
+        user = getUtility(IGPGKeySet).getOwnerIdForPerson(person)
         key = client.getKeyByFingerprint(gpgkey.fingerprint)
         self.assertThat(
             key, ContainsDict({'owner': Equals(user),
                                'fingerprint': Equals(gpgkey.fingerprint)}))
+
+    def makePersonWithMultipleGPGKeysInDifferentOpenids(self):
+        """Make a person with multiple GPG keys owned by
+        different openid identifiers. This happens as a result
+        of an account merge.
+
+        :returns: an IPerson instance with two keys under
+                  different openid identifiers.
+        """
+        person = self.factory.makePerson()
+        self.factory.makeGPGKey(person)
+        # Create a second openid identifier from 30 days ago.
+        # This simulates the account merge:
+        identifier = OpenIdIdentifier()
+        identifier.account = person.account
+        identifier.identifier = u'openid_identifier'
+        identifier.date_created = THIRTY_DAYS_AGO
+        IMasterStore(OpenIdIdentifier).add(identifier)
+        self.factory.makeGPGKey(person)
+        return person
+
+    def test_can_retrieve_keys_for_all_openid_identifiers(self):
+        person = self.makePersonWithMultipleGPGKeysInDifferentOpenids()
+        keys = getUtility(IGPGKeySet).getGPGKeysForPerson(person)
+        self.assertThat(keys, HasLength(2))
+
+    def test_can_deactivate_all_keys_with_multiple_openid_identifiers(self):
+        person = self.makePersonWithMultipleGPGKeysInDifferentOpenids()
+        keyset = getUtility(IGPGKeySet)
+        key_one, key_two = keyset.getGPGKeysForPerson(person)
+        keyset.deactivate(key_one)
+        keyset.deactivate(key_two)
+        key_one, key_two = keyset.getGPGKeysForPerson(person, active=False)
+
+        self.assertFalse(key_one.active)
+        self.assertFalse(key_two.active)
+
+    def test_can_reactivate_all_keys_with_multiple_openid_identifiers(self):
+        person = self.makePersonWithMultipleGPGKeysInDifferentOpenids()
+        keyset = getUtility(IGPGKeySet)
+        for k in keyset.getGPGKeysForPerson(person):
+            keyset.deactivate(k)
+        for k in keyset.getGPGKeysForPerson(person, active=False):
+            keyset.activate(person, k, k.can_encrypt)
+        key_one, key_two = keyset.getGPGKeysForPerson(person)
+
+        self.assertTrue(key_one.active)
+        self.assertTrue(key_two.active)
+
+    def test_cannot_reactivate_someone_elses_key(self):
+        person1 = self.factory.makePerson()
+        key = self.factory.makeGPGKey(person1)
+        person2 = self.factory.makePerson()
+
+        keyset = getUtility(IGPGKeySet)
+        keyset.deactivate(key)
+        self.assertRaises(
+            AssertionError,
+            keyset.activate,
+            person2, key, key.can_encrypt
+        )

=== modified file 'lib/lp/services/verification/browser/tests/test_logintoken.py'
--- lib/lp/services/verification/browser/tests/test_logintoken.py	2016-02-10 02:37:07 +0000
+++ lib/lp/services/verification/browser/tests/test_logintoken.py	2016-03-21 05:51:58 +0000
@@ -21,12 +21,14 @@
 from lp.services.verification.interfaces.authtoken import LoginTokenType
 from lp.services.verification.interfaces.logintoken import ILoginTokenSet
 from lp.testing import (
-    login_person,
     person_logged_in,
     TestCaseWithFactory,
     )
 from lp.testing.deprecated import LaunchpadFormHarness
-from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.layers import (
+    DatabaseFunctionalLayer,
+    LaunchpadFunctionalLayer,
+    )
 from lp.testing.views import create_initialized_view
 
 
@@ -37,7 +39,7 @@
     token to be consumed (so it can't be used again) when the user hits
     Cancel.
     """
-    layer = DatabaseFunctionalLayer
+    layer = LaunchpadFunctionalLayer
 
     def setUp(self):
         TestCaseWithFactory.setUp(self)
@@ -82,7 +84,7 @@
 
 class LoginTokenReadOnlyTests(TestCaseWithFactory):
 
-    layer = DatabaseFunctionalLayer
+    layer = LaunchpadFunctionalLayer
 
     def test_continue_action_failed_with_gpg_database_in_ro_mode(self):
         self.useFixture(FeatureFixture({

=== modified file 'lib/lp/soyuz/tests/test_archive_subscriptions.py'
--- lib/lp/soyuz/tests/test_archive_subscriptions.py	2015-09-24 11:30:24 +0000
+++ lib/lp/soyuz/tests/test_archive_subscriptions.py	2016-03-21 05:51:58 +0000
@@ -21,7 +21,10 @@
     StormStatementRecorder,
     TestCaseWithFactory,
     )
-from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.layers import (
+    DatabaseFunctionalLayer,
+    LaunchpadFunctionalLayer,
+    )
 from lp.testing.mail_helpers import pop_notifications
 from lp.testing.matchers import HasQueryCount
 from lp.testing.pages import (
@@ -129,7 +132,7 @@
 class PrivateArtifactsViewTestCase(BrowserTestCase):
     """ Tests that private team archives can be viewed."""
 
-    layer = DatabaseFunctionalLayer
+    layer = LaunchpadFunctionalLayer
 
     def setUp(self):
         """Create a test archive."""

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2016-03-09 01:37:56 +0000
+++ lib/lp/testing/factory.py	2016-03-21 05:51:58 +0000
@@ -178,6 +178,7 @@
     )
 from lp.registry.interfaces.distroseriesparent import IDistroSeriesParentSet
 from lp.registry.interfaces.gpg import IGPGKeySet
+from lp.registry.model.gpgkey import GPGServiceKey
 from lp.registry.interfaces.mailinglist import (
     IMailingListSet,
     MailingListStatus,
@@ -233,8 +234,10 @@
 from lp.services.database.sqlbase import flush_database_updates
 from lp.services.features import getFeatureFlag
 from lp.services.gpg.interfaces import (
+    GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG,
     GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG,
     GPGKeyAlgorithm,
+    IGPGClient,
     IGPGHandler,
     )
 from lp.services.identity.interfaces.account import (
@@ -603,6 +606,11 @@
             client.addKeyForTest(
                 openid_identifier, key.keyid, key.fingerprint, key.keysize,
                 key.algorithm.name, key.active, key.can_encrypt)
+            # Sadly client.addKeyForTest does not return the key that
+            # was added:
+            if getFeatureFlag(GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG):
+                return GPGServiceKey(
+                    client.getKeyByFingerprint(key.fingerprint))
         return key
 
     def makePerson(

=== modified file 'lib/lp/testing/gpgkeys/__init__.py'
--- lib/lp/testing/gpgkeys/__init__.py	2016-02-24 22:43:07 +0000
+++ lib/lp/testing/gpgkeys/__init__.py	2016-03-21 05:51:58 +0000
@@ -31,6 +31,7 @@
 from lp.services.gpg.interfaces import (
     GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG,
     GPGKeyAlgorithm,
+    IGPGClient,
     IGPGHandler,
     )
 

=== modified file 'lib/lp/testing/gpgservice/_fixture.py'
--- lib/lp/testing/gpgservice/_fixture.py	2016-02-16 05:36:37 +0000
+++ lib/lp/testing/gpgservice/_fixture.py	2016-03-21 05:51:58 +0000
@@ -36,8 +36,8 @@
 
     def setUp(self):
         super(GPGKeyServiceFixture, self).setUp()
-        # Write service config to a file on disk. This file gets deleted when the
-        # fixture ends.
+        # Write service config to a file on disk. This file gets deleted
+        # when the fixture ends.
         service_config = _get_default_service_config()
         self._config_file = NamedTemporaryFile()
         self.addCleanup(self._config_file.close)
@@ -94,12 +94,15 @@
         raise RuntimeError("Service not responding: %r" % errors)
 
     def reset_service_database(self):
-        """Reset the gpgservice instance database to the launchpad sampledata."""
+        """Reset the gpgservice instance database to the launchpad sampledata.
+        """
         conn = httplib.HTTPConnection(self.bind_address)
         test_data = {
             'keys': [
                 {
-                    'owner': 'name16_oid',
+                    'owner':
+                        config.launchpad.openid_provider_root
+                        + '+id/name16_oid',
                     'id': '12345678',
                     'fingerprint': 'ABCDEF0123456789ABCDDCBA0000111112345678',
                     'size': 1024,

=== modified file 'lib/lp/testing/gpgservice/tests/test_fixture.py'
--- lib/lp/testing/gpgservice/tests/test_fixture.py	2016-03-16 20:41:49 +0000
+++ lib/lp/testing/gpgservice/tests/test_fixture.py	2016-03-21 05:51:58 +0000
@@ -47,9 +47,10 @@
     def test_fixture_can_create_test_data(self):
         fixture = self.useFixture(GPGKeyServiceFixture())
         conn = httplib.HTTPConnection(fixture.bind_address)
-        conn.request(
-            'GET',
-            '/users/%s/keys' % base64.b64encode('name16_oid', altchars='-_'))
+        user = base64.b64encode(
+            config.launchpad.openid_provider_root + '+id/name16_oid',
+            altchars='-_')
+        conn.request('GET', '/users/%s/keys' % user)
         resp = conn.getresponse()
         self.assertEqual(200, resp.status)
         data = json.loads(resp.read())

=== modified file 'setup.py'
--- setup.py	2016-02-05 20:28:29 +0000
+++ setup.py	2016-03-21 05:51:58 +0000
@@ -41,6 +41,7 @@
         'FeedParser',
         'feedvalidator',
         'funkload',
+        'gpgservice-client',
         'html5browser',
         'httmock',
         'pygpgme',

=== modified file 'versions.cfg'
--- versions.cfg	2016-03-14 02:01:04 +0000
+++ versions.cfg	2016-03-21 05:51:58 +0000
@@ -39,6 +39,7 @@
 FormEncode = 1.2.4
 funkload = 1.16.1
 gpgservice = 0.1.2
+gpgservice-client = 0.0.2
 grokcore.component = 1.6
 gunicorn = 19.4.5
 html5browser = 0.0.9


References