Merge lp:~wgrant/launchpad/bug-mirror-access-policies into lp:launchpad/db-devel

Proposed by William Grant
Status: Merged
Merged at revision: 11413
Proposed branch: lp:~wgrant/launchpad/bug-mirror-access-policies
Merge into: lp:launchpad/db-devel
Prerequisite: lp:~wgrant/launchpad/multipolicy-4
Diff against target: 426 lines (+406/-0)
3 files modified
database/schema/patch-2209-11-3.sql (+192/-0)
database/schema/security.cfg (+1/-0)
lib/lp/bugs/tests/test_bug_mirror_access_triggers.py (+213/-0)
To merge this branch: bzr merge lp:~wgrant/launchpad/bug-mirror-access-policies
Reviewer Review Type Date Requested Status
Stuart Bishop (community) db + tests Approve
Robert Collins db Pending
Review via email: mp+94722@code.launchpad.net

Commit message

Triggers to mirror legacy bug disclosure into the new access policy schema.

Description of the change

This branch adds a PL/pgSQL function to mirror legacy bug disclosure (Bug, BugTask, BugSubscription) to the new schema (AccessArtifact, AccessArtifactGrant, AccessPolicyArtifact), and triggers to handle all updates. As discussed with stub last week, this is to let us safely gradually migrate to the new schema -- most of the triggers and function will go away once the migration is complete.

The mapping should be reasonably clear. If the bug is private, the set of AccessPolicyArtifacts is synced with the corresponding policies of the BugTasks' pillars, and the set of AccessArtifactGrants is synced with the BugSubscriptions. If the bug is public, anything that exists is removed.

This hasn't been tested on the stagings, only dogfood. On DF it performs at an average of ~0.8ms per bug when doing an initial population pass over every bug.

There are tests for most of the trigger and function behaviour.

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

On Mon, Feb 27, 2012 at 1:36 PM, William Grant <email address hidden> wrote:

> +        INSERT INTO AccessArtifactGrant
> +            (artifact, grantee, grantor, date_created)
> +            SELECT DISTINCT ON (artifact_id, BugSubscription.person)
> +                   artifact_id, BugSubscription.person,
> +                   BugSubscription.subscribed_by, BugSubscription.date_created
> +                FROM
> +                    BugSubscription
> +                    LEFT JOIN AccessArtifactGrant
> +                        ON (AccessArtifactGrant.grantee =
> +                                BugSubscription.person
> +                            AND AccessArtifactGrant.artifact = artifact_id)
> +                WHERE
> +                    AccessArtifactGrant.grantee IS NULL
> +                    AND BugSubscription.bug = bug_id;

You need an ORDER BY clause here for the SELECT DISTINCT ON. I think
"ORDER BY artifact_id, BugSubscription.person,
BugSubscription.date_created".

Tests look good.

--
Stuart Bishop <email address hidden>

