Merge ~lgp171188/launchpad:new-security-role-permission into launchpad:master
- Git
- lp:~lgp171188/launchpad
- new-security-role-permission
- Merge into master
Proposed by
Guruprasad
Status: | Merged |
---|---|
Approved by: | Guruprasad |
Approved revision: | 25b9c52ae3f37c65395659884189fa5d29c63aaa |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~lgp171188/launchpad:new-security-role-permission |
Merge into: | launchpad:master |
Diff against target: |
562 lines (+312/-42) 9 files modified
lib/lp/bugs/model/tests/test_vulnerability.py (+40/-4) lib/lp/bugs/security.py (+11/-0) lib/lp/bugs/tests/test_vulnerability.py (+63/-0) lib/lp/permissions.zcml (+4/-0) lib/lp/registry/configure.zcml (+4/-0) lib/lp/registry/interfaces/distribution.py (+13/-4) lib/lp/registry/model/distribution.py (+3/-0) lib/lp/registry/tests/test_distribution.py (+159/-22) lib/lp/security.py (+15/-12) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Andrey Fedoseev (community) | Approve | ||
Review via email: mp+423363@code.launchpad.net |
Commit message
Add a new distribution security admin role
And restrict the permissions on creating and editing vulnerabilities
to it.
Description of the change
To post a comment you must log in.
Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote : | # |
Revision history for this message
Guruprasad (lgp171188) wrote : | # |
> I have doubts about the the permission name: `SecurityAdmin`. I think there's a convention to use a verb in the permission names. `SecurityAdmin` sounds more like a role than a permission.
>
> Could it be `ManageVulnerab
I thought about it when I was about to name it and found that there are some existing permissions that do not fit in this mould - `launchpad.
Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote : | # |
Approved
review:
Approve
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Needs Fixing
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/bugs/model/tests/test_vulnerability.py b/lib/lp/bugs/model/tests/test_vulnerability.py |
2 | index f2cac72..f470e40 100644 |
3 | --- a/lib/lp/bugs/model/tests/test_vulnerability.py |
4 | +++ b/lib/lp/bugs/model/tests/test_vulnerability.py |
5 | @@ -2,8 +2,10 @@ |
6 | # GNU Affero General Public License version 3 (see the file LICENSE). |
7 | |
8 | """Tests for the vulnerability and related models.""" |
9 | +from testtools.matchers import MatchesStructure |
10 | from zope.component import getUtility |
11 | |
12 | +from lp.bugs.enums import VulnerabilityStatus |
13 | from lp.bugs.interfaces.buglink import IBugLinkTarget |
14 | from lp.bugs.interfaces.vulnerability import ( |
15 | IVulnerability, |
16 | @@ -39,34 +41,68 @@ class TestVulnerability(TestCaseWithFactory): |
17 | vulnerability = self.factory.makeVulnerability() |
18 | self.assertTrue(verifyObject(IBugLinkTarget, vulnerability)) |
19 | |
20 | - def test_random_user(self): |
21 | + def test_random_user_permissions(self): |
22 | with person_logged_in(self.factory.makePerson()): |
23 | self.assertTrue( |
24 | check_permission("launchpad.View", self.vulnerability)) |
25 | self.assertFalse( |
26 | check_permission("launchpad.Edit", self.vulnerability)) |
27 | |
28 | - def test_admin(self): |
29 | + def test_admin_permissions(self): |
30 | with admin_logged_in(): |
31 | self.assertTrue( |
32 | check_permission("launchpad.View", self.vulnerability)) |
33 | self.assertTrue( |
34 | check_permission("launchpad.Edit", self.vulnerability)) |
35 | |
36 | - def test_non_admin(self): |
37 | + def test_distribution_owner_permissions(self): |
38 | with person_logged_in(self.distribution.owner): |
39 | self.assertTrue( |
40 | check_permission("launchpad.View", self.vulnerability)) |
41 | self.assertTrue( |
42 | check_permission("launchpad.Edit", self.vulnerability)) |
43 | |
44 | - def test_anonymous(self): |
45 | + def test_security_admin_permissions(self): |
46 | + person = self.factory.makePerson() |
47 | + security_team = self.factory.makeTeam(members=[person]) |
48 | + with person_logged_in(self.distribution.owner): |
49 | + self.distribution.security_admin = security_team |
50 | + |
51 | + with person_logged_in(person): |
52 | + self.assertTrue( |
53 | + check_permission( |
54 | + "launchpad.Edit", self.vulnerability |
55 | + ) |
56 | + ) |
57 | + |
58 | + def test_anonymous_permissions(self): |
59 | with anonymous_logged_in(): |
60 | self.assertFalse( |
61 | check_permission("launchpad.View", self.vulnerability)) |
62 | self.assertFalse( |
63 | check_permission("launchpad.Edit", self.vulnerability)) |
64 | |
65 | + def test_edit_vulnerability_security_admin(self): |
66 | + person = self.factory.makePerson() |
67 | + security_team = self.factory.makeTeam(members=[person]) |
68 | + vulnerability = self.factory.makeVulnerability( |
69 | + distribution=self.distribution |
70 | + ) |
71 | + cve = self.factory.makeCVE('2022-1234') |
72 | + with person_logged_in(self.distribution.owner): |
73 | + self.distribution.security_admin = security_team |
74 | + |
75 | + with person_logged_in(security_team): |
76 | + vulnerability.cve = cve |
77 | + vulnerability.status = VulnerabilityStatus.ACTIVE |
78 | + self.assertThat( |
79 | + vulnerability, |
80 | + MatchesStructure.byEquality( |
81 | + cve=cve, |
82 | + status=VulnerabilityStatus.ACTIVE |
83 | + ) |
84 | + ) |
85 | + |
86 | |
87 | class TestVulnerabilityActivity(TestCaseWithFactory): |
88 | |
89 | diff --git a/lib/lp/bugs/security.py b/lib/lp/bugs/security.py |
90 | index 5e40c90..ce1829a 100644 |
91 | --- a/lib/lp/bugs/security.py |
92 | +++ b/lib/lp/bugs/security.py |
93 | @@ -23,6 +23,7 @@ from lp.bugs.interfaces.bugtracker import IBugTracker |
94 | from lp.bugs.interfaces.bugwatch import IBugWatch |
95 | from lp.bugs.interfaces.hasbug import IHasBug |
96 | from lp.bugs.interfaces.structuralsubscription import IStructuralSubscription |
97 | +from lp.bugs.interfaces.vulnerability import IVulnerability |
98 | from lp.registry.interfaces.role import ( |
99 | IHasAppointedDriver, |
100 | IHasOwner, |
101 | @@ -384,3 +385,13 @@ class EditBugSubscriptionFilter(AuthorizationBase): |
102 | |
103 | def checkAuthenticated(self, user): |
104 | return user.inTeam(self.obj.structural_subscription.subscriber) |
105 | + |
106 | + |
107 | +class EditVulnerability(DelegatedAuthorization): |
108 | + """The security admins of a distribution should be able to edit |
109 | + vulnerabilities in that distribution.""" |
110 | + permission = 'launchpad.Edit' |
111 | + usedfor = IVulnerability |
112 | + |
113 | + def __init__(self, obj): |
114 | + super().__init__(obj, obj.distribution, 'launchpad.SecurityAdmin') |
115 | diff --git a/lib/lp/bugs/tests/test_vulnerability.py b/lib/lp/bugs/tests/test_vulnerability.py |
116 | index be3457a..7590be2 100644 |
117 | --- a/lib/lp/bugs/tests/test_vulnerability.py |
118 | +++ b/lib/lp/bugs/tests/test_vulnerability.py |
119 | @@ -16,6 +16,7 @@ from lp.bugs.interfaces.bugtask import BugTaskImportance |
120 | from lp.services.webapp.interfaces import OAuthPermission |
121 | from lp.testing import ( |
122 | api_url, |
123 | + person_logged_in, |
124 | TestCaseWithFactory, |
125 | ) |
126 | from lp.testing.layers import DatabaseFunctionalLayer |
127 | @@ -195,6 +196,68 @@ class TestVulnerabilityWebService(TestCaseWithFactory): |
128 | self.assertEqual(209, response.status) |
129 | self.assertEqual(vulnerability.cve, another_cve) |
130 | |
131 | + def test_security_admin_can_edit_vulnerability(self): |
132 | + vulnerability = removeSecurityProxy( |
133 | + self.factory.makeVulnerability() |
134 | + ) |
135 | + owner = vulnerability.distribution.owner |
136 | + person = self.factory.makePerson() |
137 | + security_team = self.factory.makeTeam(members=[person]) |
138 | + |
139 | + with person_logged_in(owner): |
140 | + vulnerability.distribution.security_admin = security_team |
141 | + |
142 | + self.assertThat( |
143 | + vulnerability, |
144 | + MatchesStructure.byEquality( |
145 | + status=VulnerabilityStatus.NEEDS_TRIAGE, |
146 | + importance=BugTaskImportance.UNDECIDED, |
147 | + information_type=InformationType.PUBLIC, |
148 | + cve=None, |
149 | + description=None, |
150 | + notes=None, |
151 | + mitigation=None, |
152 | + date_made_public=None, |
153 | + ) |
154 | + ) |
155 | + vulnerability_url = api_url(vulnerability) |
156 | + webservice = webservice_for_person( |
157 | + person, |
158 | + permission=OAuthPermission.WRITE_PRIVATE, |
159 | + default_api_version="devel" |
160 | + ) |
161 | + now = datetime.datetime.now(pytz.UTC) |
162 | + |
163 | + response = webservice.patch( |
164 | + vulnerability_url, |
165 | + "application/json", |
166 | + json.dumps( |
167 | + { |
168 | + "status": "Active", |
169 | + "information_type": "Private", |
170 | + "importance": "Critical", |
171 | + "description": "Foo bar", |
172 | + "notes": "lgp171188> Foo bar", |
173 | + "mitigation": "Foo bar baz", |
174 | + "importance_explanation": "Foo bar bazz", |
175 | + "date_made_public": now.isoformat(), |
176 | + } |
177 | + ) |
178 | + ) |
179 | + self.assertEqual(209, response.status) |
180 | + self.assertThat( |
181 | + vulnerability, |
182 | + MatchesStructure.byEquality( |
183 | + status=VulnerabilityStatus.ACTIVE, |
184 | + importance=BugTaskImportance.CRITICAL, |
185 | + description="Foo bar", |
186 | + notes="lgp171188> Foo bar", |
187 | + mitigation="Foo bar baz", |
188 | + importance_explanation="Foo bar bazz", |
189 | + date_made_public=now, |
190 | + ) |
191 | + ) |
192 | + |
193 | def test_user_without_edit_permissions_cannot_edit_vulnerability(self): |
194 | vulnerability = removeSecurityProxy( |
195 | self.factory.makeVulnerability() |
196 | diff --git a/lib/lp/permissions.zcml b/lib/lp/permissions.zcml |
197 | index 646d9ca..dd5a177 100644 |
198 | --- a/lib/lp/permissions.zcml |
199 | +++ b/lib/lp/permissions.zcml |
200 | @@ -70,6 +70,10 @@ |
201 | id="launchpad.LanguagePacksAdmin" title="Administer Language Packs." |
202 | access_level="write" /> |
203 | |
204 | + <permission |
205 | + id="launchpad.SecurityAdmin" title="Administer vulnerabilities." |
206 | + access_level="write" /> |
207 | + |
208 | <!-- XXX: GuilhermeSalgado 2005-08-25 |
209 | To be removed soon, this is only needed by the page to upload SSH |
210 | keys, cause we decided to not allow admins to upload keys in behalf of other |
211 | diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml |
212 | index 10d677f..eefe667 100644 |
213 | --- a/lib/lp/registry/configure.zcml |
214 | +++ b/lib/lp/registry/configure.zcml |
215 | @@ -1834,6 +1834,9 @@ |
216 | interface="lp.registry.interfaces.distribution.IDistributionEditRestricted" |
217 | set_schema="lp.app.interfaces.informationtype.IInformationType"/> |
218 | <require |
219 | + permission="launchpad.SecurityAdmin" |
220 | + interface="lp.registry.interfaces.distribution.IDistributionSecurityAdminRestricted" /> |
221 | + <require |
222 | permission="launchpad.Edit" |
223 | set_attributes="answers_usage blueprints_usage codehosting_usage |
224 | bug_tracking_usage uses_launchpad"/> |
225 | @@ -1866,6 +1869,7 @@ |
226 | package_derivatives_email |
227 | redirect_default_traversal |
228 | redirect_release_uploads |
229 | + security_admin |
230 | summary |
231 | vcs |
232 | "/> |
233 | diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py |
234 | index 58e10fc..727d204 100644 |
235 | --- a/lib/lp/registry/interfaces/distribution.py |
236 | +++ b/lib/lp/registry/interfaces/distribution.py |
237 | @@ -480,6 +480,11 @@ class IDistributionView( |
238 | has_current_commercial_subscription = Attribute( |
239 | "Whether the distribution has a current commercial subscription.") |
240 | |
241 | + security_admin = Choice( |
242 | + title=_("Security Administrator"), |
243 | + description=_("The distribution security administrator."), |
244 | + required=False, vocabulary='ValidPersonOrTeam') |
245 | + |
246 | vulnerabilities = exported( |
247 | doNotSnapshot(CollectionField( |
248 | description=_("Vulnerabilities in this distribution."), |
249 | @@ -889,6 +894,10 @@ class IDistributionEditRestricted(IOfficialBugTagTargetRestricted): |
250 | def deleteOCICredentials(): |
251 | """Delete any existing OCI credentials for the distribution.""" |
252 | |
253 | + |
254 | +class IDistributionSecurityAdminRestricted(Interface): |
255 | + """IDistribution properties requiring launchpad.SecurityAdmin permission""" |
256 | + |
257 | @call_with(creator=REQUEST_USER) |
258 | @operation_parameters( |
259 | status=Choice( |
260 | @@ -954,10 +963,10 @@ class IDistributionEditRestricted(IOfficialBugTagTargetRestricted): |
261 | |
262 | @exported_as_webservice_entry(as_of="beta") |
263 | class IDistribution( |
264 | - IDistributionEditRestricted, IDistributionPublic, |
265 | - IDistributionLimitedView, IDistributionView, IHasBugSupervisor, |
266 | - IFAQTarget, IQuestionTarget, IStructuralSubscriptionTarget, |
267 | - IInformationType): |
268 | + IDistributionEditRestricted, IDistributionSecurityAdminRestricted, |
269 | + IDistributionPublic,IDistributionLimitedView, IDistributionView, |
270 | + IHasBugSupervisor, IFAQTarget, IQuestionTarget, |
271 | + IStructuralSubscriptionTarget, IInformationType): |
272 | """An operating system distribution. |
273 | |
274 | Launchpadlib example: retrieving the current version of a package in a |
275 | diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py |
276 | index 97e51c0..e9903ab 100644 |
277 | --- a/lib/lp/registry/model/distribution.py |
278 | +++ b/lib/lp/registry/model/distribution.py |
279 | @@ -685,6 +685,9 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements, |
280 | language_pack_admin = ForeignKey( |
281 | dbName='language_pack_admin', foreignKey='Person', |
282 | storm_validator=validate_public_person, notNull=False, default=None) |
283 | + security_admin = ForeignKey( |
284 | + dbName='security_admin', foreignKey='Person', |
285 | + storm_validator=validate_public_person, notNull=False, default=None) |
286 | |
287 | @cachedproperty |
288 | def main_archive(self): |
289 | diff --git a/lib/lp/registry/tests/test_distribution.py b/lib/lp/registry/tests/test_distribution.py |
290 | index ee458ca..c44df04 100644 |
291 | --- a/lib/lp/registry/tests/test_distribution.py |
292 | +++ b/lib/lp/registry/tests/test_distribution.py |
293 | @@ -86,6 +86,7 @@ from lp.soyuz.interfaces.distributionsourcepackagerelease import ( |
294 | ) |
295 | from lp.testing import ( |
296 | admin_logged_in, |
297 | + anonymous_logged_in, |
298 | api_url, |
299 | celebrity_logged_in, |
300 | login, |
301 | @@ -1813,6 +1814,33 @@ class TestDistributionVulnerabilities(TestCaseWithFactory): |
302 | |
303 | layer = DatabaseFunctionalLayer |
304 | |
305 | + def assert_newVulnerability_only_the_required_params( |
306 | + self, |
307 | + distribution, |
308 | + creator |
309 | + ): |
310 | + vulnerability = distribution.newVulnerability( |
311 | + status=VulnerabilityStatus.NEEDS_TRIAGE, |
312 | + information_type=InformationType.PUBLIC, |
313 | + creator=creator, |
314 | + ) |
315 | + |
316 | + self.assertThat( |
317 | + vulnerability, |
318 | + MatchesStructure.byEquality( |
319 | + status=VulnerabilityStatus.NEEDS_TRIAGE, |
320 | + creator=creator, |
321 | + importance=BugTaskImportance.UNDECIDED, |
322 | + information_type=InformationType.PUBLIC, |
323 | + cve=None, |
324 | + description=None, |
325 | + notes=None, |
326 | + mitigation=None, |
327 | + importance_explanation=None, |
328 | + date_made_public=None |
329 | + ) |
330 | + ) |
331 | + |
332 | def test_vulnerabilities_no_vulnerability_present(self): |
333 | distribution = self.factory.makeDistribution() |
334 | self.assertEqual(0, distribution.vulnerabilities.count()) |
335 | @@ -1830,33 +1858,34 @@ class TestDistributionVulnerabilities(TestCaseWithFactory): |
336 | set(distribution.vulnerabilities), |
337 | ) |
338 | |
339 | + def test_set_security_admin_permissions(self): |
340 | + distribution = self.factory.makeDistribution() |
341 | + person = self.factory.makePerson() |
342 | + person2 = self.factory.makePerson() |
343 | + security_team = self.factory.makeTeam(members=[person]) |
344 | + |
345 | + with person_logged_in(distribution.owner): |
346 | + distribution.security_admin = security_team |
347 | + self.assertEqual(security_team, distribution.security_admin) |
348 | + |
349 | + with admin_logged_in(): |
350 | + distribution.security_admin = None |
351 | + self.assertIsNone(distribution.security_admin) |
352 | + |
353 | + with person_logged_in(person2), ExpectedException(Unauthorized): |
354 | + distribution.security_admin = security_team |
355 | + |
356 | + with anonymous_logged_in(), ExpectedException(Unauthorized): |
357 | + distribution.security_admin = security_team |
358 | + |
359 | def test_newVulnerability_default_arguments(self): |
360 | distribution = self.factory.makeDistribution() |
361 | owner = distribution.owner |
362 | |
363 | with person_logged_in(owner): |
364 | - # The distribution owner can create a new vulnerability in |
365 | - # the distribution. |
366 | - vulnerability = distribution.newVulnerability( |
367 | - status=VulnerabilityStatus.NEEDS_TRIAGE, |
368 | - information_type=InformationType.PUBLIC, |
369 | - creator=owner, |
370 | - ) |
371 | - |
372 | - self.assertThat( |
373 | - vulnerability, |
374 | - MatchesStructure.byEquality( |
375 | - status=VulnerabilityStatus.NEEDS_TRIAGE, |
376 | - creator=owner, |
377 | - importance=BugTaskImportance.UNDECIDED, |
378 | - information_type=InformationType.PUBLIC, |
379 | - cve=None, |
380 | - description=None, |
381 | - notes=None, |
382 | - mitigation=None, |
383 | - importance_explanation=None, |
384 | - date_made_public=None |
385 | - ) |
386 | + self.assert_newVulnerability_only_the_required_params( |
387 | + distribution, |
388 | + creator=owner |
389 | ) |
390 | |
391 | def test_newVulnerability_all_parameters(self): |
392 | @@ -1896,6 +1925,54 @@ class TestDistributionVulnerabilities(TestCaseWithFactory): |
393 | ) |
394 | ) |
395 | |
396 | + def test_newVulnerability_permissions(self): |
397 | + person = self.factory.makePerson() |
398 | + distribution = self.factory.makeDistribution() |
399 | + security_team = self.factory.makeTeam(members=[person]) |
400 | + |
401 | + # The distribution owner, admin, can create a new vulnerability |
402 | + # in the distribution. |
403 | + with person_logged_in(distribution.owner): |
404 | + self.assert_newVulnerability_only_the_required_params( |
405 | + distribution, |
406 | + creator=distribution.owner |
407 | + ) |
408 | + |
409 | + with admin_logged_in(): |
410 | + self.assert_newVulnerability_only_the_required_params( |
411 | + distribution, |
412 | + creator=person |
413 | + ) |
414 | + self.factory.makeCommercialSubscription(pillar=distribution) |
415 | + |
416 | + with celebrity_logged_in('commercial_admin'): |
417 | + self.assert_newVulnerability_only_the_required_params( |
418 | + distribution, |
419 | + creator=person |
420 | + ) |
421 | + |
422 | + with person_logged_in(distribution.owner): |
423 | + distribution.security_admin = security_team |
424 | + |
425 | + # When the security admin is set for the distribution, |
426 | + # users in that team can create a new vulnerability in the |
427 | + # distribution. |
428 | + with person_logged_in(person): |
429 | + self.assert_newVulnerability_only_the_required_params( |
430 | + distribution, |
431 | + creator=person |
432 | + ) |
433 | + |
434 | + def test_newVulnerability_cannot_be_called_by_unprivileged_users(self): |
435 | + person = self.factory.makePerson() |
436 | + distribution = self.factory.makeDistribution() |
437 | + with person_logged_in(person), ExpectedException(Unauthorized): |
438 | + distribution.newVulnerability( |
439 | + status=VulnerabilityStatus.NEEDS_TRIAGE, |
440 | + information_type=InformationType.PUBLIC, |
441 | + creator=person, |
442 | + ) |
443 | + |
444 | def test_getVulnerability_non_existent_id(self): |
445 | distribution = self.factory.makeDistribution() |
446 | vulnerability = distribution.getVulnerability(9999999) |
447 | @@ -2146,6 +2223,66 @@ class TestDistributionVulnerabilitiesWebService(TestCaseWithFactory): |
448 | "date_made_public": Is(None), |
449 | })) |
450 | |
451 | + def test_newVulnerability_security_admin(self): |
452 | + distribution = self.factory.makeDistribution() |
453 | + person = self.factory.makePerson() |
454 | + security_team = self.factory.makeTeam(members=[person]) |
455 | + api_base = "http://api.launchpad.test/devel" |
456 | + distribution_url = api_base + api_url(distribution) |
457 | + owner_url = api_base + api_url(person) |
458 | + |
459 | + with person_logged_in(distribution.owner): |
460 | + distribution.security_admin = security_team |
461 | + |
462 | + webservice = webservice_for_person( |
463 | + person, |
464 | + permission=OAuthPermission.WRITE_PRIVATE, |
465 | + default_api_version="devel" |
466 | + ) |
467 | + response = webservice.named_post( |
468 | + distribution_url, |
469 | + 'newVulnerability', |
470 | + status='Needs triage', |
471 | + information_type='Public', |
472 | + creator=owner_url, |
473 | + ) |
474 | + self.assertEqual(200, response.status) |
475 | + self.assertThat(response.jsonBody(), ContainsDict({ |
476 | + "distribution_link": Equals(distribution_url), |
477 | + "id": IsInstance(int), |
478 | + "cve_link": Is(None), |
479 | + "creator_link": Equals(owner_url), |
480 | + "status": Equals("Needs triage"), |
481 | + "description": Is(None), |
482 | + "notes": Is(None), |
483 | + "mitigation": Is(None), |
484 | + "importance": Equals("Undecided"), |
485 | + "importance_explanation": Is(None), |
486 | + "information_type": Equals("Public"), |
487 | + "date_made_public": Is(None), |
488 | + })) |
489 | + |
490 | + def test_newVulnerability_unauthorized_users(self): |
491 | + distribution = self.factory.makeDistribution() |
492 | + person = self.factory.makePerson() |
493 | + api_base = "http://api.launchpad.test/devel" |
494 | + distribution_url = api_base + api_url(distribution) |
495 | + owner_url = api_base + api_url(person) |
496 | + |
497 | + webservice = webservice_for_person( |
498 | + person, |
499 | + permission=OAuthPermission.WRITE_PRIVATE, |
500 | + default_api_version="devel" |
501 | + ) |
502 | + response = webservice.named_post( |
503 | + distribution_url, |
504 | + 'newVulnerability', |
505 | + status='Needs triage', |
506 | + information_type='Public', |
507 | + creator=owner_url, |
508 | + ) |
509 | + self.assertEqual(401, response.status) |
510 | + |
511 | def test_newVulnerability_all_parameters(self): |
512 | distribution = self.factory.makeDistribution() |
513 | owner = distribution.owner |
514 | diff --git a/lib/lp/security.py b/lib/lp/security.py |
515 | index 4eeacbc..2ab931d 100644 |
516 | --- a/lib/lp/security.py |
517 | +++ b/lib/lp/security.py |
518 | @@ -55,7 +55,6 @@ from lp.blueprints.model.specificationsubscription import ( |
519 | ) |
520 | from lp.bugs.interfaces.bugtarget import IOfficialBugTagTargetRestricted |
521 | from lp.bugs.interfaces.structuralsubscription import IStructuralSubscription |
522 | -from lp.bugs.interfaces.vulnerability import IVulnerability |
523 | from lp.bugs.model.bugsubscription import BugSubscription |
524 | from lp.bugs.model.bugtaskflat import BugTaskFlat |
525 | from lp.bugs.model.bugtasksearch import get_bug_privacy_filter |
526 | @@ -1293,6 +1292,21 @@ class EditDistributionByDistroOwnersOrAdmins(AuthorizationBase): |
527 | or user.in_admin) |
528 | |
529 | |
530 | +class SecurityAdminDistribution(AuthorizationBase): |
531 | + """The security admins of a distribution should be able to create |
532 | + and edit vulnerabilities in the distribution.""" |
533 | + permission = 'launchpad.SecurityAdmin' |
534 | + usedfor = IDistribution |
535 | + |
536 | + def checkAuthenticated(self, user): |
537 | + return ( |
538 | + user.isOwner(self.obj) |
539 | + or is_commercial_case(self.obj, user) |
540 | + or user.in_admin |
541 | + or user.inTeam(self.obj.security_admin) |
542 | + ) |
543 | + |
544 | + |
545 | class ModerateDistributionByDriversOrOwnersOrAdmins(AuthorizationBase): |
546 | """Distribution drivers, owners, and admins may plan releases. |
547 | |
548 | @@ -3780,14 +3794,3 @@ class EditCIBuild(AdminByBuilddAdmin): |
549 | if auth_repository.checkAuthenticated(user): |
550 | return True |
551 | return super().checkAuthenticated(user) |
552 | - |
553 | - |
554 | -class EditVulnerability(AuthorizationBase): |
555 | - permission = 'launchpad.Edit' |
556 | - usedfor = IVulnerability |
557 | - |
558 | - def checkAuthenticated(self, user): |
559 | - return (user.in_commercial_admin or user.in_admin or |
560 | - user.isOwner(self.obj.distribution) or |
561 | - user.isDriver(self.obj.distribution) or |
562 | - user.isBugSupervisor(self.obj.distribution)) |
This looks good.
I have doubts about the the permission name: `SecurityAdmin`. I think there's a convention to use a verb in the permission names. `SecurityAdmin` sounds more like a role than a permission.
Could it be `ManageVulnerab ilties` or `AdministerVuln erabilities` instead?