Merge lp:~sinzui/launchpad/faq-mailing-list-permissions-0 into lp:launchpad

Proposed by Curtis Hovey on 2010-12-06
Status: Merged
Approved by: Graham Binns on 2010-12-07
Approved revision: no longer in the source branch.
Merged at revision: 12028
Proposed branch: lp:~sinzui/launchpad/faq-mailing-list-permissions-0
Merge into: lp:launchpad
Diff against target: 251 lines (+195/-11)
4 files modified
lib/canonical/launchpad/security.py (+17/-4)
lib/lp/answers/doc/faq.txt (+1/-7)
lib/lp/answers/tests/test_faq.py (+77/-0)
lib/lp/answers/tests/test_faqtarget.py (+100/-0)
To merge this branch: bzr merge lp:~sinzui/launchpad/faq-mailing-list-permissions-0
Reviewer Review Type Date Requested Status
Graham Binns (community) code 2010-12-06 Approve on 2010-12-07
Review via email: mp+42888@code.launchpad.net

Description of the Change

Allow answer contacts for packages to create/edit FAQs.

    Launchpad bug: https://bugs.launchpad.net/bugs/682135
    Pre-implementation: jcsackett
    Test command: ./bin/test -vv -t test_faqtarget -t test_faq
      or to test all changes are kosher: -m lp.answers

Answer contacts for Ubuntu packages cannot create FAQs. There are cases where
the link is shown but the user gets an error when creating the FAQ. This
looked like a conflicting permission at first because the permissions were
changed a few months ago. Examining the history of the code and tests shows
that the permission has aways been broken.

The problem is in the security checker. The checker is based on the question
checker (answer_contacts), but the set of permitted users is larger.
Distribution users who can edit an FAQ are comprised of distro answer_contacts
+ the answer_contacts of all the packages.

That is a lot of inTeam() checks across a lot of packages. There is an
alternate way to solve this that will involve exactly two queries regardless
of the number of packages and answer_contacts. The checker can get the user's
direct and indirect questiontargets from the user and compare them to
the context.

--------------------------------------------------------------------

RULES

    * Update the AppendFAQTarget method use the direct and team answer
      contact methods to get all the question targets. Return True if any
      are the context faq target.
    * Update the AppendQuestion method to use the same principle to make
      answers permission checks faster for large projects.

QA

    * As an answer contact for a package, but not ubuntu. Verify you can
      create an FAQ from a question.
    * Verify you can edit that FAQ.

LINT

    lib/canonical/launchpad/security.py
    lib/lp/answers/doc/faq.txt
    lib/lp/answers/tests/test_faq.py
    lib/lp/answers/tests/test_faqtarget.py

IMPLEMENTATION

Added tests to verify the expect permissions for IFAQTarget and IFAQ. The
format is similar to the Question target tests that verify that all the
implementations have the same behaviour. The test confirmed the permissions
were broken. The change in the security check addressed the issue.
    lib/canonical/launchpad/security.py
    lib/lp/answers/tests/test_faq.py
    lib/lp/answers/tests/test_faqtarget.py

Removed the redundant and incomplete permissions check.
    lib/lp/answers/doc/faq.txt

Update the Question permission rules to reduce the number of queries called
in answers for large projects. test_questiontarget is the specific test that
covers permission, but I ran all lp.answers to ensure that there were no
behavioral changes.
    lib/canonical/launchpad/security.py

To post a comment you must log in.
Graham Binns (gmb) wrote :

Hi Curtis,

Two comments, one small, one big:

Small:

> 19 + # object is being examined; the implementers are no synonymous.

s/no/not/

Big:

Also, your tests need comments or docstrings explaining what they're
testing.

review: Needs Fixing (code)
Curtis Hovey (sinzui) wrote :
Download full text (6.5 KiB)

On Tue, 2010-12-07 at 11:01 +0000, Graham Binns wrote:
> Review: Needs Fixing code
> Hi Curtis,
>
> Two comments, one small, one big:
>
> Small:
>
> > 19 + # object is being examined; the implementers are no synonymous.
>
> s/no/not/

Fixed.

> Big:
>
> Also, your tests need comments or docstrings explaining what they're
> testing.

I added docstrings to the methods that users might want to reuse. I
added comments to the test methods.