Revision history for this message
Stuart Bishop (stub) :
review: Approve (db + tests)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'database/schema/patch-2209-11-3.sql'
2--- database/schema/patch-2209-11-3.sql 1970-01-01 00:00:00 +0000
3+++ database/schema/patch-2209-11-3.sql 2012-02-27 09:40:26 +0000
4@@ -0,0 +1,192 @@
5+-- Copyright 2012 Canonical Ltd. This software is licensed under the
6+-- GNU Affero General Public License version 3 (see the file LICENSE).
7+
8+SET client_min_messages=ERROR;
9+
10+-- Magical superfunction to bring a bug's new access data into consistency
11+-- with its legacy data.
12+
13+CREATE OR REPLACE FUNCTION bug_mirror_legacy_access(bug_id integer) RETURNS void
14+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
15+ AS $$
16+DECLARE
17+ bug_row record;
18+ artifact_id integer;
19+ bugtask_row record;
20+ policy_type integer;
21+ pillars record;
22+ access_policies integer[];
23+BEGIN
24+ SELECT * INTO bug_row FROM bug WHERE id = bug_id;
25+ SELECT id INTO artifact_id FROM AccessArtifact WHERE bug = bug_id;
26+ IF NOT bug_row.private THEN
27+ IF artifact_id IS NOT NULL THEN
28+ -- Bug is public, but there are access control rows. Destroy them.
29+ DELETE FROM AccessArtifactGrant WHERE artifact = artifact_id;
30+ DELETE FROM AccessPolicyArtifact WHERE artifact = artifact_id;
31+ DELETE FROM AccessArtifact WHERE id = artifact_id;
32+ END IF;
33+ ELSE
34+ -- Bug is private. Add missing rows, remove superfluous ones.
35+
36+ -- Ensure that there's a corresponding AccessArtifact.
37+ IF artifact_id IS NULL THEN
38+ INSERT INTO AccessArtifact (bug) VALUES (bug_row.id)
39+ RETURNING id INTO artifact_id;
40+ END IF;
41+
42+ -- Ensure that the AccessArtifactGrants match BugSubscriptions.
43+ DELETE FROM AccessArtifactGrant
44+ WHERE
45+ artifact = artifact_id
46+ AND grantee NOT IN (
47+ SELECT person FROM bugsubscription WHERE bug = bug_id);
48+ INSERT INTO AccessArtifactGrant
49+ (artifact, grantee, grantor, date_created)
50+ SELECT DISTINCT ON (artifact_id, BugSubscription.person)
51+ artifact_id, BugSubscription.person,
52+ BugSubscription.subscribed_by, BugSubscription.date_created
53+ FROM
54+ BugSubscription
55+ LEFT JOIN AccessArtifactGrant
56+ ON (AccessArtifactGrant.grantee =
57+ BugSubscription.person
58+ AND AccessArtifactGrant.artifact = artifact_id)
59+ WHERE
60+ AccessArtifactGrant.grantee IS NULL
61+ AND BugSubscription.bug = bug_id
62+ ORDER BY
63+ artifact_id,
64+ BugSubscription.person,
65+ BugSubscription.date_created;
66+
67+ -- Ensure that AccessPolicyArtifacts match the implied policy
68+ -- type and the tasks' pillars.
69+ SELECT (CASE
70+ WHEN NOT bug_row.security_related THEN 4
71+ WHEN bug_row.security_related THEN 3
72+ END) INTO policy_type;
73+ SELECT
74+ array_agg(
75+ DISTINCT COALESCE(bugtask.product, productseries.product))
76+ AS products,
77+ array_agg(
78+ DISTINCT COALESCE(bugtask.distribution,
79+ distroseries.distribution))
80+ AS distributions
81+ INTO pillars
82+ FROM
83+ bugtask
84+ LEFT JOIN productseries
85+ ON productseries.id = bugtask.productseries
86+ LEFT JOIN distroseries
87+ ON distroseries.id = bugtask.distroseries
88+ WHERE bug = bug_id;
89+ SELECT array_agg(id) FROM AccessPolicy
90+ INTO access_policies
91+ WHERE
92+ type = policy_type
93+ AND (
94+ (product IS NOT NULL AND product = ANY(pillars.products))
95+ OR (distribution IS NOT NULL
96+ AND distribution = ANY(pillars.distributions)));
97+ DELETE FROM AccessPolicyArtifact
98+ WHERE
99+ artifact = artifact_id
100+ AND policy != ALL(access_policies);
101+ INSERT INTO AccessPolicyArtifact
102+ (artifact, policy)
103+ SELECT DISTINCT artifact_id, policy
104+ FROM unnest(access_policies) AS policy
105+ EXCEPT
106+ SELECT artifact_id, policy FROM AccessPolicyArtifact
107+ WHERE artifact = artifact_id;
108+ END IF;
109+END;
110+$$;
111+
112+-- Bug triggers
113+
114+CREATE OR REPLACE FUNCTION bug_mirror_legacy_access_trig() RETURNS trigger
115+ LANGUAGE plpgsql
116+ AS $$
117+BEGIN
118+ IF TG_OP = 'INSERT' THEN
119+ PERFORM bug_mirror_legacy_access(NEW.id);
120+ ELSIF TG_OP = 'UPDATE' THEN
121+ IF NEW.private != OLD.private OR NEW.security_related != OLD.security_related THEN
122+ PERFORM bug_mirror_legacy_access(OLD.id);
123+ END IF;
124+ END IF;
125+ RETURN NULL;
126+END;
127+$$;
128+
129+
130+DROP TRIGGER IF EXISTS bug_mirror_legacy_access_t ON bug;
131+CREATE TRIGGER bug_mirror_legacy_access_t
132+ AFTER INSERT OR UPDATE ON bug
133+ FOR EACH ROW EXECUTE PROCEDURE bug_mirror_legacy_access_trig();
134+
135+
136+-- BugTask triggers
137+
138+CREATE OR REPLACE FUNCTION bugtask_mirror_legacy_access_trig() RETURNS trigger
139+ LANGUAGE plpgsql
140+ AS $$
141+BEGIN
142+ IF TG_OP = 'INSERT' THEN
143+ PERFORM bug_mirror_legacy_access(NEW.bug);
144+ ELSIF TG_OP = 'UPDATE' THEN
145+ IF NEW.bug != OLD.bug THEN
146+ RAISE EXCEPTION 'cannot move bugtask to a different bug';
147+ END IF;
148+ IF (NEW.distribution IS DISTINCT FROM OLD.distribution
149+ OR NEW.product IS DISTINCT FROM OLD.product
150+ OR NEW.distroseries IS DISTINCT FROM OLD.distroseries
151+ OR NEW.productseries IS DISTINCT FROM OLD.productseries) THEN
152+ PERFORM bug_mirror_legacy_access(OLD.bug);
153+ END IF;
154+ ELSIF TG_OP = 'DELETE' THEN
155+ PERFORM bug_mirror_legacy_access(OLD.bug);
156+ END IF;
157+ RETURN NULL;
158+END;
159+$$;
160+
161+
162+DROP TRIGGER IF EXISTS bugtask_mirror_legacy_access_t ON bugtask;
163+CREATE TRIGGER bugtask_mirror_legacy_access_t
164+ AFTER INSERT OR UPDATE OR DELETE ON bugtask
165+ FOR EACH ROW EXECUTE PROCEDURE bugtask_mirror_legacy_access_trig();
166+
167+
168+-- BugSubscription triggers
169+
170+CREATE OR REPLACE FUNCTION bugsubscription_mirror_legacy_access_trig() RETURNS trigger
171+ LANGUAGE plpgsql
172+ AS $$
173+BEGIN
174+ IF TG_OP = 'INSERT' THEN
175+ PERFORM bug_mirror_legacy_access(NEW.bug);
176+ ELSIF TG_OP = 'UPDATE' THEN
177+ IF NEW.bug != OLD.bug THEN
178+ RAISE EXCEPTION 'cannot move bugsubscription to a different bug';
179+ END IF;
180+ IF NEW.person != OLD.person THEN
181+ PERFORM bug_mirror_legacy_access(OLD.bug);
182+ END IF;
183+ ELSIF TG_OP = 'DELETE' THEN
184+ PERFORM bug_mirror_legacy_access(OLD.bug);
185+ END IF;
186+ RETURN NULL;
187+END;
188+$$;
189+
190+DROP TRIGGER IF EXISTS
191+ bugsubscription_mirror_legacy_access_t ON bugsubscription;
192+CREATE TRIGGER bugsubscription_mirror_legacy_access_t
193+ AFTER INSERT OR UPDATE OR DELETE ON bugsubscription
194+ FOR EACH ROW EXECUTE PROCEDURE bugsubscription_mirror_legacy_access_trig();
195+
196+INSERT INTO LaunchpadDatabaseRevision VALUES (2209, 11, 3);
197
198=== modified file 'database/schema/security.cfg'
199--- database/schema/security.cfg 2012-02-27 09:40:25 +0000
200+++ database/schema/security.cfg 2012-02-27 09:40:26 +0000
201@@ -15,6 +15,7 @@
202 public.add_test_openid_identifier(integer) = EXECUTE
203 public.alllocks =
204 public.assert_patch_applied(integer, integer, integer) = EXECUTE
205+public.bug_mirror_legacy_access(integer) =
206 public.bug_update_latest_patch_uploaded(integer) =
207 public.bugnotificationarchive =
208 public.calculate_bug_heat(integer) = EXECUTE
209
210=== added file 'lib/lp/bugs/tests/test_bug_mirror_access_triggers.py'
211--- lib/lp/bugs/tests/test_bug_mirror_access_triggers.py 1970-01-01 00:00:00 +0000
212+++ lib/lp/bugs/tests/test_bug_mirror_access_triggers.py 2012-02-27 09:40:26 +0000
213@@ -0,0 +1,213 @@
214+# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
215+# GNU Affero General Public License version 3 (see the file LICENSE).
216+
217+__metaclass__ = type
218+
219+from storm.store import Store
220+from testtools.matchers import (
221+ MatchesSetwise,
222+ MatchesStructure,
223+ )
224+from zope.component import getUtility
225+from zope.security.proxy import removeSecurityProxy
226+
227+from lp.registry.enums import AccessPolicyType
228+from lp.registry.interfaces.accesspolicy import (
229+ IAccessArtifactGrantSource,
230+ IAccessArtifactSource,
231+ IAccessPolicySource,
232+ IAccessPolicyArtifactSource,
233+ )
234+from lp.services.features.testing import FeatureFixture
235+from lp.testing import TestCaseWithFactory
236+from lp.testing.layers import DatabaseFunctionalLayer
237+
238+
239+class TestBugMirrorAccessTriggers(TestCaseWithFactory):
240+
241+ layer = DatabaseFunctionalLayer
242+
243+ def setUp(self):
244+ super(TestBugMirrorAccessTriggers, self).setUp()
245+ self.useFixture(FeatureFixture(
246+ {'disclosure.allow_multipillar_private_bugs.enabled': 'true'}))
247+
248+ def assertMirrored(self, bug):
249+ """Check that a bug has been correctly mirrored to the new schema.
250+
251+ :return: A tuple of
252+ (AccessArtifactGrant count, AccessPolicyArtifact count) for
253+ additional checking.
254+ """
255+ # An AccessPolicyArtifact exists.
256+ artifact = getUtility(IAccessArtifactSource).find([bug]).one()
257+ self.assertIsNot(None, artifact.concrete_artifact)
258+
259+ # There is an AccessArtifactGrant for the subscriber.
260+ subs = bug.getDirectSubscriptions()
261+ grants = list(
262+ getUtility(IAccessArtifactGrantSource).findByArtifact(
263+ [artifact]))
264+ self.assertThat(
265+ grants,
266+ MatchesSetwise(*[
267+ MatchesStructure.byEquality(
268+ grantee=sub.person, grantor=sub.subscribed_by,
269+ date_created=sub.date_created) for sub in subs]))
270+ grant_count = len(grants)
271+
272+ if removeSecurityProxy(bug).security_related:
273+ policy_type = AccessPolicyType.EMBARGOEDSECURITY
274+ else:
275+ policy_type = AccessPolicyType.USERDATA
276+
277+ # Get the relevant access policies, confirming that there's one
278+ # for every pillar.
279+ pillars = set(
280+ task.pillar for task in removeSecurityProxy(bug).bugtasks)
281+ expected_policies = list(getUtility(IAccessPolicySource).find(
282+ [(pillar, policy_type) for pillar in pillars]))
283+ self.assertEqual(len(pillars), len(expected_policies))
284+
285+ # There are AccessPolicyArtifacts for each relevant policy.
286+ policies = self.getPoliciesForArtifact(artifact)
287+ self.assertContentEqual(expected_policies, policies)
288+ policy_count = len(policies)
289+
290+ # And return some counts so the test can see we looked things up
291+ # properly.
292+ return grant_count, policy_count
293+
294+ def makeBugAndPolicies(self, private=False, policy_types=None):
295+ if policy_types is None:
296+ policy_types = [
297+ AccessPolicyType.USERDATA, AccessPolicyType.EMBARGOEDSECURITY]
298+ product = self.factory.makeProduct()
299+ for policy_type in policy_types:
300+ self.factory.makeAccessPolicy(pillar=product, type=policy_type)
301+ bug = self.factory.makeBug(private=private, product=product)
302+ return removeSecurityProxy(bug)
303+
304+ def getPoliciesForArtifact(self, artifact):
305+ return set(
306+ apa.policy for apa in
307+ getUtility(IAccessPolicyArtifactSource).findByArtifact(
308+ [artifact]))
309+
310+ def getPolicyTypesForArtifact(self, artifact):
311+ return set(
312+ policy.type for policy in self.getPoliciesForArtifact(artifact))
313+
314+ def test_public_has_nothing(self):
315+ bug = self.factory.makeBug(private=False)
316+ artifact = getUtility(IAccessArtifactSource).find([bug]).one()
317+ self.assertIs(None, artifact)
318+
319+ def test_private(self):
320+ bug = self.makeBugAndPolicies(private=True)
321+ self.assertEqual((1, 1), self.assertMirrored(bug))
322+
323+ def test_add_subscriber(self):
324+ bug = self.makeBugAndPolicies(private=True)
325+ person = self.factory.makePerson()
326+ bug.subscribe(person, person)
327+ self.assertEqual((2, 1), self.assertMirrored(bug))
328+
329+ def test_remove_subscriber(self):
330+ bug = self.makeBugAndPolicies(private=True)
331+ person = self.factory.makePerson()
332+ bug.subscribe(person, person)
333+ Store.of(bug).flush()
334+ bug.unsubscribe(person, person)
335+ self.assertEqual((1, 1), self.assertMirrored(bug))
336+
337+ def test_add_task(self):
338+ # Adding a task on a new product links its policy.
339+ product = self.factory.makeProduct()
340+ self.factory.makeAccessPolicy(
341+ pillar=product, type=AccessPolicyType.USERDATA)
342+
343+ bug = self.makeBugAndPolicies(private=True)
344+ bug.addTask(bug.owner, product)
345+ self.assertEqual((1, 2), self.assertMirrored(bug))
346+
347+ def test_remove_task(self):
348+ # Removing a task removes its policy.
349+ product = self.factory.makeProduct()
350+ self.factory.makeAccessPolicy(
351+ pillar=product, type=AccessPolicyType.USERDATA)
352+
353+ bug = self.makeBugAndPolicies(private=True)
354+ task = bug.addTask(bug.owner, product)
355+ Store.of(bug).flush()
356+ removeSecurityProxy(task).destroySelf()
357+ self.assertEqual((1, 1), self.assertMirrored(bug))
358+
359+ def test_make_public(self):
360+ bug = self.makeBugAndPolicies(private=True)
361+ self.assertIsNot(
362+ None, getUtility(IAccessArtifactSource).find([bug]).one())
363+ bug.private = False
364+ self.assertIs(
365+ None, getUtility(IAccessArtifactSource).find([bug]).one())
366+
367+ def test_make_private(self):
368+ bug = self.makeBugAndPolicies(private=False)
369+ self.assertIs(
370+ None, getUtility(IAccessArtifactSource).find([bug]).one())
371+ bug.private = True
372+ self.assertIsNot(
373+ None, getUtility(IAccessArtifactSource).find([bug]).one())
374+ self.assertEqual((1, 1), self.assertMirrored(bug))
375+
376+ def test_security_related(self):
377+ # Setting the security_related flag uses EMBARGOEDSECURITY
378+ # policies instead of USERDATA.
379+ bug = self.makeBugAndPolicies(private=True)
380+ [artifact] = getUtility(IAccessArtifactSource).find([bug])
381+ self.assertEqual((1, 1), self.assertMirrored(bug))
382+ self.assertContentEqual(
383+ [AccessPolicyType.USERDATA],
384+ self.getPolicyTypesForArtifact(artifact))
385+ bug.security_related = True
386+ self.assertEqual((1, 1), self.assertMirrored(bug))
387+ self.assertContentEqual(
388+ [AccessPolicyType.EMBARGOEDSECURITY],
389+ self.getPolicyTypesForArtifact(artifact))
390+
391+ def test_productseries_task(self):
392+ # A productseries task causes a link to its product's policy.
393+ productseries = self.factory.makeProductSeries()
394+ self.factory.makeAccessPolicy(
395+ pillar=productseries.product, type=AccessPolicyType.USERDATA)
396+
397+ bug = self.makeBugAndPolicies(private=True)
398+ bug.addTask(bug.owner, productseries)
399+ self.assertEqual((1, 2), self.assertMirrored(bug))
400+ # Adding the product doesn't increase the policy count.
401+ bug.addTask(bug.owner, productseries.product)
402+ self.assertEqual((1, 2), self.assertMirrored(bug))
403+
404+ def test_distribution_task(self):
405+ # A distribution task causes a link to its policy.
406+ distro = self.factory.makeDistribution()
407+ self.factory.makeAccessPolicy(
408+ pillar=distro, type=AccessPolicyType.USERDATA)
409+
410+ bug = self.makeBugAndPolicies(private=True)
411+ bug.addTask(bug.owner, distro)
412+ self.assertEqual((1, 2), self.assertMirrored(bug))
413+
414+ def test_distroseries_task(self):
415+ # A distroseries task causes a link to its distribution's
416+ # policy.
417+ distroseries = self.factory.makeDistroSeries()
418+ self.factory.makeAccessPolicy(
419+ pillar=distroseries.distribution, type=AccessPolicyType.USERDATA)
420+
421+ bug = self.makeBugAndPolicies(private=True)
422+ bug.addTask(bug.owner, distroseries)
423+ self.assertEqual((1, 2), self.assertMirrored(bug))
424+ # Adding the distro doesn't increase the policy count.
425+ bug.addTask(bug.owner, distroseries.distribution)
426+ self.assertEqual((1, 2), self.assertMirrored(bug))

Subscribers

People subscribed via source and target branches

to status/vote changes: