← Back to team overview

launchpad-reviewers team mailing list archive

lp:~wallyworld/launchpad/legacy-unsubscribe-revokes-access-997425 into lp:launchpad

 

Ian Booth has proposed merging lp:~wallyworld/launchpad/legacy-unsubscribe-revokes-access-997425 into lp:launchpad with lp:~wallyworld/launchpad/revoke-membership-delete-subscriptions-996882 as a prerequisite.

Requested reviews:
  Curtis Hovey (sinzui)
Related bugs:
  Bug #997425 in Launchpad itself: "Remove access when someone is unsubscribed from a bug or branch"
  https://bugs.launchpad.net/launchpad/+bug/997425

For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/legacy-unsubscribe-revokes-access-997425/+merge/105312

== Implementation ==

This branch starts the process of moving code to mirror bug subscriptions and bug visibility. Eventually subscriptions and visibility/access will be divorced, but we want to ease into that situation. We someone is unsubscribed from a bug, eventually we would not normally want to also revoke access. But for now we will. This is currently done by triggers but we want to delete those. So this branch adds the unsubscribe->revoke code to the model and puts it behind a feature flag so we can coordinate when it is turned on with the trigger removal.

To test, I needed a way to disable the triggers. So I added a new fixture, which allows selected triggers to be disabled as required. The fixture is DisableTriggerFixture.

== Tests ==

Added test for the new fixture:
bin/test -vvct TestDisableTriggerFixture

Added new tests for the bug unsubscribe->revoke code:
- test_privateBugUnsubscribeRevokesVisibility
- test_privateBugUnsubscribeRetainsVisibility

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/model/bug.py
  lib/lp/bugs/tests/test_bugvisibility.py
  lib/lp/services/features/flags.py
  lib/lp/testing/fixture.py
  lib/lp/testing/tests/test_fixture.py
-- 
https://code.launchpad.net/~wallyworld/launchpad/legacy-unsubscribe-revokes-access-997425/+merge/105312
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2012-05-09 06:12:52 +0000
+++ lib/lp/bugs/model/bug.py	2012-05-10 11:42:21 +0000
@@ -162,6 +162,10 @@
     PRIVATE_INFORMATION_TYPES,
     SECURITY_INFORMATION_TYPES,
     )