{{{
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py 2010-12-06 23:13:44 +0000
+++ lib/canonical/launchpad/security.py 2010-12-07 13:45:19 +0000
@@ -1684,7 +1684,7 @@
             return True
         if IQuestionTarget.providedBy(self.obj):
             # Adapt QuestionTargets to FAQTargets to ensure the correct
- # object is being examined; the implementers are no synonymous.
+ # object is being examined; the implementers are not synonymous.
             faq_target = IFAQTarget(self.obj)
             questions_person = IQuestionsPerson(user.person)
             for target in questions_person.getDirectAnswerQuestionTargets():

=== modified file 'lib/lp/answers/tests/test_faq.py'
--- lib/lp/answers/tests/test_faq.py 2010-12-06 22:54:05 +0000
+++ lib/lp/answers/tests/test_faq.py 2010-12-07 14:13:42 +0000
@@ -18,6 +18,7 @@

 class TestFAQPermissions(TestCaseWithFactory):
+ """Test who can edit FAQs."""

     layer = DatabaseFunctionalLayer

@@ -29,29 +30,36 @@
             self.faq = self.factory.makeFAQ(target=target)

     def addAnswerContact(self, answer_contact):
+ """Add the test person to the faq target's answer contacts."""
         language_set = getUtility(ILanguageSet)
         answer_contact.addLanguage(language_set['en'])
         self.faq.target.addAnswerContact(answer_contact)

     def assertCanEdit(self, user, faq):
+ """Assert that the user can edit an FAQ."""
         can_edit = check_permission('launchpad.Edit', faq)
         self.assertTrue(can_edit, 'User cannot edit %s' % faq)

     def assertCannotEdit(self, user, faq):
+ """Assert that the user cannot edit an FAQ."""
         can_edit = check_permission('launchpad.Edit', faq)
         self.assertFalse(can_edit, 'User can edit edit %s' % faq)

     def test_owner_can_edit(self):
+ # The owner of an FAQ target can edit its FAQs.
         login_person(self.owner)
         self.assertCanEdit(self.owner, self.faq)

     def test_direct_answer_contact_can_edit(self):
+ # A direct answer contact for an FAQ target can edit its FAQs.
         direct_answer_contact = self.factory.makePerson()
         login_person(direct_answer_contact)
         self.addAnswerContact(direct_answer_contact)
         self.assertCanEdit(direct_answer_contact, self.faq)

     def test_indirect_answer_contact_can_edit(self):
+ # A indirect answer contact (a member of a team that is an answer
+ # contact) for an FAQ target can edit its FAQs.
         indirect_answer_contact = self.factory.makePerson()
         direct_answer_contact = self.factory.makeTeam()
         with person_logged_in(direct_answer_contact.teamowner):
@@ -...

Read more...

Graham Binns (gmb) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/security.py'
2--- lib/canonical/launchpad/security.py 2010-12-02 00:47:21 +0000
3+++ lib/canonical/launchpad/security.py 2010-12-07 14:17:15 +0000
4@@ -43,6 +43,7 @@
5 from lp.answers.interfaces.faq import IFAQ
6 from lp.answers.interfaces.faqtarget import IFAQTarget
7 from lp.answers.interfaces.question import IQuestion
8+from lp.answers.interfaces.questionsperson import IQuestionsPerson
9 from lp.answers.interfaces.questiontarget import IQuestionTarget
10 from lp.blueprints.interfaces.specification import (
11 ISpecification,
12@@ -1653,8 +1654,13 @@
13 """Allow user who can administer the question and answer contacts."""
14 if AdminQuestion.checkAuthenticated(self, user):
15 return True
16- for answer_contact in self.obj.target.answer_contacts:
17- if user.inTeam(answer_contact):
18+ question_target = self.obj.target
19+ questions_person = IQuestionsPerson(user.person)
20+ for target in questions_person.getDirectAnswerQuestionTargets():
21+ if target == question_target:
22+ return True
23+ for target in questions_person.getTeamAnswerQuestionTargets():
24+ if target == question_target:
25 return True
26 return False
27
28@@ -1677,8 +1683,15 @@
29 if EditByOwnersOrAdmins.checkAuthenticated(self, user):
30 return True
31 if IQuestionTarget.providedBy(self.obj):
32- for answer_contact in self.obj.answer_contacts:
33- if user.inTeam(answer_contact):
34+ # Adapt QuestionTargets to FAQTargets to ensure the correct
35+ # object is being examined; the implementers are not synonymous.
36+ faq_target = IFAQTarget(self.obj)
37+ questions_person = IQuestionsPerson(user.person)
38+ for target in questions_person.getDirectAnswerQuestionTargets():
39+ if IFAQTarget(target) == faq_target:
40+ return True
41+ for target in questions_person.getTeamAnswerQuestionTargets():
42+ if IFAQTarget(target) == faq_target:
43 return True
44 return False
45
46
47=== modified file 'lib/lp/answers/doc/faq.txt'
48--- lib/lp/answers/doc/faq.txt 2010-10-18 22:24:59 +0000
49+++ lib/lp/answers/doc/faq.txt 2010-12-07 14:17:15 +0000
50@@ -192,13 +192,7 @@
51 >>> check_permission('launchpad.Edit', firefox_faq)
52 True
53
54-But No Privileges Person doesn't:
55-
56- >>> login('no-priv@canonical.com')
57- >>> check_permission('launchpad.Edit', firefox_faq)
58- False
59-
60-But if we make him an answer contact, he will:
61+Answer contacts can also edit FAQs:
62
63 # An answer contact needs a preferred language.
64
65
66=== added file 'lib/lp/answers/tests/test_faq.py'
67--- lib/lp/answers/tests/test_faq.py 1970-01-01 00:00:00 +0000
68+++ lib/lp/answers/tests/test_faq.py 2010-12-07 14:17:15 +0000
69@@ -0,0 +1,77 @@
70+# Copyright 2010 Canonical Ltd. This software is licensed under the
71+# GNU Affero General Public License version 3 (see the file LICENSE).
72+
73+"""Tests for IFAQ"""
74+
75+__metaclass__ = type
76+
77+from zope.component import getUtility
78+
79+from canonical.testing.layers import DatabaseFunctionalLayer
80+from canonical.launchpad.webapp.authorization import check_permission
81+from lp.services.worlddata.interfaces.language import ILanguageSet
82+from lp.testing import (
83+ login_person,
84+ person_logged_in,
85+ TestCaseWithFactory,
86+ )
87+
88+
89+class TestFAQPermissions(TestCaseWithFactory):
90+ """Test who can edit FAQs."""
91+
92+ layer = DatabaseFunctionalLayer
93+
94+ def setUp(self):
95+ super(TestFAQPermissions, self).setUp()
96+ target = self.factory.makeProduct()
97+ self.owner = target.owner
98+ with person_logged_in(self.owner):
99+ self.faq = self.factory.makeFAQ(target=target)
100+
101+ def addAnswerContact(self, answer_contact):
102+ """Add the test person to the faq target's answer contacts."""
103+ language_set = getUtility(ILanguageSet)
104+ answer_contact.addLanguage(language_set['en'])
105+ self.faq.target.addAnswerContact(answer_contact)
106+
107+ def assertCanEdit(self, user, faq):
108+ """Assert that the user can edit an FAQ."""
109+ can_edit = check_permission('launchpad.Edit', faq)
110+ self.assertTrue(can_edit, 'User cannot edit %s' % faq)
111+
112+ def assertCannotEdit(self, user, faq):
113+ """Assert that the user cannot edit an FAQ."""
114+ can_edit = check_permission('launchpad.Edit', faq)
115+ self.assertFalse(can_edit, 'User can edit edit %s' % faq)
116+
117+ def test_owner_can_edit(self):
118+ # The owner of an FAQ target can edit its FAQs.
119+ login_person(self.owner)
120+ self.assertCanEdit(self.owner, self.faq)
121+
122+ def test_direct_answer_contact_can_edit(self):
123+ # A direct answer contact for an FAQ target can edit its FAQs.
124+ direct_answer_contact = self.factory.makePerson()
125+ login_person(direct_answer_contact)
126+ self.addAnswerContact(direct_answer_contact)
127+ self.assertCanEdit(direct_answer_contact, self.faq)
128+
129+ def test_indirect_answer_contact_can_edit(self):
130+ # A indirect answer contact (a member of a team that is an answer
131+ # contact) for an FAQ target can edit its FAQs.
132+ indirect_answer_contact = self.factory.makePerson()
133+ direct_answer_contact = self.factory.makeTeam()
134+ with person_logged_in(direct_answer_contact.teamowner):
135+ direct_answer_contact.addMember(
136+ indirect_answer_contact, direct_answer_contact.teamowner)
137+ self.addAnswerContact(direct_answer_contact)
138+ login_person(indirect_answer_contact)
139+ self.assertCanEdit(indirect_answer_contact, self.faq)
140+
141+ def test_nonparticipating_user_cannot_edit(self):
142+ # A user that is neither an owner of, or answer contact for, an
143+ # FAQ target's cannot edit a its FAQs.
144+ nonparticipant = self.factory.makePerson()
145+ login_person(nonparticipant)
146+ self.assertCannotEdit(nonparticipant, self.faq)
147
148=== added file 'lib/lp/answers/tests/test_faqtarget.py'
149--- lib/lp/answers/tests/test_faqtarget.py 1970-01-01 00:00:00 +0000
150+++ lib/lp/answers/tests/test_faqtarget.py 2010-12-07 14:17:15 +0000
151@@ -0,0 +1,100 @@
152+# Copyright 2010 Canonical Ltd. This software is licensed under the
153+# GNU Affero General Public License version 3 (see the file LICENSE).
154+
155+"""Tests for IFAQTarget"""
156+
157+__metaclass__ = type
158+
159+from zope.component import getUtility
160+
161+from canonical.testing.layers import DatabaseFunctionalLayer
162+from canonical.launchpad.webapp.authorization import check_permission
163+from lp.answers.interfaces.faqtarget import IFAQTarget
164+from lp.services.worlddata.interfaces.language import ILanguageSet
165+from lp.testing import (
166+ login_person,
167+ person_logged_in,
168+ TestCaseWithFactory,
169+ )
170+
171+
172+class BaseIFAQTargetTests:
173+ """Common tests for all IFAQTargets."""
174+
175+ layer = DatabaseFunctionalLayer
176+
177+ def addAnswerContact(self, answer_contact):
178+ """Add the test person to the faq target's answer contacts."""
179+ language_set = getUtility(ILanguageSet)
180+ answer_contact.addLanguage(language_set['en'])
181+ self.target.addAnswerContact(answer_contact)
182+
183+ def assertCanAppend(self, user, target):
184+ """Assert that the user can add an FAQ to an FAQ target."""
185+ can_append = check_permission('launchpad.Append', IFAQTarget(target))
186+ self.assertTrue(can_append, 'User cannot append to %s' % target)
187+
188+ def assertCannotAppend(self, user, target):
189+ """Assert that the user cannot add an FAQ to an FAQ target."""
190+ can_append = check_permission('launchpad.Append', IFAQTarget(target))
191+ self.assertFalse(can_append, 'User can append append to %s' % target)
192+
193+ def test_owner_can_append(self):
194+ # An owner of an FAQ target can add an FAQ.
195+ login_person(self.owner)
196+ self.assertCanAppend(self.owner, self.target)
197+
198+ def test_direct_answer_contact_can_append(self):
199+ # An direct answer contact for an FAQ target can add an FAQ.
200+ direct_answer_contact = self.factory.makePerson()
201+ login_person(direct_answer_contact)
202+ self.addAnswerContact(direct_answer_contact)
203+ self.assertCanAppend(direct_answer_contact, self.target)
204+
205+ def test_indirect_answer_contact_can_append(self):
206+ # An indirect answer contact (a member of a team that is an answer
207+ # contact) for an FAQ target can add an FAQ.
208+ indirect_answer_contact = self.factory.makePerson()
209+ direct_answer_contact = self.factory.makeTeam()
210+ with person_logged_in(direct_answer_contact.teamowner):
211+ direct_answer_contact.addMember(
212+ indirect_answer_contact, direct_answer_contact.teamowner)
213+ self.addAnswerContact(direct_answer_contact)
214+ login_person(indirect_answer_contact)
215+ self.assertCanAppend(indirect_answer_contact, self.target)
216+
217+ def test_nonparticipating_user_cannot_append(self):
218+ # A user that is neither an owner of, or answer contact for, an
219+ # FAQ target cannot add an FAQ.
220+ nonparticipant = self.factory.makePerson()
221+ login_person(nonparticipant)
222+ self.assertCannotAppend(nonparticipant, self.target)
223+
224+
225+class TestDistributionPermissions(BaseIFAQTargetTests, TestCaseWithFactory):
226+ """Test who can add FAQs to a distribution."""
227+
228+ def setUp(self):
229+ super(TestDistributionPermissions, self).setUp()
230+ self.target = self.factory.makeDistribution()
231+ self.owner = self.target.owner
232+
233+
234+class TestProductPermissions(BaseIFAQTargetTests, TestCaseWithFactory):
235+ """Test who can add FAQs to a product."""
236+
237+ def setUp(self):
238+ super(TestProductPermissions, self).setUp()
239+ self.target = self.factory.makeProduct()
240+ self.owner = self.target.owner
241+
242+
243+class TestDSPPermissions(BaseIFAQTargetTests, TestCaseWithFactory):
244+ """Test who can add FAQs for a distribution source package."""
245+
246+ def setUp(self):
247+ super(TestDSPPermissions, self).setUp()
248+ distribution = self.factory.makeDistribution()
249+ self.owner = distribution.owner
250+ self.target = self.factory.makeDistributionSourcePackage(
251+ sourcepackagename='fnord', distribution=distribution)