Merge lp:~cjwatson/launchpad/git-permissions-vocabulary into lp:launchpad

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
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.

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)