Merge lp:~twom/launchpad/branch-permissions-for-gitapi into lp:launchpad
- branch-permissions-for-gitapi
- Merge into devel
Proposed by
Tom Wardill
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 18795 | ||||
Proposed branch: | lp:~twom/launchpad/branch-permissions-for-gitapi | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/git-permissions-model | ||||
Diff against target: |
909 lines (+824/-0) 6 files modified
lib/lp/code/interfaces/gitapi.py (+6/-0) lib/lp/code/interfaces/gitrepository.py (+10/-0) lib/lp/code/model/gitrepository.py (+15/-0) lib/lp/code/model/tests/test_gitrepository.py (+125/-0) lib/lp/code/xmlrpc/git.py (+91/-0) lib/lp/code/xmlrpc/tests/test_git.py (+577/-0) |
||||
To merge this branch: | bzr merge lp:~twom/launchpad/branch-permissions-for-gitapi | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+355716@code.launchpad.net |
This proposal supersedes a proposal from 2018-09-26.
Commit message
Add GitRuleGrant api to xmlrpc API
Description of the change
To post a comment you must log in.
Revision history for this message
Tom Wardill (twom) wrote : | # |
Revision history for this message
Colin Watson (cjwatson) wrote : | # |
A quick first-pass review to get you started ...
review:
Needs Fixing
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Needs Fixing
Revision history for this message
Colin Watson (cjwatson) wrote : | # |
Thanks, this is looking better now. Just a few more bits and pieces.
A quick pass over the whole diff to conform to the prevailing style would be good (e.g. public methods of model objects get a """See `IFoo`.""" docstring, no blank line at the start of methods, multi-line displays generally have a newline after the opening punctuation and then a four-space indent for their contents rather than the contents being indented to the opening punctuation, etc.).
review:
Approve
Revision history for this message
Colin Watson (cjwatson) : | # |
Revision history for this message
Tom Wardill (twom) : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/code/interfaces/gitapi.py' |
2 | --- lib/lp/code/interfaces/gitapi.py 2015-03-31 04:18:22 +0000 |
3 | +++ lib/lp/code/interfaces/gitapi.py 2018-10-15 14:44:56 +0000 |
4 | @@ -67,3 +67,9 @@ |
5 | :returns: An `Unauthorized` fault, as password authentication is |
6 | not yet supported. |
7 | """ |
8 | + |
9 | + def listRefRules(self, repository, user): |
10 | + """Return the list of ref rules for `user` in `repository` |
11 | + |
12 | + :returns: A list of rules for the user in the specified repository |
13 | + """ |
14 | |
15 | === modified file 'lib/lp/code/interfaces/gitrepository.py' |
16 | --- lib/lp/code/interfaces/gitrepository.py 2018-10-12 16:41:14 +0000 |
17 | +++ lib/lp/code/interfaces/gitrepository.py 2018-10-15 14:44:56 +0000 |
18 | @@ -746,6 +746,16 @@ |
19 | :param user: The `IPerson` who is moving the rule. |
20 | """ |
21 | |
22 | + def findRuleGrantsByGrantee(grantee): |
23 | + """Find the grants for a grantee applied to this repository. |
24 | + |
25 | + :param grantee: The Person affected |
26 | + """ |
27 | + |
28 | + def findRuleGrantsForRepositoryOwner(): |
29 | + """Find the grants of type REPOSITORY_OWNER applied to this repository. |
30 | + """ |
31 | + |
32 | @export_read_operation() |
33 | @operation_for_version("devel") |
34 | def canBeDeleted(): |
35 | |
36 | === modified file 'lib/lp/code/model/gitrepository.py' |
37 | --- lib/lp/code/model/gitrepository.py 2018-10-12 16:41:14 +0000 |
38 | +++ lib/lp/code/model/gitrepository.py 2018-10-15 14:44:56 +0000 |
39 | @@ -72,6 +72,7 @@ |
40 | from lp.app.interfaces.services import IService |
41 | from lp.code.enums import ( |
42 | BranchMergeProposalStatus, |
43 | + GitGranteeType, |
44 | GitObjectType, |
45 | GitRepositoryType, |
46 | ) |
47 | @@ -1193,6 +1194,20 @@ |
48 | return Store.of(self).find( |
49 | GitRuleGrant, GitRuleGrant.repository_id == self.id) |
50 | |
51 | + def findRuleGrantsByGrantee(self, grantee): |
52 | + """See `IGitRepository`.""" |
53 | + clauses = [ |
54 | + GitRuleGrant.grantee_type == GitGranteeType.PERSON, |
55 | + TeamParticipation.person == grantee, |
56 | + GitRuleGrant.grantee == TeamParticipation.teamID |
57 | + ] |
58 | + return self.grants.find(*clauses).config(distinct=True) |
59 | + |
60 | + def findRuleGrantsForRepositoryOwner(self): |
61 | + """See `IGitRepository`.""" |
62 | + return self.grants.find( |
63 | + GitRuleGrant.grantee_type == GitGranteeType.REPOSITORY_OWNER) |
64 | + |
65 | def getActivity(self): |
66 | """See `IGitRepository`.""" |
67 | clauses = [GitActivity.repository_id == self.id] |
68 | |
69 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' |
70 | --- lib/lp/code/model/tests/test_gitrepository.py 2018-10-12 16:41:14 +0000 |
71 | +++ lib/lp/code/model/tests/test_gitrepository.py 2018-10-15 14:44:56 +0000 |
72 | @@ -47,6 +47,7 @@ |
73 | BranchSubscriptionDiffSize, |
74 | BranchSubscriptionNotificationLevel, |
75 | CodeReviewNotificationLevel, |
76 | + GitGranteeType, |
77 | GitObjectType, |
78 | GitRepositoryType, |
79 | TargetRevisionControlSystems, |
80 | @@ -224,6 +225,130 @@ |
81 | bmp = self.factory.makeBranchMergeProposalForGit(target_ref=ref) |
82 | self.assertEqual([bmp], list(repository.getMergeProposals())) |
83 | |
84 | + def test_findRuleGrantsByGrantee_person(self): |
85 | + requester = self.factory.makePerson() |
86 | + repository = removeSecurityProxy( |
87 | + self.factory.makeGitRepository(owner=requester)) |
88 | + |
89 | + rule = self.factory.makeGitRule(repository) |
90 | + grant = self.factory.makeGitRuleGrant( |
91 | + rule=rule, grantee=requester, can_push=True, can_create=True) |
92 | + |
93 | + results = repository.findRuleGrantsByGrantee(requester) |
94 | + self.assertEqual([grant], list(results)) |
95 | + |
96 | + def test_findRuleGrantsByGrantee_team(self): |
97 | + requester = self.factory.makeTeam() |
98 | + repository = removeSecurityProxy( |
99 | + self.factory.makeGitRepository(owner=requester)) |
100 | + |
101 | + rule = self.factory.makeGitRule(repository) |
102 | + grant = self.factory.makeGitRuleGrant( |
103 | + rule=rule, grantee=requester, can_push=True, can_create=True) |
104 | + |
105 | + results = repository.findRuleGrantsByGrantee(requester) |
106 | + self.assertEqual([grant], list(results)) |
107 | + |
108 | + def test_findRuleGrantsByGrantee_member_of_team(self): |
109 | + member = self.factory.makePerson() |
110 | + requester = self.factory.makeTeam(members=[member]) |
111 | + repository = removeSecurityProxy( |
112 | + self.factory.makeGitRepository(owner=requester)) |
113 | + |
114 | + rule = self.factory.makeGitRule(repository) |
115 | + grant = self.factory.makeGitRuleGrant( |
116 | + rule=rule, grantee=requester, can_push=True, can_create=True) |
117 | + |
118 | + results = repository.findRuleGrantsByGrantee(requester) |
119 | + self.assertEqual([grant], list(results)) |
120 | + |
121 | + def test_findRuleGrantsByGrantee_team_in_team(self): |
122 | + member = self.factory.makePerson() |
123 | + team = self.factory.makeTeam(owner=member, members=[member]) |
124 | + top_level = removeSecurityProxy(self.factory.makeTeam()) |
125 | + top_level.addMember(team, top_level.teamowner, force_team_add=True) |
126 | + |
127 | + repository = removeSecurityProxy( |
128 | + self.factory.makeGitRepository(owner=top_level)) |
129 | + |
130 | + rule = self.factory.makeGitRule(repository) |
131 | + grant = self.factory.makeGitRuleGrant( |
132 | + rule=rule, grantee=top_level, can_push=True, can_create=True) |
133 | + |
134 | + results = repository.findRuleGrantsByGrantee(member) |
135 | + self.assertEqual([grant], list(results)) |
136 | + |
137 | + def test_findRuleGrantsByGrantee_team_in_team_not_owner(self): |
138 | + member = self.factory.makePerson() |
139 | + team = self.factory.makeTeam(owner=member, members=[member]) |
140 | + top_level = removeSecurityProxy(self.factory.makeTeam()) |
141 | + top_level.addMember(team, top_level.teamowner, force_team_add=True) |
142 | + |
143 | + repository = removeSecurityProxy( |
144 | + self.factory.makeGitRepository()) |
145 | + |
146 | + rule = self.factory.makeGitRule(repository) |
147 | + grant = self.factory.makeGitRuleGrant( |
148 | + rule=rule, grantee=top_level, can_push=True, can_create=True) |
149 | + |
150 | + results = repository.findRuleGrantsByGrantee(member) |
151 | + self.assertEqual([grant], list(results)) |
152 | + |
153 | + def test_findRuleGrantsByGrantee_not_owner(self): |
154 | + requester = self.factory.makePerson() |
155 | + repository = removeSecurityProxy( |
156 | + self.factory.makeGitRepository()) |
157 | + |
158 | + rule = self.factory.makeGitRule(repository) |
159 | + grant = self.factory.makeGitRuleGrant( |
160 | + rule=rule, grantee=requester, can_push=True, can_create=True) |
161 | + |
162 | + results = repository.findRuleGrantsByGrantee(requester) |
163 | + self.assertEqual([grant], list(results)) |
164 | + |
165 | + def test_findRuleGrantsByGrantee_grantee_type(self): |
166 | + requester = self.factory.makePerson() |
167 | + repository = removeSecurityProxy( |
168 | + self.factory.makeGitRepository(owner=requester)) |
169 | + |
170 | + rule = self.factory.makeGitRule(repository) |
171 | + grant = self.factory.makeGitRuleGrant( |
172 | + rule=rule, |
173 | + grantee=GitGranteeType.REPOSITORY_OWNER, |
174 | + can_push=True, |
175 | + can_create=True) |
176 | + |
177 | + results = repository.findRuleGrantsForRepositoryOwner() |
178 | + self.assertEqual([grant], list(results)) |
179 | + |
180 | + def test_findRuleGrantsByGrantee_owner_and_other(self): |
181 | + requester = self.factory.makePerson() |
182 | + other = self.factory.makePerson() |
183 | + repository = removeSecurityProxy( |
184 | + self.factory.makeGitRepository(owner=requester)) |
185 | + |
186 | + rule = self.factory.makeGitRule(repository) |
187 | + self.factory.makeGitRuleGrant( |
188 | + rule=rule, grantee=other, can_push=True, can_create=True) |
189 | + |
190 | + results = repository.findRuleGrantsByGrantee(requester) |
191 | + self.assertEqual([], list(results)) |
192 | + |
193 | + def test_findRuleGrantsByGrantee_owner_and_other_with_owner_grant(self): |
194 | + requester = self.factory.makePerson() |
195 | + other = self.factory.makePerson() |
196 | + repository = removeSecurityProxy( |
197 | + self.factory.makeGitRepository(owner=requester)) |
198 | + |
199 | + rule = self.factory.makeGitRule(repository) |
200 | + self.factory.makeGitRuleGrant( |
201 | + rule=rule, grantee=other, can_push=True, can_create=True) |
202 | + owner_grant = self.factory.makeGitRuleGrant( |
203 | + rule=rule, grantee=requester, can_push=True) |
204 | + |
205 | + results = repository.findRuleGrantsByGrantee(requester) |
206 | + self.assertEqual([owner_grant], list(results)) |
207 | + |
208 | |
209 | class TestGitIdentityMixin(TestCaseWithFactory): |
210 | """Test the defaults and identities provided by GitIdentityMixin.""" |
211 | |
212 | === modified file 'lib/lp/code/xmlrpc/git.py' |
213 | --- lib/lp/code/xmlrpc/git.py 2018-08-28 13:58:37 +0000 |
214 | +++ lib/lp/code/xmlrpc/git.py 2018-10-15 14:44:56 +0000 |
215 | @@ -325,3 +325,94 @@ |
216 | else: |
217 | # Only macaroons are supported for password authentication. |
218 | return faults.Unauthorized() |
219 | + |
220 | + def _sortPermissions(self, set_of_permissions): |
221 | + """Sort an iterable of permission strings for consistency""" |
222 | + permissions = [] |
223 | + if 'create' in set_of_permissions: |
224 | + permissions.append('create') |
225 | + if 'push' in set_of_permissions: |
226 | + permissions.append('push') |
227 | + if 'force_push' in set_of_permissions: |
228 | + permissions.append('force_push') |
229 | + return permissions |
230 | + |
231 | + def _buildPermissions(self, grant): |
232 | + """Build a set of the available permissions from a GitRuleGrant""" |
233 | + permissions = set() |
234 | + if grant.can_create: |
235 | + permissions.add('create') |
236 | + if grant.can_push: |
237 | + permissions.add('push') |
238 | + if grant.can_force_push: |
239 | + # can_force_push implies can_push. |
240 | + permissions.add('push') |
241 | + permissions.add('force_push') |
242 | + return permissions |
243 | + |
244 | + def _listRefRules(self, requester, translated_path): |
245 | + repository = removeSecurityProxy( |
246 | + getUtility(IGitLookup).getByHostingPath(translated_path)) |
247 | + is_owner = requester.inTeam(repository.owner) |
248 | + grants = repository.findRuleGrantsByGrantee(requester) |
249 | + # If the user is the owner, get the grants for REPOSITORY_OWNER |
250 | + # and add them to our available grants to match against |
251 | + if is_owner: |
252 | + owner_grants = repository.findRuleGrantsForRepositoryOwner() |
253 | + grants = grants.union(owner_grants) |
254 | + result = [] |
255 | + |
256 | + for rule in repository.rules: |
257 | + # Do we have any grants for this rule, for this user? |
258 | + matching_grants = [x for x in grants if x.rule == rule] |
259 | + # If we don't have any grants, but the user is the owner, |
260 | + # they get a default grant to the ref specified by the rule. |
261 | + if is_owner and not matching_grants: |
262 | + result.append( |
263 | + {'ref_pattern': rule.ref_pattern, |
264 | + 'permissions': ['create', 'push'], |
265 | + }) |
266 | + continue |
267 | + # If we otherwise don't have any matching grants, |
268 | + # we can ignore this rule |
269 | + if not matching_grants: |
270 | + continue |
271 | + |
272 | + # Permissions are a union of all the applicable grants |
273 | + union_permissions = set() |
274 | + for grant in matching_grants: |
275 | + permissions = self._buildPermissions(grant) |
276 | + union_permissions.update(permissions) |
277 | + |
278 | + # If the user is the repository owner, they essentially have |
279 | + # the equivalent of a default team grant, but only |
280 | + # if there is no explicit grant to them otherwise specified |
281 | + if is_owner and not any(g for g in owner_grants if g.rule == rule): |
282 | + union_permissions.update(['create', 'push']) |
283 | + |
284 | + # Sort the permissions from the set for consistency |
285 | + sorted_permissions = self._sortPermissions(union_permissions) |
286 | + result.append( |
287 | + {'ref_pattern': rule.ref_pattern, |
288 | + 'permissions': sorted_permissions, |
289 | + }) |
290 | + |
291 | + # The last rule for a repository owner is a default permission |
292 | + # for everything. This is overridden by any matching rules previously |
293 | + # in the list |
294 | + if is_owner: |
295 | + result.append( |
296 | + {'ref_pattern': '*', |
297 | + 'permissions': ['create', 'push', 'force_push'], |
298 | + }) |
299 | + |
300 | + return result |
301 | + |
302 | + def listRefRules(self, translated_path, auth_params): |
303 | + """See `IGitAPI`""" |
304 | + requester_id = auth_params.get("uid") |
305 | + return run_with_login( |
306 | + requester_id, |
307 | + self._listRefRules, |
308 | + translated_path, |
309 | + ) |
310 | |
311 | === modified file 'lib/lp/code/xmlrpc/tests/test_git.py' |
312 | --- lib/lp/code/xmlrpc/tests/test_git.py 2018-08-28 14:07:38 +0000 |
313 | +++ lib/lp/code/xmlrpc/tests/test_git.py 2018-10-15 14:44:56 +0000 |
314 | @@ -6,11 +6,17 @@ |
315 | __metaclass__ = type |
316 | |
317 | from pymacaroons import Macaroon |
318 | +from testtools.matchers import ( |
319 | + Equals, |
320 | + MatchesListwise, |
321 | + MatchesDict, |
322 | + ) |
323 | from zope.component import getUtility |
324 | from zope.security.proxy import removeSecurityProxy |
325 | |
326 | from lp.app.enums import InformationType |
327 | from lp.code.enums import ( |
328 | + GitGranteeType, |
329 | GitRepositoryType, |
330 | TargetRevisionControlSystems, |
331 | ) |
332 | @@ -260,6 +266,577 @@ |
333 | self.assertEqual( |
334 | initial_count, getUtility(IAllGitRepositories).count()) |
335 | |
336 | + def test_listRefRules_simple(self): |
337 | + # Test that correct ref rules are retrieved for a Person |
338 | + requester = self.factory.makePerson() |
339 | + repository = removeSecurityProxy( |
340 | + self.factory.makeGitRepository()) |
341 | + |
342 | + rule = self.factory.makeGitRule(repository) |
343 | + self.factory.makeGitRuleGrant( |
344 | + rule=rule, grantee=requester, can_push=True, can_create=True) |
345 | + |
346 | + results = self.git_api.listRefRules( |
347 | + repository.getInternalPath(), |
348 | + {'uid': requester.id}) |
349 | + self.assertThat(results, MatchesListwise([ |
350 | + MatchesDict({ |
351 | + 'ref_pattern': Equals('refs/heads/*'), |
352 | + 'permissions': Equals(['create', 'push']), |
353 | + }), |
354 | + ])) |
355 | + |
356 | + def test_listRefRules_with_other_grants(self): |
357 | + # Test that findRuleGrantsByGrantee only returns relevant rules |
358 | + requester = self.factory.makePerson() |
359 | + other_user = self.factory.makePerson() |
360 | + repository = removeSecurityProxy( |
361 | + self.factory.makeGitRepository()) |
362 | + |
363 | + rule = self.factory.makeGitRule(repository) |
364 | + self.factory.makeGitRuleGrant( |
365 | + rule=rule, grantee=requester, can_push=True) |
366 | + self.factory.makeGitRuleGrant( |
367 | + rule=rule, grantee=other_user, can_create=True) |
368 | + |
369 | + results = self.git_api.listRefRules( |
370 | + repository.getInternalPath(), |
371 | + {'uid': requester.id}) |
372 | + self.assertThat(results, MatchesListwise([ |
373 | + MatchesDict({ |
374 | + 'ref_pattern': Equals('refs/heads/*'), |
375 | + 'permissions': Equals(['push']), |
376 | + }) |
377 | + ])) |
378 | + |
379 | + def test_listRefRules_owner_has_default(self): |
380 | + owner = self.factory.makePerson() |
381 | + repository = removeSecurityProxy( |
382 | + self.factory.makeGitRepository(owner=owner)) |
383 | + |
384 | + results = self.git_api.listRefRules( |
385 | + repository.getInternalPath(), |
386 | + {'uid': owner.id}) |
387 | + |
388 | + self.assertThat(results, MatchesListwise([ |
389 | + MatchesDict({ |
390 | + 'ref_pattern': Equals('*'), |
391 | + 'permissions': Equals(['create', 'push', 'force_push']), |
392 | + }), |
393 | + ])) |
394 | + |
395 | + def test_listRefRules_owner_modifies_rules(self): |
396 | + owner = self.factory.makePerson() |
397 | + person = self.factory.makePerson() |
398 | + repository = removeSecurityProxy( |
399 | + self.factory.makeGitRepository(owner=owner)) |
400 | + |
401 | + rule = self.factory.makeGitRule( |
402 | + repository, ref_pattern=u'refs/heads/stable/*') |
403 | + self.factory.makeGitRuleGrant( |
404 | + rule=rule, grantee=person, can_push=True) |
405 | + |
406 | + results = self.git_api.listRefRules( |
407 | + repository.getInternalPath(), |
408 | + {'uid': owner.id}) |
409 | + |
410 | + self.assertThat(results, MatchesListwise([ |
411 | + MatchesDict({ |
412 | + 'ref_pattern': Equals('refs/heads/stable/*'), |
413 | + 'permissions': Equals(['create', 'push']), |
414 | + }), |
415 | + MatchesDict({ |
416 | + 'ref_pattern': Equals('*'), |
417 | + 'permissions': Equals(['create', 'push', 'force_push']), |
418 | + }), |
419 | + ])) |
420 | + |
421 | + def test_listRefRules_owner_no_default_with_explicit(self): |
422 | + owner = self.factory.makePerson() |
423 | + repository = removeSecurityProxy( |
424 | + self.factory.makeGitRepository(owner=owner)) |
425 | + |
426 | + rule = self.factory.makeGitRule( |
427 | + repository, ref_pattern=u'refs/heads/stable/*') |
428 | + self.factory.makeGitRuleGrant( |
429 | + rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER, can_push=True) |
430 | + |
431 | + results = self.git_api.listRefRules( |
432 | + repository.getInternalPath(), |
433 | + {'uid': owner.id}) |
434 | + |
435 | + self.assertThat(results, MatchesListwise([ |
436 | + MatchesDict({ |
437 | + 'ref_pattern': Equals('refs/heads/stable/*'), |
438 | + 'permissions': Equals(['push']), |
439 | + }), |
440 | + MatchesDict({ |
441 | + 'ref_pattern': Equals('*'), |
442 | + 'permissions': Equals(['create', 'push', 'force_push']), |
443 | + }), |
444 | + ])) |
445 | + |
446 | + def test_listRefRules_owner_modifies_rules_multiple_grants(self): |
447 | + owner = self.factory.makePerson() |
448 | + person = self.factory.makePerson() |
449 | + repository = removeSecurityProxy( |
450 | + self.factory.makeGitRepository(owner=owner)) |
451 | + |
452 | + rule = self.factory.makeGitRule( |
453 | + repository, ref_pattern=u'refs/heads/stable/*') |
454 | + self.factory.makeGitRuleGrant( |
455 | + rule=rule, grantee=person, can_push=True, can_create=True) |
456 | + |
457 | + self.factory.makeGitRuleGrant( |
458 | + rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER, can_push=True) |
459 | + |
460 | + results = self.git_api.listRefRules( |
461 | + repository.getInternalPath(), |
462 | + {'uid': owner.id}) |
463 | + |
464 | + self.assertThat(results, MatchesListwise([ |
465 | + MatchesDict({ |
466 | + 'ref_pattern': Equals('refs/heads/stable/*'), |
467 | + 'permissions': Equals(['push']), |
468 | + }), |
469 | + MatchesDict({ |
470 | + 'ref_pattern': Equals('*'), |
471 | + 'permissions': Equals(['create', 'push', 'force_push']), |
472 | + }), |
473 | + ])) |
474 | + |
475 | + def test_listRefRules_no_grants(self): |
476 | + # User that has no grants and is not the owner |
477 | + requester = self.factory.makePerson() |
478 | + owner = self.factory.makePerson() |
479 | + repository = removeSecurityProxy( |
480 | + self.factory.makeGitRepository(owner=owner)) |
481 | + |
482 | + rule = self.factory.makeGitRule(repository) |
483 | + self.factory.makeGitRuleGrant( |
484 | + rule=rule, grantee=owner, can_push=True, can_create=True) |
485 | + |
486 | + results = self.git_api.listRefRules( |
487 | + repository.getInternalPath(), |
488 | + {'uid': requester.id}) |
489 | + self.assertEqual(0, len(results)) |
490 | + |
491 | + def test_listRefRules_owner_has_default_with_other_grant(self): |
492 | + owner = self.factory.makePerson() |
493 | + repository = removeSecurityProxy( |
494 | + self.factory.makeGitRepository(owner=owner)) |
495 | + |
496 | + rule = self.factory.makeGitRule( |
497 | + repository=repository, ref_pattern=u'refs/heads/master') |
498 | + self.factory.makeGitRuleGrant( |
499 | + rule=rule, grantee=owner, can_push=True, can_create=True) |
500 | + |
501 | + results = self.git_api.listRefRules( |
502 | + repository.getInternalPath(), |
503 | + {'uid': owner.id}) |
504 | + self.assertEqual(len(results), 2) |
505 | + # Default grant should be last in pattern |
506 | + self.assertThat(results, MatchesListwise([ |
507 | + MatchesDict({ |
508 | + 'ref_pattern': Equals('refs/heads/master'), |
509 | + 'permissions': Equals(['create', 'push']), |
510 | + }), |
511 | + MatchesDict({ |
512 | + 'ref_pattern': Equals('*'), |
513 | + 'permissions': Equals(['create', 'push', 'force_push']), |
514 | + }), |
515 | + ])) |
516 | + |
517 | + def test_listRefRules_owner_is_team(self): |
518 | + member = self.factory.makePerson() |
519 | + owner = self.factory.makeTeam(members=[member]) |
520 | + repository = removeSecurityProxy( |
521 | + self.factory.makeGitRepository( |
522 | + owner=owner, information_type=InformationType.USERDATA)) |
523 | + |
524 | + results = self.git_api.listRefRules( |
525 | + repository.getInternalPath(), |
526 | + {'uid': member.id}) |
527 | + |
528 | + # Should have default grant as member of owning team |
529 | + self.assertThat(results, MatchesListwise([ |
530 | + MatchesDict({ |
531 | + 'ref_pattern': Equals('*'), |
532 | + 'permissions': Equals(['create', 'push', 'force_push']), |
533 | + }), |
534 | + ])) |
535 | + |
536 | + def test_listRefRules_owner_is_team_with_grants(self): |
537 | + member = self.factory.makePerson() |
538 | + owner = self.factory.makeTeam(members=[member]) |
539 | + repository = removeSecurityProxy( |
540 | + self.factory.makeGitRepository( |
541 | + owner=owner, information_type=InformationType.USERDATA)) |
542 | + |
543 | + rule = self.factory.makeGitRule( |
544 | + repository=repository, ref_pattern=u'refs/heads/master') |
545 | + self.factory.makeGitRuleGrant( |
546 | + rule=rule, grantee=owner, can_push=True, can_create=True) |
547 | + |
548 | + results = self.git_api.listRefRules( |
549 | + repository.getInternalPath(), |
550 | + {'uid': member.id}) |
551 | + |
552 | + # Should have default grant as member of owning team |
553 | + self.assertThat(results, MatchesListwise([ |
554 | + MatchesDict({ |
555 | + 'ref_pattern': Equals('refs/heads/master'), |
556 | + 'permissions': Equals(['create', 'push']), |
557 | + }), |
558 | + MatchesDict({ |
559 | + 'ref_pattern': Equals('*'), |
560 | + 'permissions': Equals(['create', 'push', 'force_push']), |
561 | + }), |
562 | + ])) |
563 | + |
564 | + def test_listRefRules_owner_is_team_with_grants_to_person(self): |
565 | + member = self.factory.makePerson() |
566 | + other_member = self.factory.makePerson() |
567 | + owner = self.factory.makeTeam(members=[member, other_member]) |
568 | + repository = removeSecurityProxy( |
569 | + self.factory.makeGitRepository( |
570 | + owner=owner, information_type=InformationType.USERDATA)) |
571 | + |
572 | + rule = self.factory.makeGitRule( |
573 | + repository=repository, ref_pattern=u'refs/heads/master') |
574 | + self.factory.makeGitRuleGrant( |
575 | + rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER, |
576 | + can_push=True, can_create=True) |
577 | + |
578 | + rule = self.factory.makeGitRule( |
579 | + repository=repository, ref_pattern=u'refs/heads/tags') |
580 | + self.factory.makeGitRuleGrant( |
581 | + rule=rule, grantee=member, can_create=True) |
582 | + |
583 | + # This should not appear |
584 | + self.factory.makeGitRuleGrant( |
585 | + rule=rule, grantee=other_member, can_push=True) |
586 | + |
587 | + results = self.git_api.listRefRules( |
588 | + repository.getInternalPath(), |
589 | + {'uid': member.id}) |
590 | + |
591 | + # Should have default grant as member of owning team |
592 | + self.assertThat(results, MatchesListwise([ |
593 | + MatchesDict({ |
594 | + 'ref_pattern': Equals('refs/heads/master'), |
595 | + 'permissions': Equals(['create', 'push']), |
596 | + }), |
597 | + MatchesDict({ |
598 | + 'ref_pattern': Equals('refs/heads/tags'), |
599 | + 'permissions': Equals(['create', 'push']), |
600 | + }), |
601 | + MatchesDict({ |
602 | + 'ref_pattern': Equals('*'), |
603 | + 'permissions': Equals(['create', 'push', 'force_push']), |
604 | + }), |
605 | + ])) |
606 | + |
607 | + def test_listRefRules_multiple_grants_to_same_ref_with_owner(self): |
608 | + member = self.factory.makePerson() |
609 | + owner = self.factory.makeTeam(members=[member]) |
610 | + repository = removeSecurityProxy( |
611 | + self.factory.makeGitRepository(owner=owner)) |
612 | + |
613 | + rule = self.factory.makeGitRule(repository=repository) |
614 | + self.factory.makeGitRuleGrant( |
615 | + rule=rule, grantee=member, can_create=True) |
616 | + self.factory.makeGitRuleGrant( |
617 | + rule=rule, grantee=owner, can_push=True) |
618 | + |
619 | + results = self.git_api.listRefRules( |
620 | + repository.getInternalPath(), |
621 | + {'uid': member.id}) |
622 | + |
623 | + self.assertThat(results, MatchesListwise([ |
624 | + MatchesDict({ |
625 | + 'ref_pattern': Equals('refs/heads/*'), |
626 | + 'permissions': Equals(['create', 'push']), |
627 | + }), |
628 | + MatchesDict({ |
629 | + 'ref_pattern': Equals('*'), |
630 | + 'permissions': Equals(['create', 'push', 'force_push']), |
631 | + }), |
632 | + ])) |
633 | + |
634 | + def test_listRefRules_multiple_grants_collapsing(self): |
635 | + member = self.factory.makePerson() |
636 | + second_member = self.factory.makePerson() |
637 | + third_member = self.factory.makePerson() |
638 | + owner = self.factory.makePerson() |
639 | + repository = removeSecurityProxy( |
640 | + self.factory.makeGitRepository(owner=owner)) |
641 | + |
642 | + rule = self.factory.makeGitRule(repository=repository) |
643 | + self.factory.makeGitRuleGrant( |
644 | + rule=rule, grantee=member, can_create=True) |
645 | + self.factory.makeGitRuleGrant( |
646 | + rule=rule, grantee=second_member, can_push=True) |
647 | + self.factory.makeGitRuleGrant( |
648 | + rule=rule, grantee=third_member, can_force_push=True) |
649 | + |
650 | + results = self.git_api.listRefRules( |
651 | + repository.getInternalPath(), |
652 | + {'uid': owner.id}) |
653 | + |
654 | + self.assertThat(results, MatchesListwise([ |
655 | + MatchesDict({ |
656 | + 'ref_pattern': Equals('refs/heads/*'), |
657 | + 'permissions': Equals(['create', 'push']), |
658 | + }), |
659 | + MatchesDict({ |
660 | + 'ref_pattern': Equals('*'), |
661 | + 'permissions': Equals(['create', 'push', 'force_push']), |
662 | + }), |
663 | + ])) |
664 | + |
665 | + def test_listRefRules_grantee_owner_type(self): |
666 | + owner = self.factory.makePerson() |
667 | + repository = removeSecurityProxy( |
668 | + self.factory.makeGitRepository(owner=owner)) |
669 | + rule = self.factory.makeGitRule(repository=repository) |
670 | + self.factory.makeGitRuleGrant( |
671 | + rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER, |
672 | + can_create=True) |
673 | + |
674 | + results = self.git_api.listRefRules( |
675 | + repository.getInternalPath(), |
676 | + {'uid': owner.id}) |
677 | + |
678 | + self.assertThat(results, MatchesListwise([ |
679 | + MatchesDict({ |
680 | + 'ref_pattern': Equals('refs/heads/*'), |
681 | + 'permissions': Equals(['create']), |
682 | + }), |
683 | + MatchesDict({ |
684 | + 'ref_pattern': Equals('*'), |
685 | + 'permissions': Equals(['create', 'push', 'force_push']), |
686 | + }), |
687 | + ])) |
688 | + |
689 | + def test_listRefRules_grantee_owner_type_and_other_grants(self): |
690 | + owner = self.factory.makePerson() |
691 | + other_person = self.factory.makePerson() |
692 | + repository = removeSecurityProxy( |
693 | + self.factory.makeGitRepository(owner=owner)) |
694 | + rule = self.factory.makeGitRule(repository=repository) |
695 | + self.factory.makeGitRuleGrant( |
696 | + rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER, |
697 | + can_create=True) |
698 | + |
699 | + rule = self.factory.makeGitRule( |
700 | + repository=repository, ref_pattern=u'refs/heads/other') |
701 | + self.factory.makeGitRuleGrant( |
702 | + rule=rule, grantee=other_person, can_push=True) |
703 | + |
704 | + results = self.git_api.listRefRules( |
705 | + repository.getInternalPath(), |
706 | + {'uid': owner.id}) |
707 | + |
708 | + self.assertThat(results, MatchesListwise([ |
709 | + MatchesDict({ |
710 | + 'ref_pattern': Equals('refs/heads/other'), |
711 | + 'permissions': Equals(['create', 'push']), |
712 | + }), |
713 | + MatchesDict({ |
714 | + 'ref_pattern': Equals('refs/heads/*'), |
715 | + 'permissions': Equals(['create']), |
716 | + }), |
717 | + MatchesDict({ |
718 | + 'ref_pattern': Equals('*'), |
719 | + 'permissions': Equals(['create', 'push', 'force_push']), |
720 | + }), |
721 | + ])) |
722 | + |
723 | + def test_listRefRules_grantee_example_one(self): |
724 | + user_a = self.factory.makePerson() |
725 | + user_b = self.factory.makePerson() |
726 | + user_c = self.factory.makePerson() |
727 | + stable_team = self.factory.makeTeam(members=[user_a, user_b]) |
728 | + next_team = self.factory.makeTeam(members=[user_b, user_c]) |
729 | + |
730 | + repository = removeSecurityProxy( |
731 | + self.factory.makeGitRepository(owner=user_a)) |
732 | + |
733 | + rule = self.factory.makeGitRule( |
734 | + repository, ref_pattern=u'refs/heads/stable/next') |
735 | + self.factory.makeGitRuleGrant( |
736 | + rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER, |
737 | + can_force_push=True) |
738 | + |
739 | + rule = self.factory.makeGitRule( |
740 | + repository, ref_pattern=u'refs/heads/archived/*') |
741 | + self.factory.makeGitRuleGrant( |
742 | + rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER) |
743 | + self.factory.makeGitRuleGrant( |
744 | + rule=rule, grantee=user_b, can_create=True) |
745 | + |
746 | + rule = self.factory.makeGitRule( |
747 | + repository, ref_pattern=u'refs/heads/stable/*') |
748 | + self.factory.makeGitRuleGrant( |
749 | + rule=rule, grantee=stable_team, can_push=True) |
750 | + |
751 | + rule = self.factory.makeGitRule( |
752 | + repository, ref_pattern=u'refs/heads/*/next') |
753 | + self.factory.makeGitRuleGrant( |
754 | + rule=rule, grantee=next_team, can_force_push=True) |
755 | + |
756 | + rule = self.factory.makeGitRule( |
757 | + repository, ref_pattern=u'refs/tags/*') |
758 | + self.factory.makeGitRuleGrant( |
759 | + rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER, |
760 | + can_create=True) |
761 | + self.factory.makeGitRuleGrant( |
762 | + rule=rule, grantee=stable_team, can_create=True) |
763 | + |
764 | + results = self.git_api.listRefRules( |
765 | + repository.getInternalPath(), |
766 | + {'uid': user_a.id}) |
767 | + |
768 | + self.assertThat(results, MatchesListwise([ |
769 | + MatchesDict( |
770 | + {'ref_pattern': Equals('refs/heads/stable/next'), |
771 | + 'permissions': Equals(['push', 'force_push']), |
772 | + }), |
773 | + MatchesDict( |
774 | + {'ref_pattern': Equals('refs/heads/archived/*'), |
775 | + 'permissions': Equals([]), |
776 | + }), |
777 | + MatchesDict( |
778 | + {'ref_pattern': Equals('refs/heads/stable/*'), |
779 | + 'permissions': Equals(['create', 'push']), |
780 | + }), |
781 | + MatchesDict( |
782 | + {'ref_pattern': Equals('refs/heads/*/next'), |
783 | + 'permissions': Equals(['create', 'push']), |
784 | + }), |
785 | + MatchesDict( |
786 | + {'ref_pattern': Equals('refs/tags/*'), |
787 | + 'permissions': Equals(['create']), |
788 | + }), |
789 | + MatchesDict( |
790 | + {'ref_pattern': Equals('*'), |
791 | + 'permissions': Equals(['create', 'push', 'force_push']), |
792 | + }), |
793 | + ])) |
794 | + |
795 | + def test_listRefRules_grantee_example_two(self): |
796 | + user_a = self.factory.makePerson() |
797 | + user_b = self.factory.makePerson() |
798 | + user_c = self.factory.makePerson() |
799 | + stable_team = self.factory.makeTeam(members=[user_a, user_b]) |
800 | + next_team = self.factory.makeTeam(members=[user_b, user_c]) |
801 | + |
802 | + repository = removeSecurityProxy( |
803 | + self.factory.makeGitRepository(owner=user_a)) |
804 | + |
805 | + rule = self.factory.makeGitRule( |
806 | + repository, ref_pattern=u'refs/heads/stable/next') |
807 | + self.factory.makeGitRuleGrant( |
808 | + rule=rule, grantee=user_a, can_force_push=True) |
809 | + |
810 | + rule = self.factory.makeGitRule( |
811 | + repository, ref_pattern=u'refs/heads/archived/*') |
812 | + self.factory.makeGitRuleGrant( |
813 | + rule=rule, grantee=user_a) |
814 | + self.factory.makeGitRuleGrant( |
815 | + rule=rule, grantee=user_b, can_create=True) |
816 | + |
817 | + rule = self.factory.makeGitRule( |
818 | + repository, ref_pattern=u'refs/heads/stable/*') |
819 | + self.factory.makeGitRuleGrant( |
820 | + rule=rule, grantee=stable_team, can_push=True) |
821 | + |
822 | + rule = self.factory.makeGitRule( |
823 | + repository, ref_pattern=u'refs/heads/*/next') |
824 | + self.factory.makeGitRuleGrant( |
825 | + rule=rule, grantee=next_team, can_force_push=True) |
826 | + |
827 | + rule = self.factory.makeGitRule( |
828 | + repository, ref_pattern=u'refs/tags/*') |
829 | + self.factory.makeGitRuleGrant( |
830 | + rule=rule, grantee=user_a, can_create=True) |
831 | + self.factory.makeGitRuleGrant( |
832 | + rule=rule, grantee=stable_team, can_create=True) |
833 | + |
834 | + results = self.git_api.listRefRules( |
835 | + repository.getInternalPath(), |
836 | + {'uid': user_b.id}) |
837 | + |
838 | + self.assertThat(results, MatchesListwise([ |
839 | + MatchesDict( |
840 | + {'ref_pattern': Equals('refs/heads/archived/*'), |
841 | + 'permissions': Equals(['create']), |
842 | + }), |
843 | + MatchesDict( |
844 | + {'ref_pattern': Equals('refs/heads/stable/*'), |
845 | + 'permissions': Equals(['push']), |
846 | + }), |
847 | + MatchesDict( |
848 | + {'ref_pattern': Equals('refs/heads/*/next'), |
849 | + 'permissions': Equals(['push', 'force_push']), |
850 | + }), |
851 | + MatchesDict( |
852 | + {'ref_pattern': Equals('refs/tags/*'), |
853 | + 'permissions': Equals(['create']), |
854 | + }), |
855 | + ])) |
856 | + |
857 | + def test_listRefRules_grantee_example_three(self): |
858 | + user_a = self.factory.makePerson() |
859 | + user_b = self.factory.makePerson() |
860 | + user_c = self.factory.makePerson() |
861 | + stable_team = self.factory.makeTeam(members=[user_a, user_b]) |
862 | + next_team = self.factory.makeTeam(members=[user_b, user_c]) |
863 | + |
864 | + repository = removeSecurityProxy( |
865 | + self.factory.makeGitRepository(owner=user_a)) |
866 | + |
867 | + rule = self.factory.makeGitRule( |
868 | + repository, ref_pattern=u'refs/heads/stable/next') |
869 | + self.factory.makeGitRuleGrant( |
870 | + rule=rule, grantee=user_a, can_force_push=True) |
871 | + |
872 | + rule = self.factory.makeGitRule( |
873 | + repository, ref_pattern=u'refs/heads/archived/*') |
874 | + self.factory.makeGitRuleGrant( |
875 | + rule=rule, grantee=user_a) |
876 | + self.factory.makeGitRuleGrant( |
877 | + rule=rule, grantee=user_b, can_create=True) |
878 | + |
879 | + rule = self.factory.makeGitRule( |
880 | + repository, ref_pattern=u'refs/heads/stable/*') |
881 | + self.factory.makeGitRuleGrant( |
882 | + rule=rule, grantee=stable_team, can_push=True) |
883 | + |
884 | + rule = self.factory.makeGitRule( |
885 | + repository, ref_pattern=u'refs/heads/*/next') |
886 | + self.factory.makeGitRuleGrant( |
887 | + rule=rule, grantee=next_team, can_force_push=True) |
888 | + |
889 | + rule = self.factory.makeGitRule( |
890 | + repository, ref_pattern=u'refs/tags/*') |
891 | + self.factory.makeGitRuleGrant( |
892 | + rule=rule, grantee=user_a, can_create=True) |
893 | + self.factory.makeGitRuleGrant( |
894 | + rule=rule, grantee=stable_team, can_create=True) |
895 | + |
896 | + results = self.git_api.listRefRules( |
897 | + repository.getInternalPath(), |
898 | + {'uid': user_c.id}) |
899 | + |
900 | + self.assertThat(results, MatchesListwise([ |
901 | + MatchesDict( |
902 | + {'ref_pattern': Equals('refs/heads/*/next'), |
903 | + 'permissions': Equals(['push', 'force_push']), |
904 | + }), |
905 | + ])) |
906 | + |
907 | |
908 | class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory): |
909 | """Tests for the implementation of `IGitAPI`.""" |
I'm unsure on the use of IGitLookup to add an interface for doing the GitRuleGrant lookups, it seemed logical at the time but on reflection it doesn't quite feel to be in the right place/correct structure