Merge lp:~cjwatson/launchpad/git-permissions-vocabulary into lp:launchpad
- git-permissions-vocabulary
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 18803 | ||||
Proposed branch: | lp:~cjwatson/launchpad/git-permissions-vocabulary | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
366 lines (+261/-18) 6 files modified
lib/lp/code/interfaces/gitrule.py (+18/-0) lib/lp/code/model/gitjob.py (+17/-8) lib/lp/code/model/gitrule.py (+3/-9) lib/lp/code/vocabularies/configure.zcml (+12/-1) lib/lp/code/vocabularies/gitrule.py (+108/-0) lib/lp/code/vocabularies/tests/test_gitrule_vocabularies.py (+103/-0) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/git-permissions-vocabulary | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+357599@code.launchpad.net |
Commit message
Add a vocabulary for typical combinations of Git access permissions.
Description of the change
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Revision history for this message
Colin Watson (cjwatson) : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/code/interfaces/gitrule.py' |
2 | --- lib/lp/code/interfaces/gitrule.py 2018-10-19 23:10:41 +0000 |
3 | +++ lib/lp/code/interfaces/gitrule.py 2018-10-22 00:40:44 +0000 |
4 | @@ -7,6 +7,7 @@ |
5 | |
6 | __metaclass__ = type |
7 | __all__ = [ |
8 | + 'describe_git_permissions', |
9 | 'IGitNascentRuleGrant', |
10 | 'IGitRule', |
11 | 'IGitRuleGrant', |
12 | @@ -218,3 +219,20 @@ |
13 | |
14 | can_force_push = copy_field( |
15 | IGitRuleGrant["can_force_push"], required=False, default=False) |
16 | + |
17 | + |
18 | +def describe_git_permissions(permissions, verbose=False): |
19 | + """Return human-readable descriptions of a set of Git access permissions. |
20 | + |
21 | + :param permissions: A collection of `GitPermissionType`. |
22 | + :return: A list of human-readable descriptions of the input permissions, |
23 | + in a conventional order. |
24 | + """ |
25 | + names = [] |
26 | + if GitPermissionType.CAN_CREATE in permissions: |
27 | + names.append("create") |
28 | + if GitPermissionType.CAN_PUSH in permissions: |
29 | + names.append("push") |
30 | + if GitPermissionType.CAN_FORCE_PUSH in permissions: |
31 | + names.append("force-push") |
32 | + return names |
33 | |
34 | === modified file 'lib/lp/code/model/gitjob.py' |
35 | --- lib/lp/code/model/gitjob.py 2018-10-16 10:04:18 +0000 |
36 | +++ lib/lp/code/model/gitjob.py 2018-10-22 00:40:44 +0000 |
37 | @@ -33,7 +33,10 @@ |
38 | from zope.security.proxy import removeSecurityProxy |
39 | |
40 | from lp.app.errors import NotFoundError |
41 | -from lp.code.enums import GitActivityType |
42 | +from lp.code.enums import ( |
43 | + GitActivityType, |
44 | + GitPermissionType, |
45 | + ) |
46 | from lp.code.interfaces.githosting import IGitHostingClient |
47 | from lp.code.interfaces.gitjob import ( |
48 | IGitJob, |
49 | @@ -44,6 +47,7 @@ |
50 | IReclaimGitRepositorySpaceJob, |
51 | IReclaimGitRepositorySpaceJobSource, |
52 | ) |
53 | +from lp.code.interfaces.gitrule import describe_git_permissions |
54 | from lp.code.mail.branch import BranchMailer |
55 | from lp.registry.interfaces.person import IPersonSet |
56 | from lp.services.config import config |
57 | @@ -319,15 +323,20 @@ |
58 | } |
59 | |
60 | |
61 | +_activity_permissions = { |
62 | + "can_create": GitPermissionType.CAN_CREATE, |
63 | + "can_push": GitPermissionType.CAN_PUSH, |
64 | + "can_force_push": GitPermissionType.CAN_FORCE_PUSH, |
65 | + } |
66 | + |
67 | + |
68 | def describe_grants(activity_value): |
69 | - output = [] |
70 | if activity_value is not None: |
71 | - if activity_value.get("can_create"): |
72 | - output.append("create") |
73 | - if activity_value.get("can_push"): |
74 | - output.append("push") |
75 | - if activity_value.get("can_force_push"): |
76 | - output.append("force-push") |
77 | + output = describe_git_permissions({ |
78 | + permission for attr, permission in _activity_permissions.items() |
79 | + if activity_value.get(attr)}) |
80 | + else: |
81 | + output = [] |
82 | return english_list(output) |
83 | |
84 | |
85 | |
86 | === modified file 'lib/lp/code/model/gitrule.py' |
87 | --- lib/lp/code/model/gitrule.py 2018-10-19 23:10:41 +0000 |
88 | +++ lib/lp/code/model/gitrule.py 2018-10-22 00:40:44 +0000 |
89 | @@ -48,6 +48,7 @@ |
90 | ) |
91 | from lp.code.interfaces.gitactivity import IGitActivitySet |
92 | from lp.code.interfaces.gitrule import ( |
93 | + describe_git_permissions, |
94 | IGitNascentRuleGrant, |
95 | IGitRule, |
96 | IGitRuleGrant, |
97 | @@ -283,20 +284,13 @@ |
98 | self.date_last_modified = date_created |
99 | |
100 | def __repr__(self): |
101 | - permissions = [] |
102 | - if self.can_create: |
103 | - permissions.append("create") |
104 | - if self.can_push: |
105 | - permissions.append("push") |
106 | - if self.can_force_push: |
107 | - permissions.append("force-push") |
108 | if self.grantee_type == GitGranteeType.PERSON: |
109 | grantee_name = "~%s" % self.grantee.name |
110 | else: |
111 | grantee_name = self.grantee_type.title.lower() |
112 | return "<GitRuleGrant [%s] to %s for %s:%s>" % ( |
113 | - ", ".join(permissions), grantee_name, self.repository.unique_name, |
114 | - self.rule.ref_pattern) |
115 | + ", ".join(describe_git_permissions(self.permissions)), |
116 | + grantee_name, self.repository.unique_name, self.rule.ref_pattern) |
117 | |
118 | @property |
119 | def permissions(self): |
120 | |
121 | === modified file 'lib/lp/code/vocabularies/configure.zcml' |
122 | --- lib/lp/code/vocabularies/configure.zcml 2015-06-24 21:14:20 +0000 |
123 | +++ lib/lp/code/vocabularies/configure.zcml 2018-10-22 00:40:44 +0000 |
124 | @@ -1,4 +1,4 @@ |
125 | -<!-- Copyright 2010-2012 Canonical Ltd. This software is licensed under the |
126 | +<!-- Copyright 2010-2018 Canonical Ltd. This software is licensed under the |
127 | GNU Affero General Public License version 3 (see the file LICENSE). |
128 | --> |
129 | |
130 | @@ -77,4 +77,15 @@ |
131 | <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary"/> |
132 | </class> |
133 | |
134 | + <securedutility |
135 | + name="GitPermissions" |
136 | + component=".gitrule.GitPermissionsVocabulary" |
137 | + provides="zope.schema.interfaces.IVocabularyFactory"> |
138 | + <allow interface="zope.schema.interfaces.IVocabularyFactory"/> |
139 | + </securedutility> |
140 | + |
141 | + <class class=".gitrule.GitPermissionsVocabulary"> |
142 | + <allow interface="zope.schema.interfaces.IVocabularyTokenized"/> |
143 | + </class> |
144 | + |
145 | </configure> |
146 | |
147 | === added file 'lib/lp/code/vocabularies/gitrule.py' |
148 | --- lib/lp/code/vocabularies/gitrule.py 1970-01-01 00:00:00 +0000 |
149 | +++ lib/lp/code/vocabularies/gitrule.py 2018-10-22 00:40:44 +0000 |
150 | @@ -0,0 +1,108 @@ |
151 | +# Copyright 2018 Canonical Ltd. This software is licensed under the |
152 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
153 | + |
154 | +"""Vocabularies related to Git access rules.""" |
155 | + |
156 | +from __future__ import absolute_import, print_function, unicode_literals |
157 | + |
158 | +__metaclass__ = type |
159 | +__all__ = [ |
160 | + 'GitPermissionsVocabulary', |
161 | + ] |
162 | + |
163 | +from zope.schema.vocabulary import ( |
164 | + SimpleTerm, |
165 | + SimpleVocabulary, |
166 | + ) |
167 | + |
168 | +from lp import _ |
169 | +from lp.code.enums import GitPermissionType |
170 | +from lp.code.interfaces.gitref import IGitRef |
171 | +from lp.code.interfaces.gitrule import ( |
172 | + describe_git_permissions, |
173 | + IGitRule, |
174 | + IGitRuleGrant, |
175 | + ) |
176 | + |
177 | + |
178 | +branch_permissions = [ |
179 | + SimpleTerm( |
180 | + frozenset(), |
181 | + "cannot_push", _("Cannot push")), |
182 | + SimpleTerm( |
183 | + frozenset({ |
184 | + GitPermissionType.CAN_PUSH, |
185 | + }), |
186 | + "can_push_existing", _("Can push if the branch already exists")), |
187 | + SimpleTerm( |
188 | + frozenset({ |
189 | + GitPermissionType.CAN_CREATE, |
190 | + GitPermissionType.CAN_PUSH, |
191 | + }), |
192 | + "can_push", _("Can push")), |
193 | + SimpleTerm( |
194 | + frozenset({ |
195 | + GitPermissionType.CAN_CREATE, |
196 | + GitPermissionType.CAN_PUSH, |
197 | + GitPermissionType.CAN_FORCE_PUSH, |
198 | + }), |
199 | + "can_force_push", _("Can force-push")), |
200 | + ] |
201 | + |
202 | + |
203 | +tag_permissions = [ |
204 | + SimpleTerm( |
205 | + frozenset(), |
206 | + "cannot_create", _("Cannot create")), |
207 | + SimpleTerm( |
208 | + frozenset({ |
209 | + GitPermissionType.CAN_CREATE, |
210 | + }), |
211 | + "can_create", _("Can create")), |
212 | + SimpleTerm( |
213 | + frozenset({ |
214 | + GitPermissionType.CAN_CREATE, |
215 | + GitPermissionType.CAN_PUSH, |
216 | + GitPermissionType.CAN_FORCE_PUSH, |
217 | + }), |
218 | + "can_move", _("Can move")), |
219 | + ] |
220 | + |
221 | + |
222 | +class GitPermissionsVocabulary(SimpleVocabulary): |
223 | + """A vocabulary for typical combinations of Git access permissions. |
224 | + |
225 | + The terms of this vocabulary are combinations of permissions that make |
226 | + sense in the UI without being too overwhelming, depending on the rule's |
227 | + reference pattern (different combinations make sense for branches vs. |
228 | + tags). |
229 | + """ |
230 | + |
231 | + def __init__(self, context): |
232 | + if IGitRef.providedBy(context): |
233 | + path = context.path |
234 | + elif IGitRule.providedBy(context): |
235 | + path = context.ref_pattern |
236 | + elif IGitRuleGrant.providedBy(context): |
237 | + path = context.rule.ref_pattern |
238 | + else: |
239 | + raise AssertionError("GitPermissionsVocabulary needs a context.") |
240 | + if path.startswith("refs/tags/"): |
241 | + terms = list(tag_permissions) |
242 | + else: |
243 | + # We could restrict this to just refs/heads/*, but it's helpful |
244 | + # to be able to offer *some* choices in the UI if somebody tries |
245 | + # to create grants for e.g. refs/*, and the choices we offer for |
246 | + # branches are probably more useful there than those we offer |
247 | + # for tags. |
248 | + terms = list(branch_permissions) |
249 | + if IGitRuleGrant.providedBy(context): |
250 | + grant_permissions = context.permissions |
251 | + if grant_permissions not in (term.value for term in terms): |
252 | + # Supplement the vocabulary with any atypical permissions |
253 | + # used by this grant. |
254 | + names = describe_git_permissions(grant_permissions) |
255 | + terms.append(SimpleTerm( |
256 | + grant_permissions, |
257 | + "custom", "Custom permissions: %s" % ", ".join(names))) |
258 | + super(GitPermissionsVocabulary, self).__init__(terms) |
259 | |
260 | === added file 'lib/lp/code/vocabularies/tests/test_gitrule_vocabularies.py' |
261 | --- lib/lp/code/vocabularies/tests/test_gitrule_vocabularies.py 1970-01-01 00:00:00 +0000 |
262 | +++ lib/lp/code/vocabularies/tests/test_gitrule_vocabularies.py 2018-10-22 00:40:44 +0000 |
263 | @@ -0,0 +1,103 @@ |
264 | +# Copyright 2018 Canonical Ltd. This software is licensed under the |
265 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
266 | + |
267 | +"""Test vocabularies related to Git access rules.""" |
268 | + |
269 | +from __future__ import absolute_import, print_function, unicode_literals |
270 | + |
271 | +__metaclass__ = type |
272 | + |
273 | +from lp.code.enums import GitPermissionType |
274 | +from lp.code.vocabularies.gitrule import GitPermissionsVocabulary |
275 | +from lp.testing import TestCaseWithFactory |
276 | +from lp.testing.layers import DatabaseFunctionalLayer |
277 | + |
278 | + |
279 | +class TestGitPermissionsVocabulary(TestCaseWithFactory): |
280 | + |
281 | + layer = DatabaseFunctionalLayer |
282 | + |
283 | + expected_branch_values = [ |
284 | + set(), |
285 | + {GitPermissionType.CAN_PUSH}, |
286 | + {GitPermissionType.CAN_CREATE, GitPermissionType.CAN_PUSH}, |
287 | + {GitPermissionType.CAN_CREATE, GitPermissionType.CAN_PUSH, |
288 | + GitPermissionType.CAN_FORCE_PUSH}, |
289 | + ] |
290 | + expected_branch_tokens = [ |
291 | + "cannot_push", "can_push_existing", "can_push", "can_force_push", |
292 | + ] |
293 | + expected_tag_values = [ |
294 | + set(), |
295 | + {GitPermissionType.CAN_CREATE}, |
296 | + {GitPermissionType.CAN_CREATE, GitPermissionType.CAN_PUSH, |
297 | + GitPermissionType.CAN_FORCE_PUSH}, |
298 | + ] |
299 | + expected_tag_tokens = ["cannot_create", "can_create", "can_move"] |
300 | + |
301 | + def assertVocabularyHasTerms(self, context, expected_values, |
302 | + expected_tokens): |
303 | + vocabulary = GitPermissionsVocabulary(context) |
304 | + terms = list(vocabulary) |
305 | + self.assertEqual(expected_values, [term.value for term in terms]) |
306 | + self.assertEqual(expected_tokens, [term.token for term in terms]) |
307 | + return terms |
308 | + |
309 | + def test_ref_branch(self): |
310 | + [ref] = self.factory.makeGitRefs(paths=["refs/heads/master"]) |
311 | + self.assertVocabularyHasTerms( |
312 | + ref, self.expected_branch_values, self.expected_branch_tokens) |
313 | + |
314 | + def test_ref_tag(self): |
315 | + [ref] = self.factory.makeGitRefs(paths=["refs/tags/1.0"]) |
316 | + self.assertVocabularyHasTerms( |
317 | + ref, self.expected_tag_values, self.expected_tag_tokens) |
318 | + |
319 | + def test_ref_other(self): |
320 | + [ref] = self.factory.makeGitRefs(paths=["refs/other"]) |
321 | + self.assertVocabularyHasTerms( |
322 | + ref, self.expected_branch_values, self.expected_branch_tokens) |
323 | + |
324 | + def test_rule_branch(self): |
325 | + rule = self.factory.makeGitRule(ref_pattern="refs/heads/*") |
326 | + self.assertVocabularyHasTerms( |
327 | + rule, self.expected_branch_values, self.expected_branch_tokens) |
328 | + |
329 | + def test_rule_tag(self): |
330 | + rule = self.factory.makeGitRule(ref_pattern="refs/tags/*") |
331 | + self.assertVocabularyHasTerms( |
332 | + rule, self.expected_tag_values, self.expected_tag_tokens) |
333 | + |
334 | + def test_rule_other(self): |
335 | + rule = self.factory.makeGitRule(ref_pattern="refs/*") |
336 | + self.assertVocabularyHasTerms( |
337 | + rule, self.expected_branch_values, self.expected_branch_tokens) |
338 | + |
339 | + def test_rule_grant_branch(self): |
340 | + grant = self.factory.makeGitRuleGrant( |
341 | + ref_pattern="refs/heads/*", can_create=True, can_push=True) |
342 | + self.assertVocabularyHasTerms( |
343 | + grant, self.expected_branch_values, self.expected_branch_tokens) |
344 | + |
345 | + def test_rule_grant_branch_with_custom(self): |
346 | + grant = self.factory.makeGitRuleGrant( |
347 | + ref_pattern="refs/heads/*", can_push=True, can_force_push=True) |
348 | + expected_values = ( |
349 | + self.expected_branch_values + |
350 | + [{GitPermissionType.CAN_PUSH, GitPermissionType.CAN_FORCE_PUSH}]) |
351 | + expected_tokens = self.expected_branch_tokens + ["custom"] |
352 | + terms = self.assertVocabularyHasTerms( |
353 | + grant, expected_values, expected_tokens) |
354 | + self.assertEqual( |
355 | + "Custom permissions: push, force-push", terms[-1].title) |
356 | + |
357 | + def test_rule_grant_tag(self): |
358 | + grant = self.factory.makeGitRuleGrant( |
359 | + ref_pattern="refs/tags/*", can_create=True) |
360 | + self.assertVocabularyHasTerms( |
361 | + grant, self.expected_tag_values, self.expected_tag_tokens) |
362 | + |
363 | + def test_rule_grant_other(self): |
364 | + grant = self.factory.makeGitRuleGrant(ref_pattern="refs/*") |
365 | + self.assertVocabularyHasTerms( |
366 | + grant, self.expected_branch_values, self.expected_branch_tokens) |