+from lp.registry.interfaces.accesspolicy import (
+    IAccessArtifactGrantSource,
+    IAccessArtifactSource,
+    )
 from lp.registry.interfaces.distribution import IDistribution
 from lp.registry.interfaces.distroseries import IDistroSeries
 from lp.registry.interfaces.person import (
@@ -880,6 +884,14 @@
                 store.flush()
                 self.updateHeat()
                 del get_property_cache(self)._known_viewers
+
+                # Revoke access to bug if feature flag is on.
+                flag = 'disclosure.legacy_subscription_visibility.enabled'
+                if bool(getFeatureFlag(flag)):
+                    artifacts_to_delete = getUtility(
+                        IAccessArtifactSource).find([self])
+                    getUtility(IAccessArtifactGrantSource).revokeByArtifact(
+                        artifacts_to_delete, [person])
                 return
 
     def unsubscribeFromDupes(self, person, unsubscribed_by):

=== modified file 'lib/lp/bugs/tests/test_bugvisibility.py'
--- lib/lp/bugs/tests/test_bugvisibility.py	2012-05-09 06:12:52 +0000
+++ lib/lp/bugs/tests/test_bugvisibility.py	2012-05-10 11:42:21 +0000
@@ -4,21 +4,26 @@
 """Tests for visibility of a bug."""
 
 from lp.registry.enums import InformationType
+from lp.services.features.testing import FeatureFixture
 from lp.testing import (
     celebrity_logged_in,
     TestCaseWithFactory,
     )
+from lp.testing.fixture import DisableTriggerFixture
 from lp.testing.layers import (
     DatabaseFunctionalLayer,
-    LaunchpadFunctionalLayer,
+    ZopelessDatabaseLayer,
     )
 
 
+LEGACY_VISIBILITY_FLAG = {
+    u"disclosure.legacy_subscription_visibility.enabled": u"true"}
+
+
 class TestPublicBugVisibility(TestCaseWithFactory):
     """Test visibility for a public bug."""
 
     layer = DatabaseFunctionalLayer
-    #layer = LaunchpadFunctionalLayer
 
     def setUp(self):
         super(TestPublicBugVisibility, self).setUp()
@@ -38,7 +43,7 @@
 class TestPrivateBugVisibility(TestCaseWithFactory):
     """Test visibility for a private bug."""
 
-    layer = LaunchpadFunctionalLayer
+    layer = ZopelessDatabaseLayer
 
     def setUp(self):
         super(TestPrivateBugVisibility, self).setUp()
@@ -78,6 +83,43 @@
             self.bug.subscribe(user, self.owner)
         self.assertTrue(self.bug.userCanView(user))
 
-    def test_publicBugAnonUser(self):
+    def test_privateBugAnonUser(self):
         # Since the bug is private, the anonymous user cannot see it.
         self.assertFalse(self.bug.userCanView(None))
+
+    @property
+    def disable_trigger_fixture(self):
+        return DisableTriggerFixture(
+                {'bugsubscription':
+                     'bugsubscription_mirror_legacy_access_t',
+                 'bug': 'bug_mirror_legacy_access_t',
+                 'bugtask': 'bugtask_mirror_legacy_access_t',
+            })
+
+    def test_privateBugUnsubscribeRevokesVisibility(self):
+        # A person unsubscribed from a private bug can no longer see it.
+        # Requires feature flag since the default model behaviour is to leave
+        # any access grants untouched.
+        # This test disables the current temporary database triggers which
+        # mirror subscription status into visibility.
+        with FeatureFixture(LEGACY_VISIBILITY_FLAG):
+            user = self.factory.makePerson()
+            with celebrity_logged_in('admin'):
+                self.bug.subscribe(user, self.owner)
+                self.assertTrue(self.bug.userCanView(user))
+                with self.disable_trigger_fixture:
+                    self.bug.unsubscribe(user, self.owner)
+            self.assertFalse(self.bug.userCanView(user))
+
+    def test_privateBugUnsubscribeRetainsVisibility(self):
+        # A person unsubscribed from a private bug can still see it if the
+        # feature flag to enable legacy subscription visibility is not set.
+        # This test disables the current temporary database triggers which
+        # mirror subscription status into visibility.
+        user = self.factory.makePerson()
+        with celebrity_logged_in('admin'):
+            self.bug.subscribe(user, self.owner)
+            self.assertTrue(self.bug.userCanView(user))
+            with self.disable_trigger_fixture:
+                self.bug.unsubscribe(user, self.owner)
+        self.assertTrue(self.bug.userCanView(user))

=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py	2012-05-08 01:31:10 +0000
+++ lib/lp/services/features/flags.py	2012-05-10 11:42:21 +0000
@@ -281,6 +281,13 @@
      '',
      '',
      ''),
+    ('disclosure.legacy_subscription_visibility.enabled',
+     'boolean',
+     ('If true, the legacy behaviour of unsubscribing from a bug or branch'
+      'revokes access.'),
+     '',
+     '',
+     ''),
     ('disclosure.enhanced_sharing.writable',
      'boolean',
      ('If true, will allow the use of the new sharing view and apis used '

=== modified file 'lib/lp/testing/fixture.py'
--- lib/lp/testing/fixture.py	2012-05-08 21:04:08 +0000
+++ lib/lp/testing/fixture.py	2012-05-10 11:42:21 +0000
@@ -7,6 +7,7 @@
 __all__ = [
     'CaptureOops',
     'DemoMode',
+    'DisableTriggerFixture',
     'PGBouncerFixture',
     'PGNotReadyError',
     'Urllib2Fixture',
@@ -25,7 +26,6 @@
     EnvironmentVariableFixture,
     Fixture,
     )
-import itertools
 from lazr.restful.utils import get_current_browser_request
 import oops
 import oops_amqp
@@ -41,6 +41,7 @@
 from zope.component import (
     adapter,
     getGlobalSiteManager,
+    getUtility,
     provideHandler,
     )
 from zope.interface import Interface
@@ -57,6 +58,12 @@
 from lp.services.messaging.rabbit import connect
 from lp.services.timeline.requesttimeline import get_request_timeline
 from lp.services.webapp.errorlog import ErrorReportEvent
+from lp.services.webapp.interfaces import (
+    DEFAULT_FLAVOR,
+    MAIN_STORE,
+    IStoreSelector,
+    )
+from lp.testing.dbuser import dbuser
 
 
 class PGNotReadyError(Exception):
@@ -132,7 +139,7 @@
         """Start PGBouncer, waiting for it to accept connections if neccesary.
         """
         super(PGBouncerFixture, self).start()
-        s = socket.socket()
+        socket.socket()
         for i in xrange(retries):
             try:
                 socket.create_connection((self.host, self.port))
@@ -146,7 +153,6 @@
             raise PGNotReadyError("Not ready after %d attempts." % retries)
 
 
-
 class ZopeAdapterFixture(Fixture):
     """A fixture to register and unregister an adapter."""
 
@@ -388,3 +394,33 @@
 <a href="http://example.com";>File a bug</a>.
             ''')
         self.addCleanup(lambda: config.pop('demo-fixture'))
+
+
+class DisableTriggerFixture(Fixture):
+    """Let tests disable database triggers."""
+
+    def __init__(self, table_triggers=None):
+        self.table_triggers = table_triggers or {}
+
+    def setUp(self):
+        super(DisableTriggerFixture, self).setUp()
+        self._disable_triggers()
+        self.addCleanup(self._enable_triggers)
+
+    def _process_triggers(self, mode):
+        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+        with dbuser('postgres'):
+            for table, trigger in self.table_triggers.items():
+                sql = ("ALTER TABLE %(table)s %(mode)s trigger "
+                       "%(trigger)s") % {
+                    'table': table,
+                    'mode': mode,
+                    'trigger': trigger,
+                }
+                store.execute(sql)
+
+    def _disable_triggers(self):
+        self._process_triggers(mode='DISABLE')
+
+    def _enable_triggers(self):
+        self._process_triggers(mode='ENABLE')

=== modified file 'lib/lp/testing/tests/test_fixture.py'
--- lib/lp/testing/tests/test_fixture.py	2012-01-18 07:35:31 +0000
+++ lib/lp/testing/tests/test_fixture.py	2012-05-10 11:42:21 +0000
@@ -36,6 +36,7 @@
 from lp.testing import TestCase
 from lp.testing.fixture import (
     CaptureOops,
+    DisableTriggerFixture,
     PGBouncerFixture,
     ZopeAdapterFixture,
     ZopeUtilityFixture,
@@ -45,6 +46,7 @@
     DatabaseLayer,
     LaunchpadLayer,
     LaunchpadZopelessLayer,
+    ZopelessDatabaseLayer,
     )
 
 
@@ -263,3 +265,62 @@
         capture = self.useFixture(CaptureOops())
         capture.sync()
         capture.sync()
+
+
+class TestDisableTriggerFixture(TestCase):
+    """Test the DisableTriggerFixture class."""
+
+    layer = ZopelessDatabaseLayer
+
+    def setUp(self):
+        super(TestDisableTriggerFixture, self).setUp()
+        con_str = dbconfig.rw_main_master + ' user=launchpad_main'
+        con = psycopg2.connect(con_str)
+        con.set_isolation_level(0)
+        self.cursor = con.cursor()
+        # Create a test table and trigger.
+        setup_sql = """
+        CREATE OR REPLACE FUNCTION trig() RETURNS trigger
+        LANGUAGE plpgsql
+        AS
+        'BEGIN
+            update test_trigger set col_b = NEW.col_a
+            where col_a = NEW.col_a;
+            RETURN NULL;
+        END;';
+        DROP TABLE IF EXISTS test_trigger CASCADE;
+        CREATE TABLE test_trigger(col_a integer, col_b integer);
+        CREATE TRIGGER test_trigger_t
+            AFTER INSERT on test_trigger
+            FOR EACH ROW EXECUTE PROCEDURE trig();
+        """
+        self.cursor.execute(setup_sql)
+        self.addCleanup(self._cleanup)
+
+    def _cleanup(self):
+        self.cursor.execute('DROP TABLE test_trigger CASCADE;')
+        self.cursor.close()
+        self.cursor.connection.close()
+
+    def test_triggers_are_disabled(self):
+        # Test that the fixture correctly disables specified triggers.
+        with DisableTriggerFixture({'test_trigger': 'test_trigger_t'}):
+            self.cursor.execute(
+                'INSERT INTO test_trigger(col_a) values (1)')
+            self.cursor.execute(
+                'SELECT col_b FROM test_trigger WHERE col_a = 1')
+            [col_b] = self.cursor.fetchone()
+            self.assertEqual(None, col_b)
+
+    def test_triggers_are_enabled_after(self):
+        # Test that the fixture correctly enables the triggers again when it
+        # is done.
+        with DisableTriggerFixture({'test_trigger': 'test_trigger_t'}):
+            self.cursor.execute(
+                'INSERT INTO test_trigger(col_a) values (1)')
+        self.cursor.execute(
+            'INSERT INTO test_trigger(col_a) values (2)')
+        self.cursor.execute(
+            'SELECT col_b FROM test_trigger WHERE col_a = 2')
+        [col_b] = self.cursor.fetchone()
+        self.assertEqual(2, col_b)


Follow ups