Merge lp:~twom/launchpad/branch-permissions-for-gitapi into lp:launchpad
- branch-permissions-for-gitapi
- Merge into devel
Proposed by
Tom Wardill
Status: | Superseded | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~twom/launchpad/branch-permissions-for-gitapi | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
1527 lines (+1208/-4) 20 files modified
database/schema/patch-2209-85-0.sql (+68/-0) database/schema/security.cfg (+4/-0) lib/lp/code/configure.zcml (+37/-0) lib/lp/code/enums.py (+21/-0) lib/lp/code/interfaces/gitapi.py (+6/-0) lib/lp/code/interfaces/gitlookup.py (+12/-0) lib/lp/code/interfaces/gitrepository.py (+23/-0) lib/lp/code/interfaces/gitrule.py (+172/-0) lib/lp/code/model/gitlookup.py (+13/-0) lib/lp/code/model/gitrepository.py (+62/-0) lib/lp/code/model/gitrule.py (+199/-0) lib/lp/code/model/tests/test_gitrepository.py (+102/-1) lib/lp/code/model/tests/test_gitrule.py (+200/-0) lib/lp/code/xmlrpc/git.py (+43/-0) lib/lp/code/xmlrpc/tests/test_git.py (+119/-0) lib/lp/registry/personmerge.py (+22/-1) lib/lp/registry/tests/test_personmerge.py (+36/-1) lib/lp/security.py (+40/-0) lib/lp/testing/factory.py (+25/-0) scripts/close-account.py (+4/-1) |
||||
To merge this branch: | bzr merge lp:~twom/launchpad/branch-permissions-for-gitapi | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Launchpad code reviewers | Pending | ||
Review via email: mp+355715@code.launchpad.net |
This proposal has been superseded by 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.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'database/schema/patch-2209-85-0.sql' |
2 | --- database/schema/patch-2209-85-0.sql 1970-01-01 00:00:00 +0000 |
3 | +++ database/schema/patch-2209-85-0.sql 2018-09-26 15:35:28 +0000 |
4 | @@ -0,0 +1,68 @@ |
5 | +-- Copyright 2018 Canonical Ltd. This software is licensed under the |
6 | +-- GNU Affero General Public License version 3 (see the file LICENSE). |
7 | + |
8 | +SET client_min_messages=ERROR; |
9 | + |
10 | +CREATE TABLE GitRule ( |
11 | + id serial PRIMARY KEY, |
12 | + repository integer NOT NULL REFERENCES gitrepository, |
13 | + position integer NOT NULL, |
14 | + ref_pattern text NOT NULL, |
15 | + creator integer NOT NULL REFERENCES person, |
16 | + date_created timestamp without time zone DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') NOT NULL, |
17 | + date_last_modified timestamp without time zone DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') NOT NULL, |
18 | + CONSTRAINT gitrule__repository__position__key UNIQUE (repository, position) DEFERRABLE INITIALLY DEFERRED, |
19 | + CONSTRAINT gitrule__repository__ref_pattern__key UNIQUE (repository, ref_pattern), |
20 | + -- Used by repository_matches_rule constraint on GitRuleGrant. |
21 | + CONSTRAINT gitrule__repository__id__key UNIQUE (repository, id) |
22 | +); |
23 | + |
24 | +COMMENT ON TABLE GitRule IS 'An access rule for a Git repository.'; |
25 | +COMMENT ON COLUMN GitRule.repository IS 'The repository that this rule is for.'; |
26 | +COMMENT ON COLUMN GitRule.position IS 'The position of this rule in its repository''s rule order.'; |
27 | +COMMENT ON COLUMN GitRule.ref_pattern IS 'The pattern of references matched by this rule.'; |
28 | +COMMENT ON COLUMN GitRule.creator IS 'The user who created this rule.'; |
29 | +COMMENT ON COLUMN GitRule.date_created IS 'The time when this rule was created.'; |
30 | +COMMENT ON COLUMN GitRule.date_last_modified IS 'The time when this rule was last modified.'; |
31 | + |
32 | +CREATE TABLE GitRuleGrant ( |
33 | + id serial PRIMARY KEY, |
34 | + repository integer NOT NULL REFERENCES gitrepository, |
35 | + rule integer NOT NULL REFERENCES gitrule, |
36 | + grantee_type integer NOT NULL, |
37 | + grantee integer REFERENCES person, |
38 | + can_create boolean DEFAULT false NOT NULL, |
39 | + can_push boolean DEFAULT false NOT NULL, |
40 | + can_force_push boolean DEFAULT false NOT NULL, |
41 | + grantor integer NOT NULL REFERENCES person, |
42 | + date_created timestamp without time zone DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') NOT NULL, |
43 | + date_last_modified timestamp without time zone DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') NOT NULL, |
44 | + CONSTRAINT repository_matches_rule FOREIGN KEY (repository, rule) REFERENCES gitrule (repository, id), |
45 | + -- 2 == PERSON |
46 | + CONSTRAINT has_grantee CHECK ((grantee_type = 2) = (grantee IS NOT NULL)) |
47 | +); |
48 | + |
49 | +CREATE INDEX gitrulegrant__repository__idx |
50 | + ON GitRuleGrant(repository); |
51 | +CREATE UNIQUE INDEX gitrulegrant__rule__grantee_type__key |
52 | + ON GitRuleGrant(rule, grantee_type) |
53 | + -- 2 == PERSON |
54 | + WHERE grantee_type != 2; |
55 | +CREATE UNIQUE INDEX gitrulegrant__rule__grantee_type__grantee_key |
56 | + ON GitRuleGrant(rule, grantee_type, grantee) |
57 | + -- 2 == PERSON |
58 | + WHERE grantee_type = 2; |
59 | + |
60 | +COMMENT ON TABLE GitRuleGrant IS 'An access grant for a Git repository rule.'; |
61 | +COMMENT ON COLUMN GitRuleGrant.repository IS 'The repository that this grant is for.'; |
62 | +COMMENT ON COLUMN GitRuleGrant.rule IS 'The rule that this grant is for.'; |
63 | +COMMENT ON COLUMN GitRuleGrant.grantee_type IS 'The type of entity being granted access.'; |
64 | +COMMENT ON COLUMN GitRuleGrant.grantee IS 'The person or team being granted access.'; |
65 | +COMMENT ON COLUMN GitRuleGrant.can_create IS 'Whether creating references is allowed.'; |
66 | +COMMENT ON COLUMN GitRuleGrant.can_push IS 'Whether pushing references is allowed.'; |
67 | +COMMENT ON COLUMN GitRuleGrant.can_force_push IS 'Whether force-pushing references is allowed.'; |
68 | +COMMENT ON COLUMN GitRuleGrant.grantor IS 'The user who created this grant.'; |
69 | +COMMENT ON COLUMN GitRuleGrant.date_created IS 'The time when this grant was created.'; |
70 | +COMMENT ON COLUMN GitRuleGrant.date_last_modified IS 'The time when this grant was last modified.'; |
71 | + |
72 | +INSERT INTO LaunchpadDatabaseRevision VALUES (2209, 85, 0); |
73 | |
74 | === modified file 'database/schema/security.cfg' |
75 | --- database/schema/security.cfg 2018-09-10 12:45:07 +0000 |
76 | +++ database/schema/security.cfg 2018-09-26 15:35:28 +0000 |
77 | @@ -190,6 +190,8 @@ |
78 | public.gitjob = SELECT, INSERT, UPDATE, DELETE |
79 | public.gitref = SELECT, INSERT, UPDATE, DELETE |
80 | public.gitrepository = SELECT, INSERT, UPDATE, DELETE |
81 | +public.gitrule = SELECT, INSERT, UPDATE, DELETE |
82 | +public.gitrulegrant = SELECT, INSERT, UPDATE, DELETE |
83 | public.gitsubscription = SELECT, INSERT, UPDATE, DELETE |
84 | public.hwdevice = SELECT |
85 | public.hwdeviceclass = SELECT, INSERT, DELETE |
86 | @@ -2210,6 +2212,8 @@ |
87 | public.faq = SELECT, UPDATE |
88 | public.featureflagchangelogentry = SELECT, UPDATE |
89 | public.gitrepository = SELECT, UPDATE |
90 | +public.gitrule = SELECT, UPDATE |
91 | +public.gitrulegrant = SELECT, UPDATE, DELETE |
92 | public.gitsubscription = SELECT, UPDATE, DELETE |
93 | public.gpgkey = SELECT, UPDATE |
94 | public.hwsubmission = SELECT, UPDATE |
95 | |
96 | === modified file 'lib/lp/code/configure.zcml' |
97 | --- lib/lp/code/configure.zcml 2018-05-16 17:33:18 +0000 |
98 | +++ lib/lp/code/configure.zcml 2018-09-26 15:35:28 +0000 |
99 | @@ -919,6 +919,35 @@ |
100 | <allow interface="lp.code.interfaces.gitref.IGitRefRemoteSet" /> |
101 | </securedutility> |
102 | |
103 | + <!-- Git repository access rules --> |
104 | + |
105 | + <class class="lp.code.model.gitrule.GitRule"> |
106 | + <require |
107 | + permission="launchpad.View" |
108 | + interface="lp.code.interfaces.gitrule.IGitRuleView |
109 | + lp.code.interfaces.gitrule.IGitRuleEditableAttributes" /> |
110 | + <require |
111 | + permission="launchpad.Edit" |
112 | + interface="lp.code.interfaces.gitrule.IGitRuleEdit" |
113 | + set_schema="lp.code.interfaces.gitrule.IGitRuleEditableAttributes" /> |
114 | + </class> |
115 | + <subscriber |
116 | + for="lp.code.interfaces.gitrule.IGitRule zope.lifecycleevent.interfaces.IObjectModifiedEvent" |
117 | + handler="lp.code.model.gitrule.git_rule_modified"/> |
118 | + <class class="lp.code.model.gitrule.GitRuleGrant"> |
119 | + <require |
120 | + permission="launchpad.View" |
121 | + interface="lp.code.interfaces.gitrule.IGitRuleGrantView |
122 | + lp.code.interfaces.gitrule.IGitRuleGrantEditableAttributes" /> |
123 | + <require |
124 | + permission="launchpad.Edit" |
125 | + interface="lp.code.interfaces.gitrule.IGitRuleGrantEdit" |
126 | + set_schema="lp.code.interfaces.gitrule.IGitRuleGrantEditableAttributes" /> |
127 | + </class> |
128 | + <subscriber |
129 | + for="lp.code.interfaces.gitrule.IGitRuleGrant zope.lifecycleevent.interfaces.IObjectModifiedEvent" |
130 | + handler="lp.code.model.gitrule.git_rule_grant_modified"/> |
131 | + |
132 | <!-- GitCollection --> |
133 | |
134 | <class class="lp.code.model.gitcollection.GenericGitCollection"> |
135 | @@ -975,6 +1004,9 @@ |
136 | <adapter factory="lp.code.model.defaultgit.OwnerProjectDefaultGitRepository" /> |
137 | <adapter factory="lp.code.model.defaultgit.OwnerPackageDefaultGitRepository" /> |
138 | |
139 | + <class class="lp.code.model.gitlookup.GitRuleGrantLookup"> |
140 | + <allow interface="lp.code.interfaces.gitlookup.IGitRuleGrantLookup" /> |
141 | + </class> |
142 | <class class="lp.code.model.gitlookup.GitLookup"> |
143 | <allow interface="lp.code.interfaces.gitlookup.IGitLookup" /> |
144 | </class> |
145 | @@ -984,6 +1016,11 @@ |
146 | <allow interface="lp.code.interfaces.gitlookup.IGitLookup" /> |
147 | </securedutility> |
148 | <securedutility |
149 | + class="lp.code.model.gitlookup.GitRuleGrantLookup" |
150 | + provides="lp.code.interfaces.gitlookup.IGitRuleGrantLookup"> |
151 | + <allow interface="lp.code.interfaces.gitlookup.IGitRuleGrantLookup" /> |
152 | + </securedutility> |
153 | + <securedutility |
154 | class="lp.code.model.gitlookup.GitTraverser" |
155 | provides="lp.code.interfaces.gitlookup.IGitTraverser"> |
156 | <allow interface="lp.code.interfaces.gitlookup.IGitTraverser" /> |
157 | |
158 | === modified file 'lib/lp/code/enums.py' |
159 | --- lib/lp/code/enums.py 2017-06-15 01:02:11 +0000 |
160 | +++ lib/lp/code/enums.py 2018-09-26 15:35:28 +0000 |
161 | @@ -21,6 +21,7 @@ |
162 | 'CodeImportReviewStatus', |
163 | 'CodeReviewNotificationLevel', |
164 | 'CodeReviewVote', |
165 | + 'GitGranteeType', |
166 | 'GitObjectType', |
167 | 'GitRepositoryType', |
168 | 'NON_CVS_RCS_TYPES', |
169 | @@ -175,6 +176,26 @@ |
170 | """) |
171 | |
172 | |
173 | +class GitGranteeType(DBEnumeratedType): |
174 | + """Git Grantee Type |
175 | + |
176 | + Access grants for Git repositories can be made to various kinds of |
177 | + grantees. |
178 | + """ |
179 | + |
180 | + REPOSITORY_OWNER = DBItem(1, """ |
181 | + Repository owner |
182 | + |
183 | + A grant to the owner of the associated repository. |
184 | + """) |
185 | + |
186 | + PERSON = DBItem(2, """ |
187 | + Person |
188 | + |
189 | + A grant to a particular person or team. |
190 | + """) |
191 | + |
192 | + |
193 | class BranchLifecycleStatusFilter(EnumeratedType): |
194 | """Branch Lifecycle Status Filter |
195 | |
196 | |
197 | === modified file 'lib/lp/code/interfaces/gitapi.py' |
198 | --- lib/lp/code/interfaces/gitapi.py 2015-03-31 04:18:22 +0000 |
199 | +++ lib/lp/code/interfaces/gitapi.py 2018-09-26 15:35:28 +0000 |
200 | @@ -67,3 +67,9 @@ |
201 | :returns: An `Unauthorized` fault, as password authentication is |
202 | not yet supported. |
203 | """ |
204 | + |
205 | + def listRefRules(self, repository, user): |
206 | + """Return the list of RefRules for `user` in `repository` |
207 | + |
208 | + :returns: A List of rules for the user in the specified repository |
209 | + """ |
210 | |
211 | === modified file 'lib/lp/code/interfaces/gitlookup.py' |
212 | --- lib/lp/code/interfaces/gitlookup.py 2015-03-30 14:47:22 +0000 |
213 | +++ lib/lp/code/interfaces/gitlookup.py 2018-09-26 15:35:28 +0000 |
214 | @@ -6,6 +6,7 @@ |
215 | __metaclass__ = type |
216 | __all__ = [ |
217 | 'IGitLookup', |
218 | + 'IGitRuleGrantLookup', |
219 | 'IGitTraversable', |
220 | 'IGitTraverser', |
221 | ] |
222 | @@ -145,3 +146,14 @@ |
223 | leading part of a path as a repository, such as external code |
224 | browsers. |
225 | """ |
226 | + |
227 | + |
228 | +class IGitRuleGrantLookup(Interface): |
229 | + """Utility for looking up a GitRuleGrant by properties""" |
230 | + |
231 | + def getByRulesAffectingPerson(repository, grantee_id): |
232 | + """Find all the rules for a repository that affect a Person. |
233 | + |
234 | + :param repository: An instance of a GitRepository |
235 | + :param granteed_id: An integer of the id of the Person |
236 | + """ |
237 | |
238 | === modified file 'lib/lp/code/interfaces/gitrepository.py' |
239 | --- lib/lp/code/interfaces/gitrepository.py 2018-08-23 17:03:05 +0000 |
240 | +++ lib/lp/code/interfaces/gitrepository.py 2018-09-26 15:35:28 +0000 |
241 | @@ -266,6 +266,10 @@ |
242 | # Really ICodeImport, patched in _schema_circular_imports.py. |
243 | schema=Interface)) |
244 | |
245 | + rules = Attribute("The access rules for this repository.") |
246 | + |
247 | + grants = Attribute("The access grants for this repository.") |
248 | + |
249 | @operation_parameters( |
250 | path=TextLine(title=_("A string to look up as a path."))) |
251 | # Really IGitRef, patched in _schema_circular_imports.py. |
252 | @@ -715,6 +719,25 @@ |
253 | This may be helpful in cases where a previous scan crashed. |
254 | """ |
255 | |
256 | + def addRule(ref_pattern, creator, position=None): |
257 | + """Add an access rule to this repository. |
258 | + |
259 | + :param ref_pattern: The reference pattern that the new rule should |
260 | + match. |
261 | + :param creator: The `IPerson` who is adding the rule. |
262 | + :param position: The list position at which to insert the rule, or |
263 | + None to append it. |
264 | + """ |
265 | + |
266 | + def moveRule(rule, position): |
267 | + """Move a rule to a new position in its repository's rule order. |
268 | + |
269 | + :param rule: The `IGitRule` to move. |
270 | + :param position: The new position. For example, 0 puts the rule at |
271 | + the start, while `len(repository.rules)` puts the rule at the |
272 | + end. |
273 | + """ |
274 | + |
275 | @export_read_operation() |
276 | @operation_for_version("devel") |
277 | def canBeDeleted(): |
278 | |
279 | === added file 'lib/lp/code/interfaces/gitrule.py' |
280 | --- lib/lp/code/interfaces/gitrule.py 1970-01-01 00:00:00 +0000 |
281 | +++ lib/lp/code/interfaces/gitrule.py 2018-09-26 15:35:28 +0000 |
282 | @@ -0,0 +1,172 @@ |
283 | +# Copyright 2018 Canonical Ltd. This software is licensed under the |
284 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
285 | + |
286 | +"""Git repository access rules.""" |
287 | + |
288 | +from __future__ import absolute_import, print_function, unicode_literals |
289 | + |
290 | +__metaclass__ = type |
291 | +__all__ = [ |
292 | + 'IGitRule', |
293 | + 'IGitRuleGrant', |
294 | + ] |
295 | + |
296 | +from lazr.restful.fields import Reference |
297 | +from zope.interface import ( |
298 | + Attribute, |
299 | + Interface, |
300 | + ) |
301 | +from zope.schema import ( |
302 | + Bool, |
303 | + Choice, |
304 | + Datetime, |
305 | + Int, |
306 | + TextLine, |
307 | + ) |
308 | + |
309 | +from lp import _ |
310 | +from lp.code.enums import GitGranteeType |
311 | +from lp.code.interfaces.gitrepository import IGitRepository |
312 | +from lp.services.fields import ( |
313 | + PersonChoice, |
314 | + PublicPersonChoice, |
315 | + ) |
316 | + |
317 | + |
318 | +class IGitRuleView(Interface): |
319 | + """`IGitRule` attributes that require launchpad.View.""" |
320 | + |
321 | + id = Int(title=_("ID"), readonly=True, required=True) |
322 | + |
323 | + repository = Reference( |
324 | + title=_("Repository"), required=True, readonly=True, |
325 | + schema=IGitRepository, |
326 | + description=_("The repository that this rule is for.")) |
327 | + |
328 | + position = Int( |
329 | + title=_("Position"), required=True, readonly=True, |
330 | + description=_( |
331 | + "The position of this rule in its repository's rule order.")) |
332 | + |
333 | + creator = PublicPersonChoice( |
334 | + title=_("Creator"), required=True, readonly=True, |
335 | + vocabulary="ValidPerson", |
336 | + description=_("The user who created this rule.")) |
337 | + |
338 | + date_created = Datetime( |
339 | + title=_("Date created"), required=True, readonly=True, |
340 | + description=_("The time when this rule was created.")) |
341 | + |
342 | + grants = Attribute("The access grants for this rule.") |
343 | + |
344 | + |
345 | +class IGitRuleEditableAttributes(Interface): |
346 | + """`IGitRule` attributes that can be edited. |
347 | + |
348 | + These attributes need launchpad.View to see, and launchpad.Edit to change. |
349 | + """ |
350 | + |
351 | + ref_pattern = TextLine( |
352 | + title=_("Pattern"), required=True, readonly=False, |
353 | + description=_("The pattern of references matched by this rule.")) |
354 | + |
355 | + date_last_modified = Datetime( |
356 | + title=_("Date last modified"), required=True, readonly=True, |
357 | + description=_("The time when this rule was last modified.")) |
358 | + |
359 | + |
360 | +class IGitRuleEdit(Interface): |
361 | + """`IGitRule` attributes that require launchpad.Edit.""" |
362 | + |
363 | + def addGrant(grantee, grantor, can_create=False, can_push=False, |
364 | + can_force_push=False): |
365 | + """Add an access grant to this rule. |
366 | + |
367 | + :param grantee: The `IPerson` who is being granted permission, or an |
368 | + item of `GitGranteeType` other than `GitGranteeType.PERSON` to |
369 | + grant permission to some other kind of entity. |
370 | + :param grantor: The `IPerson` who is granting permission. |
371 | + :param can_create: Whether the grantee can create references |
372 | + matching this rule. |
373 | + :param can_push: Whether the grantee can push references matching |
374 | + this rule. |
375 | + :param can_force_push: Whether the grantee can force-push references |
376 | + matching this rule. |
377 | + """ |
378 | + |
379 | + def destroySelf(): |
380 | + """Delete this rule.""" |
381 | + |
382 | + |
383 | +class IGitRule(IGitRuleView, IGitRuleEditableAttributes, IGitRuleEdit): |
384 | + """An access rule for a Git repository.""" |
385 | + |
386 | + |
387 | +class IGitRuleGrantView(Interface): |
388 | + """`IGitRuleGrant` attributes that require launchpad.View.""" |
389 | + |
390 | + id = Int(title=_("ID"), readonly=True, required=True) |
391 | + |
392 | + repository = Reference( |
393 | + title=_("Repository"), required=True, readonly=True, |
394 | + schema=IGitRepository, |
395 | + description=_("The repository that this grant is for.")) |
396 | + |
397 | + rule = Reference( |
398 | + title=_("Rule"), required=True, readonly=True, |
399 | + schema=IGitRule, |
400 | + description=_("The rule that this grant is for.")) |
401 | + |
402 | + grantor = PublicPersonChoice( |
403 | + title=_("Grantor"), required=True, readonly=True, |
404 | + vocabulary="ValidPerson", |
405 | + description=_("The user who created this grant.")) |
406 | + |
407 | + grantee_type = Choice( |
408 | + title=_("Grantee type"), required=True, readonly=True, |
409 | + vocabulary=GitGranteeType, |
410 | + description=_("The type of grantee for this grant.")) |
411 | + |
412 | + grantee = PersonChoice( |
413 | + title=_("Grantee"), required=False, readonly=True, |
414 | + vocabulary="ValidPersonOrTeam", |
415 | + description=_("The person being granted access.")) |
416 | + |
417 | + date_created = Datetime( |
418 | + title=_("Date created"), required=True, readonly=True, |
419 | + description=_("The time when this grant was created.")) |
420 | + |
421 | + |
422 | +class IGitRuleGrantEditableAttributes(Interface): |
423 | + """`IGitRuleGrant` attributes that can be edited. |
424 | + |
425 | + These attributes need launchpad.View to see, and launchpad.Edit to change. |
426 | + """ |
427 | + |
428 | + can_create = Bool( |
429 | + title=_("Can create"), required=True, readonly=False, |
430 | + description=_("Whether creating references is allowed.")) |
431 | + |
432 | + can_push = Bool( |
433 | + title=_("Can push"), required=True, readonly=False, |
434 | + description=_("Whether pushing references is allowed.")) |
435 | + |
436 | + can_force_push = Bool( |
437 | + title=_("Can force-push"), required=True, readonly=False, |
438 | + description=_("Whether force-pushing references is allowed.")) |
439 | + |
440 | + date_last_modified = Datetime( |
441 | + title=_("Date last modified"), required=True, readonly=True, |
442 | + description=_("The time when this grant was last modified.")) |
443 | + |
444 | + |
445 | +class IGitRuleGrantEdit(Interface): |
446 | + """`IGitRuleGrant` attributes that require launchpad.Edit.""" |
447 | + |
448 | + def destroySelf(): |
449 | + """Delete this access grant.""" |
450 | + |
451 | + |
452 | +class IGitRuleGrant(IGitRuleGrantView, IGitRuleGrantEditableAttributes, |
453 | + IGitRuleGrantEdit): |
454 | + """An access grant for a Git repository rule.""" |
455 | |
456 | === modified file 'lib/lp/code/model/gitlookup.py' |
457 | --- lib/lp/code/model/gitlookup.py 2018-07-23 10:28:33 +0000 |
458 | +++ lib/lp/code/model/gitlookup.py 2018-09-26 15:35:28 +0000 |
459 | @@ -27,6 +27,7 @@ |
460 | ) |
461 | from lp.code.interfaces.gitlookup import ( |
462 | IGitLookup, |
463 | + IGitRuleGrantLookup, |
464 | IGitTraversable, |
465 | IGitTraverser, |
466 | ) |
467 | @@ -34,6 +35,7 @@ |
468 | from lp.code.interfaces.gitrepository import IGitRepositorySet |
469 | from lp.code.interfaces.hasgitrepositories import IHasGitRepositories |
470 | from lp.code.model.gitrepository import GitRepository |
471 | +from lp.code.model.gitrule import GitRuleGrant |
472 | from lp.registry.errors import NoSuchSourcePackageName |
473 | from lp.registry.interfaces.distribution import IDistribution |
474 | from lp.registry.interfaces.distributionsourcepackage import ( |
475 | @@ -372,3 +374,14 @@ |
476 | if trailing: |
477 | trailing_segments.insert(0, trailing) |
478 | return repository, "/".join(trailing_segments) |
479 | + |
480 | + |
481 | +@implementer(IGitRuleGrantLookup) |
482 | +class GitRuleGrantLookup: |
483 | + |
484 | + def getByRulesAffectingPerson(self, repository, grantee): |
485 | + grants = IStore(GitRuleGrant).find( |
486 | + GitRuleGrant, |
487 | + GitRuleGrant.repository == repository) |
488 | + grants = [grant for grant in grants if grantee.inTeam(grant.grantee)] |
489 | + return grants |
490 | |
491 | === modified file 'lib/lp/code/model/gitrepository.py' |
492 | --- lib/lp/code/model/gitrepository.py 2018-09-06 14:25:46 +0000 |
493 | +++ lib/lp/code/model/gitrepository.py 2018-09-26 15:35:28 +0000 |
494 | @@ -41,6 +41,7 @@ |
495 | Bool, |
496 | DateTime, |
497 | Int, |
498 | + List, |
499 | Reference, |
500 | Unicode, |
501 | ) |
502 | @@ -112,6 +113,10 @@ |
503 | GitRef, |
504 | GitRefDefault, |
505 | ) |
506 | +from lp.code.model.gitrule import ( |
507 | + GitRule, |
508 | + GitRuleGrant, |
509 | + ) |
510 | from lp.code.model.gitsubscription import GitSubscription |
511 | from lp.registry.enums import PersonVisibility |
512 | from lp.registry.errors import CannotChangeInformationType |
513 | @@ -1121,6 +1126,61 @@ |
514 | def code_import(self): |
515 | return getUtility(ICodeImportSet).getByGitRepository(self) |
516 | |
517 | + @property |
518 | + def rules(self): |
519 | + """See `IGitRepository`.""" |
520 | + return Store.of(self).find( |
521 | + GitRule, GitRule.repository == self).order_by(GitRule.position) |
522 | + |
523 | + def _syncRulePositions(self, rules): |
524 | + """Synchronise rule positions with their order in a provided list. |
525 | + |
526 | + :param rules: A sequence of `IGitRule`s in the desired order. |
527 | + """ |
528 | + # This approach requires fetching all this repository's rules, which |
529 | + # is potentially more work than necessary. However, it has the |
530 | + # benefit of being simple, and because it ensures the correct |
531 | + # position of all rules it tends to be self-correcting. |
532 | + for position, rule in enumerate(rules): |
533 | + if rule.repository != self: |
534 | + raise AssertionError("%r does not belong to %r" % (rule, self)) |
535 | + if rule.position != position: |
536 | + removeSecurityProxy(rule).position = position |
537 | + |
538 | + def addRule(self, ref_pattern, creator, position=None): |
539 | + """See `IGitRepository`.""" |
540 | + rules = list(self.rules) |
541 | + rule = GitRule( |
542 | + repository=self, |
543 | + # -1 isn't a valid position, but _syncRulePositions will correct |
544 | + # it in a moment. |
545 | + position=position if position is not None else -1, |
546 | + ref_pattern=ref_pattern, creator=creator, date_created=DEFAULT) |
547 | + if position is None: |
548 | + rules.append(rule) |
549 | + else: |
550 | + rules.insert(position, rule) |
551 | + self._syncRulePositions(rules) |
552 | + return rule |
553 | + |
554 | + def moveRule(self, rule, position): |
555 | + """See `IGitRepository`.""" |
556 | + if rule.repository != self: |
557 | + raise ValueError("%r does not belong to %r" % (rule, self)) |
558 | + if position < 0: |
559 | + raise ValueError("Negative positions are not supported") |
560 | + if position != rule.position: |
561 | + rules = list(self.rules) |
562 | + rules.remove(rule) |
563 | + rules.insert(position, rule) |
564 | + self._syncRulePositions(rules) |
565 | + |
566 | + @property |
567 | + def grants(self): |
568 | + """See `IGitRepository`.""" |
569 | + return Store.of(self).find( |
570 | + GitRuleGrant, GitRuleGrant.repository_id == self.id) |
571 | + |
572 | def canBeDeleted(self): |
573 | """See `IGitRepository`.""" |
574 | # Can't delete if the repository is associated with anything. |
575 | @@ -1252,6 +1312,8 @@ |
576 | self._deleteRepositorySubscriptions() |
577 | self._deleteJobs() |
578 | getUtility(IWebhookSet).delete(self.webhooks) |
579 | + self.grants.remove() |
580 | + self.rules.remove() |
581 | |
582 | # Now destroy the repository. |
583 | repository_name = self.unique_name |
584 | |
585 | === added file 'lib/lp/code/model/gitrule.py' |
586 | --- lib/lp/code/model/gitrule.py 1970-01-01 00:00:00 +0000 |
587 | +++ lib/lp/code/model/gitrule.py 2018-09-26 15:35:28 +0000 |
588 | @@ -0,0 +1,199 @@ |
589 | +# Copyright 2018 Canonical Ltd. This software is licensed under the |
590 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
591 | + |
592 | +"""Git repository access rules.""" |
593 | + |
594 | +from __future__ import absolute_import, print_function, unicode_literals |
595 | + |
596 | +__metaclass__ = type |
597 | +__all__ = [ |
598 | + 'GitRule', |
599 | + 'GitRuleGrant', |
600 | + ] |
601 | + |
602 | +from lazr.enum import DBItem |
603 | +import pytz |
604 | +from storm.locals import ( |
605 | + Bool, |
606 | + DateTime, |
607 | + Int, |
608 | + Reference, |
609 | + Store, |
610 | + Unicode, |
611 | + ) |
612 | +from zope.interface import implementer |
613 | +from zope.security.proxy import removeSecurityProxy |
614 | + |
615 | +from lp.code.enums import GitGranteeType |
616 | +from lp.code.interfaces.gitrule import ( |
617 | + IGitRule, |
618 | + IGitRuleGrant, |
619 | + ) |
620 | +from lp.registry.interfaces.person import ( |
621 | + validate_person, |
622 | + validate_public_person, |
623 | + ) |
624 | +from lp.services.database.constants import ( |
625 | + DEFAULT, |
626 | + UTC_NOW, |
627 | + ) |
628 | +from lp.services.database.enumcol import DBEnum |
629 | +from lp.services.database.stormbase import StormBase |
630 | + |
631 | + |
632 | +def git_rule_modified(rule, event): |
633 | + """Update date_last_modified when a GitRule is modified. |
634 | + |
635 | + This method is registered as a subscriber to `IObjectModifiedEvent` |
636 | + events on Git repository rules. |
637 | + """ |
638 | + if event.edited_fields: |
639 | + rule.date_last_modified = UTC_NOW |
640 | + |
641 | + |
642 | +@implementer(IGitRule) |
643 | +class GitRule(StormBase): |
644 | + """See `IGitRule`.""" |
645 | + |
646 | + __storm_table__ = 'GitRule' |
647 | + |
648 | + id = Int(primary=True) |
649 | + |
650 | + repository_id = Int(name='repository', allow_none=False) |
651 | + repository = Reference(repository_id, 'GitRepository.id') |
652 | + |
653 | + position = Int(name='position', allow_none=False) |
654 | + |
655 | + ref_pattern = Unicode(name='ref_pattern', allow_none=False) |
656 | + |
657 | + creator_id = Int( |
658 | + name='creator', allow_none=False, validator=validate_public_person) |
659 | + creator = Reference(creator_id, 'Person.id') |
660 | + |
661 | + date_created = DateTime( |
662 | + name='date_created', tzinfo=pytz.UTC, allow_none=False) |
663 | + date_last_modified = DateTime( |
664 | + name='date_last_modified', tzinfo=pytz.UTC, allow_none=False) |
665 | + |
666 | + def __init__(self, repository, position, ref_pattern, creator, |
667 | + date_created): |
668 | + super(GitRule, self).__init__() |
669 | + self.repository = repository |
670 | + self.position = position |
671 | + self.ref_pattern = ref_pattern |
672 | + self.creator = creator |
673 | + self.date_created = date_created |
674 | + self.date_last_modified = date_created |
675 | + |
676 | + def __repr__(self): |
677 | + return "<GitRule '%s'> for %r" % (self.ref_pattern, self.repository) |
678 | + |
679 | + @property |
680 | + def grants(self): |
681 | + """See `IGitRule`.""" |
682 | + return Store.of(self).find( |
683 | + GitRuleGrant, GitRuleGrant.rule_id == self.id) |
684 | + |
685 | + def addGrant(self, grantee, grantor, can_create=False, can_push=False, |
686 | + can_force_push=False): |
687 | + """See `IGitRule`.""" |
688 | + return GitRuleGrant( |
689 | + rule=self, grantee=grantee, can_create=can_create, |
690 | + can_push=can_push, can_force_push=can_force_push, grantor=grantor, |
691 | + date_created=DEFAULT) |
692 | + |
693 | + def destroySelf(self): |
694 | + """See `IGitRule`.""" |
695 | + for grant in self.grants: |
696 | + grant.destroySelf() |
697 | + rules = list(self.repository.rules) |
698 | + Store.of(self).remove(self) |
699 | + rules.remove(self) |
700 | + removeSecurityProxy(self.repository)._syncRulePositions(rules) |
701 | + |
702 | + |
703 | +def git_rule_grant_modified(grant, event): |
704 | + """Update date_last_modified when a GitRuleGrant is modified. |
705 | + |
706 | + This method is registered as a subscriber to `IObjectModifiedEvent` |
707 | + events on Git repository grants. |
708 | + """ |
709 | + if event.edited_fields: |
710 | + grant.date_last_modified = UTC_NOW |
711 | + |
712 | + |
713 | +@implementer(IGitRuleGrant) |
714 | +class GitRuleGrant(StormBase): |
715 | + """See `IGitRuleGrant`.""" |
716 | + |
717 | + __storm_table__ = 'GitRuleGrant' |
718 | + |
719 | + id = Int(primary=True) |
720 | + |
721 | + repository_id = Int(name='repository', allow_none=False) |
722 | + repository = Reference(repository_id, 'GitRepository.id') |
723 | + |
724 | + rule_id = Int(name='rule', allow_none=False) |
725 | + rule = Reference(rule_id, 'GitRule.id') |
726 | + |
727 | + grantee_type = DBEnum( |
728 | + name='grantee_type', enum=GitGranteeType, allow_none=False) |
729 | + |
730 | + grantee_id = Int( |
731 | + name='grantee', allow_none=True, validator=validate_person) |
732 | + grantee = Reference(grantee_id, 'Person.id') |
733 | + |
734 | + can_create = Bool(name='can_create', allow_none=False) |
735 | + can_push = Bool(name='can_push', allow_none=False) |
736 | + can_force_push = Bool(name='can_force_push', allow_none=False) |
737 | + |
738 | + grantor_id = Int( |
739 | + name='grantor', allow_none=False, validator=validate_public_person) |
740 | + grantor = Reference(grantor_id, 'Person.id') |
741 | + |
742 | + date_created = DateTime( |
743 | + name='date_created', tzinfo=pytz.UTC, allow_none=False) |
744 | + date_last_modified = DateTime( |
745 | + name='date_last_modified', tzinfo=pytz.UTC, allow_none=False) |
746 | + |
747 | + def __init__(self, rule, grantee, can_create, can_push, can_force_push, |
748 | + grantor, date_created): |
749 | + if isinstance(grantee, DBItem) and grantee.enum == GitGranteeType: |
750 | + if grantee == GitGranteeType.PERSON: |
751 | + raise ValueError( |
752 | + "grantee may not be GitGranteeType.PERSON; pass a person " |
753 | + "object instead") |
754 | + grantee_type = grantee |
755 | + grantee = None |
756 | + else: |
757 | + grantee_type = GitGranteeType.PERSON |
758 | + |
759 | + self.repository = rule.repository |
760 | + self.rule = rule |
761 | + self.grantee_type = grantee_type |
762 | + self.grantee = grantee |
763 | + self.can_create = can_create |
764 | + self.can_push = can_push |
765 | + self.can_force_push = can_force_push |
766 | + self.grantor = grantor |
767 | + self.date_created = date_created |
768 | + self.date_last_modified = date_created |
769 | + |
770 | + def __repr__(self): |
771 | + permissions = [] |
772 | + if self.can_create: |
773 | + permissions.append("create") |
774 | + if self.can_push: |
775 | + permissions.append("push") |
776 | + if self.can_force_push: |
777 | + permissions.append("force-push") |
778 | + if self.grantee_type == GitGranteeType.PERSON: |
779 | + grantee_name = "~%s" % self.grantee.name |
780 | + else: |
781 | + grantee_name = self.grantee_type.title.lower() |
782 | + return "<GitRuleGrant [%s] to %s> for %r" % ( |
783 | + ", ".join(permissions), grantee_name, self.rule) |
784 | + |
785 | + def destroySelf(self): |
786 | + """See `IGitRuleGrant`.""" |
787 | + Store.of(self).remove(self) |
788 | |
789 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' |
790 | --- lib/lp/code/model/tests/test_gitrepository.py 2018-08-31 14:25:40 +0000 |
791 | +++ lib/lp/code/model/tests/test_gitrepository.py 2018-09-26 15:35:28 +0000 |
792 | @@ -1,4 +1,4 @@ |
793 | -# Copyright 2015-2017 Canonical Ltd. This software is licensed under the |
794 | +# Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
795 | # GNU Affero General Public License version 3 (see the file LICENSE). |
796 | |
797 | """Tests for Git repositories.""" |
798 | @@ -23,6 +23,7 @@ |
799 | from testtools.matchers import ( |
800 | EndsWith, |
801 | LessThan, |
802 | + MatchesListwise, |
803 | MatchesSetwise, |
804 | MatchesStructure, |
805 | ) |
806 | @@ -537,6 +538,14 @@ |
807 | transaction.commit() |
808 | self.assertRaises(LostObjectError, getattr, webhook, 'target') |
809 | |
810 | + def test_related_rules_and_grants_deleted(self): |
811 | + rule = self.factory.makeGitRule(repository=self.repository) |
812 | + grant = self.factory.makeGitRuleGrant(rule=rule) |
813 | + self.repository.destroySelf() |
814 | + transaction.commit() |
815 | + self.assertRaises(LostObjectError, getattr, grant, 'rule') |
816 | + self.assertRaises(LostObjectError, getattr, rule, 'repository') |
817 | + |
818 | |
819 | class TestGitRepositoryDeletionConsequences(TestCaseWithFactory): |
820 | """Test determination and application of repository deletion |
821 | @@ -2179,6 +2188,98 @@ |
822 | self.assertEqual('Some text', ret) |
823 | |
824 | |
825 | +class TestGitRepositoryRules(TestCaseWithFactory): |
826 | + |
827 | + layer = DatabaseFunctionalLayer |
828 | + |
829 | + def test_rules(self): |
830 | + repository = self.factory.makeGitRepository() |
831 | + other_repository = self.factory.makeGitRepository() |
832 | + self.factory.makeGitRule( |
833 | + repository=repository, ref_pattern="refs/heads/*") |
834 | + self.factory.makeGitRule( |
835 | + repository=repository, ref_pattern="refs/heads/stable/*") |
836 | + self.factory.makeGitRule( |
837 | + repository=other_repository, ref_pattern="refs/heads/*") |
838 | + self.assertThat(list(repository.rules), MatchesListwise([ |
839 | + MatchesStructure.byEquality( |
840 | + repository=repository, |
841 | + ref_pattern="refs/heads/*"), |
842 | + MatchesStructure.byEquality( |
843 | + repository=repository, |
844 | + ref_pattern="refs/heads/stable/*"), |
845 | + ])) |
846 | + |
847 | + def test_addRule_append(self): |
848 | + repository = self.factory.makeGitRepository() |
849 | + initial_rule = self.factory.makeGitRule( |
850 | + repository=repository, ref_pattern="refs/heads/*") |
851 | + self.assertEqual(0, initial_rule.position) |
852 | + with person_logged_in(repository.owner): |
853 | + new_rule = repository.addRule( |
854 | + "refs/heads/stable/*", repository.owner) |
855 | + self.assertEqual(1, new_rule.position) |
856 | + self.assertEqual([initial_rule, new_rule], list(repository.rules)) |
857 | + |
858 | + def test_addRule_insert(self): |
859 | + repository = self.factory.makeGitRepository() |
860 | + initial_rules = [ |
861 | + self.factory.makeGitRule( |
862 | + repository=repository, ref_pattern="refs/heads/*"), |
863 | + self.factory.makeGitRule( |
864 | + repository=repository, ref_pattern="refs/heads/protected"), |
865 | + self.factory.makeGitRule( |
866 | + repository=repository, ref_pattern="refs/heads/another"), |
867 | + ] |
868 | + self.assertEqual([0, 1, 2], [rule.position for rule in initial_rules]) |
869 | + with person_logged_in(repository.owner): |
870 | + new_rule = repository.addRule( |
871 | + "refs/heads/stable/*", repository.owner, position=1) |
872 | + self.assertEqual(1, new_rule.position) |
873 | + self.assertEqual([0, 2, 3], [rule.position for rule in initial_rules]) |
874 | + self.assertEqual( |
875 | + [initial_rules[0], new_rule, initial_rules[1], initial_rules[2]], |
876 | + list(repository.rules)) |
877 | + |
878 | + def test_moveRule(self): |
879 | + repository = self.factory.makeGitRepository() |
880 | + rules = [ |
881 | + self.factory.makeGitRule( |
882 | + repository=repository, |
883 | + ref_pattern=self.factory.getUniqueUnicode( |
884 | + prefix="refs/heads/")) |
885 | + for _ in range(5)] |
886 | + with person_logged_in(repository.owner): |
887 | + self.assertEqual(rules, list(repository.rules)) |
888 | + repository.moveRule(rules[0], 4) |
889 | + self.assertEqual(rules[1:] + [rules[0]], list(repository.rules)) |
890 | + repository.moveRule(rules[0], 0) |
891 | + self.assertEqual(rules, list(repository.rules)) |
892 | + repository.moveRule(rules[2], 1) |
893 | + self.assertEqual( |
894 | + [rules[0], rules[2], rules[1], rules[3], rules[4]], |
895 | + list(repository.rules)) |
896 | + |
897 | + def test_moveRule_non_negative(self): |
898 | + rule = self.factory.makeGitRule() |
899 | + with person_logged_in(rule.repository.owner): |
900 | + self.assertRaises(ValueError, rule.repository.moveRule, rule, -1) |
901 | + |
902 | + def test_grants(self): |
903 | + repository = self.factory.makeGitRepository() |
904 | + other_repository = self.factory.makeGitRepository() |
905 | + rule = self.factory.makeGitRule(repository=repository) |
906 | + other_rule = self.factory.makeGitRule(repository=other_repository) |
907 | + grants = [ |
908 | + self.factory.makeGitRuleGrant( |
909 | + rule=rule, grantee=self.factory.makePerson()) |
910 | + for _ in range(2) |
911 | + ] |
912 | + self.factory.makeGitRuleGrant( |
913 | + rule=other_rule, grantee=self.factory.makePerson()) |
914 | + self.assertContentEqual(grants, repository.grants) |
915 | + |
916 | + |
917 | class TestGitRepositorySet(TestCaseWithFactory): |
918 | |
919 | layer = DatabaseFunctionalLayer |
920 | |
921 | === added file 'lib/lp/code/model/tests/test_gitrule.py' |
922 | --- lib/lp/code/model/tests/test_gitrule.py 1970-01-01 00:00:00 +0000 |
923 | +++ lib/lp/code/model/tests/test_gitrule.py 2018-09-26 15:35:28 +0000 |
924 | @@ -0,0 +1,200 @@ |
925 | +# Copyright 2018 Canonical Ltd. This software is licensed under the |
926 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
927 | + |
928 | +"""Tests for Git repository access rules.""" |
929 | + |
930 | +from __future__ import absolute_import, print_function, unicode_literals |
931 | + |
932 | +__metaclass__ = type |
933 | + |
934 | +from storm.store import Store |
935 | +from testtools.matchers import ( |
936 | + Equals, |
937 | + Is, |
938 | + MatchesSetwise, |
939 | + MatchesStructure, |
940 | + ) |
941 | + |
942 | +from lp.code.enums import GitGranteeType |
943 | +from lp.code.interfaces.gitrule import ( |
944 | + IGitRule, |
945 | + IGitRuleGrant, |
946 | + ) |
947 | +from lp.services.database.sqlbase import get_transaction_timestamp |
948 | +from lp.testing import ( |
949 | + person_logged_in, |
950 | + TestCaseWithFactory, |
951 | + verifyObject, |
952 | + ) |
953 | +from lp.testing.layers import DatabaseFunctionalLayer |
954 | + |
955 | + |
956 | +class TestGitRule(TestCaseWithFactory): |
957 | + |
958 | + layer = DatabaseFunctionalLayer |
959 | + |
960 | + def test_implements_IGitRule(self): |
961 | + rule = self.factory.makeGitRule() |
962 | + verifyObject(IGitRule, rule) |
963 | + |
964 | + def test_properties(self): |
965 | + owner = self.factory.makeTeam() |
966 | + member = self.factory.makePerson(member_of=[owner]) |
967 | + repository = self.factory.makeGitRepository(owner=owner) |
968 | + rule = self.factory.makeGitRule( |
969 | + repository=repository, ref_pattern="refs/heads/stable/*", |
970 | + creator=member) |
971 | + now = get_transaction_timestamp(Store.of(rule)) |
972 | + self.assertThat(rule, MatchesStructure.byEquality( |
973 | + repository=repository, |
974 | + ref_pattern="refs/heads/stable/*", |
975 | + creator=member, |
976 | + date_created=now, |
977 | + date_last_modified=now)) |
978 | + |
979 | + def test_repr(self): |
980 | + repository = self.factory.makeGitRepository() |
981 | + rule = self.factory.makeGitRule(repository=repository) |
982 | + self.assertEqual( |
983 | + "<GitRule 'refs/heads/*'> for %r" % repository, repr(rule)) |
984 | + |
985 | + def test_grants(self): |
986 | + rule = self.factory.makeGitRule() |
987 | + other_rule = self.factory.makeGitRule( |
988 | + repository=rule.repository, ref_pattern="refs/heads/stable/*") |
989 | + grantees = [self.factory.makePerson() for _ in range(2)] |
990 | + self.factory.makeGitRuleGrant( |
991 | + rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER, |
992 | + can_create=True) |
993 | + self.factory.makeGitRuleGrant( |
994 | + rule=rule, grantee=grantees[0], can_push=True) |
995 | + self.factory.makeGitRuleGrant( |
996 | + rule=rule, grantee=grantees[1], can_force_push=True) |
997 | + self.factory.makeGitRuleGrant( |
998 | + rule=other_rule, grantee=grantees[0], can_push=True) |
999 | + self.assertThat(rule.grants, MatchesSetwise( |
1000 | + MatchesStructure( |
1001 | + rule=Equals(rule), |
1002 | + grantee_type=Equals(GitGranteeType.REPOSITORY_OWNER), |
1003 | + grantee=Is(None), |
1004 | + can_create=Is(True), |
1005 | + can_push=Is(False), |
1006 | + can_force_push=Is(False)), |
1007 | + MatchesStructure( |
1008 | + rule=Equals(rule), |
1009 | + grantee_type=Equals(GitGranteeType.PERSON), |
1010 | + grantee=Equals(grantees[0]), |
1011 | + can_create=Is(False), |
1012 | + can_push=Is(True), |
1013 | + can_force_push=Is(False)), |
1014 | + MatchesStructure( |
1015 | + rule=Equals(rule), |
1016 | + grantee_type=Equals(GitGranteeType.PERSON), |
1017 | + grantee=Equals(grantees[1]), |
1018 | + can_create=Is(False), |
1019 | + can_push=Is(False), |
1020 | + can_force_push=Is(True)))) |
1021 | + |
1022 | + def test_destroySelf(self): |
1023 | + repository = self.factory.makeGitRepository() |
1024 | + rules = [ |
1025 | + self.factory.makeGitRule( |
1026 | + repository=repository, |
1027 | + ref_pattern=self.factory.getUniqueUnicode( |
1028 | + prefix="refs/heads/")) |
1029 | + for _ in range(4)] |
1030 | + self.assertEqual([0, 1, 2, 3], [rule.position for rule in rules]) |
1031 | + self.assertEqual(rules, list(repository.rules)) |
1032 | + with person_logged_in(repository.owner): |
1033 | + rules[1].destroySelf() |
1034 | + del rules[1] |
1035 | + self.assertEqual([0, 1, 2], [rule.position for rule in rules]) |
1036 | + self.assertEqual(rules, list(repository.rules)) |
1037 | + |
1038 | + def test_destroySelf_removes_grants(self): |
1039 | + repository = self.factory.makeGitRepository() |
1040 | + rule = self.factory.makeGitRule(repository=repository) |
1041 | + grant = self.factory.makeGitRuleGrant(rule=rule) |
1042 | + self.assertEqual([grant], list(repository.grants)) |
1043 | + with person_logged_in(repository.owner): |
1044 | + rule.destroySelf() |
1045 | + self.assertEqual([], list(repository.grants)) |
1046 | + |
1047 | + |
1048 | +class TestGitRuleGrant(TestCaseWithFactory): |
1049 | + |
1050 | + layer = DatabaseFunctionalLayer |
1051 | + |
1052 | + def test_implements_IGitRuleGrant(self): |
1053 | + grant = self.factory.makeGitRuleGrant() |
1054 | + verifyObject(IGitRuleGrant, grant) |
1055 | + |
1056 | + def test_properties_owner(self): |
1057 | + owner = self.factory.makeTeam() |
1058 | + member = self.factory.makePerson(member_of=[owner]) |
1059 | + rule = self.factory.makeGitRule(owner=owner) |
1060 | + grant = self.factory.makeGitRuleGrant( |
1061 | + rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER, grantor=member, |
1062 | + can_create=True, can_force_push=True) |
1063 | + now = get_transaction_timestamp(Store.of(grant)) |
1064 | + self.assertThat(grant, MatchesStructure( |
1065 | + repository=Equals(rule.repository), |
1066 | + rule=Equals(rule), |
1067 | + grantee_type=Equals(GitGranteeType.REPOSITORY_OWNER), |
1068 | + grantee=Is(None), |
1069 | + can_create=Is(True), |
1070 | + can_push=Is(False), |
1071 | + can_force_push=Is(True), |
1072 | + grantor=Equals(member), |
1073 | + date_created=Equals(now), |
1074 | + date_last_modified=Equals(now))) |
1075 | + |
1076 | + def test_properties_person(self): |
1077 | + owner = self.factory.makeTeam() |
1078 | + member = self.factory.makePerson(member_of=[owner]) |
1079 | + rule = self.factory.makeGitRule(owner=owner) |
1080 | + grantee = self.factory.makePerson() |
1081 | + grant = self.factory.makeGitRuleGrant( |
1082 | + rule=rule, grantee=grantee, grantor=member, can_push=True) |
1083 | + now = get_transaction_timestamp(Store.of(rule)) |
1084 | + self.assertThat(grant, MatchesStructure( |
1085 | + repository=Equals(rule.repository), |
1086 | + rule=Equals(rule), |
1087 | + grantee_type=Equals(GitGranteeType.PERSON), |
1088 | + grantee=Equals(grantee), |
1089 | + can_create=Is(False), |
1090 | + can_push=Is(True), |
1091 | + can_force_push=Is(False), |
1092 | + grantor=Equals(member), |
1093 | + date_created=Equals(now), |
1094 | + date_last_modified=Equals(now))) |
1095 | + |
1096 | + def test_repr_owner(self): |
1097 | + rule = self.factory.makeGitRule() |
1098 | + grant = self.factory.makeGitRuleGrant( |
1099 | + rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER, |
1100 | + can_create=True, can_push=True) |
1101 | + self.assertEqual( |
1102 | + "<GitRuleGrant [create, push] to repository owner> for %r" % rule, |
1103 | + repr(grant)) |
1104 | + |
1105 | + def test_repr_person(self): |
1106 | + rule = self.factory.makeGitRule() |
1107 | + grantee = self.factory.makePerson() |
1108 | + grant = self.factory.makeGitRuleGrant( |
1109 | + rule=rule, grantee=grantee, can_push=True) |
1110 | + self.assertEqual( |
1111 | + "<GitRuleGrant [push] to ~%s> for %r" % (grantee.name, rule), |
1112 | + repr(grant)) |
1113 | + |
1114 | + def test_destroySelf(self): |
1115 | + rule = self.factory.makeGitRule() |
1116 | + grants = [ |
1117 | + self.factory.makeGitRuleGrant( |
1118 | + rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER, |
1119 | + can_create=True), |
1120 | + self.factory.makeGitRuleGrant(rule=rule, can_push=True), |
1121 | + ] |
1122 | + with person_logged_in(rule.repository.owner): |
1123 | + grants[1].destroySelf() |
1124 | + self.assertThat(rule.grants, MatchesSetwise(Equals(grants[0]))) |
1125 | |
1126 | === modified file 'lib/lp/code/xmlrpc/git.py' |
1127 | --- lib/lp/code/xmlrpc/git.py 2018-08-28 13:58:37 +0000 |
1128 | +++ lib/lp/code/xmlrpc/git.py 2018-09-26 15:35:28 +0000 |
1129 | @@ -40,6 +40,7 @@ |
1130 | from lp.code.interfaces.gitjob import IGitRefScanJobSource |
1131 | from lp.code.interfaces.gitlookup import ( |
1132 | IGitLookup, |
1133 | + IGitRuleGrantLookup, |
1134 | IGitTraverser, |
1135 | ) |
1136 | from lp.code.interfaces.gitnamespace import ( |
1137 | @@ -325,3 +326,45 @@ |
1138 | else: |
1139 | # Only macaroons are supported for password authentication. |
1140 | return faults.Unauthorized() |
1141 | + |
1142 | + def _isRepositoryOwner(self, requester, repository): |
1143 | + try: |
1144 | + return requester.inTeam(repository.owner) |
1145 | + except Unauthorized: |
1146 | + return False |
1147 | + |
1148 | + def _listRefRules(self, requester, translated_path): |
1149 | + repository = getUtility(IGitLookup).getByHostingPath(translated_path) |
1150 | + grants = getUtility(IGitRuleGrantLookup).getByRulesAffectingPerson( |
1151 | + repository, requester) |
1152 | + |
1153 | + lines = [] |
1154 | + for grant in grants: |
1155 | + permissions = [] |
1156 | + if grant.can_create: |
1157 | + permissions.append("create") |
1158 | + if grant.can_push: |
1159 | + permissions.append("push") |
1160 | + if grant.can_force_push: |
1161 | + permissions.append("force-push") |
1162 | + lines.append( |
1163 | + {'ref_pattern': grant.rule.ref_pattern, |
1164 | + 'permissions': permissions}) |
1165 | + |
1166 | + if self._isRepositoryOwner(requester, repository): |
1167 | + lines.append({ |
1168 | + 'ref_pattern': '*', |
1169 | + 'permissions': ['create', 'push', 'force-push']}) |
1170 | + return lines |
1171 | + |
1172 | + def listRefRules(self, translated_path, auth_params): |
1173 | + """See `IGitAPI`""" |
1174 | + requester_id = auth_params.get("uid") |
1175 | + if requester_id is None: |
1176 | + requester_id = LAUNCHPAD_ANONYMOUS |
1177 | + |
1178 | + return run_with_login( |
1179 | + requester_id, |
1180 | + self._listRefRules, |
1181 | + translated_path, |
1182 | + ) |
1183 | |
1184 | === modified file 'lib/lp/code/xmlrpc/tests/test_git.py' |
1185 | --- lib/lp/code/xmlrpc/tests/test_git.py 2018-08-28 14:07:38 +0000 |
1186 | +++ lib/lp/code/xmlrpc/tests/test_git.py 2018-09-26 15:35:28 +0000 |
1187 | @@ -260,6 +260,125 @@ |
1188 | self.assertEqual( |
1189 | initial_count, getUtility(IAllGitRepositories).count()) |
1190 | |
1191 | + def test_listRefRules(self): |
1192 | + # Test that GitGrantRule (ref rule) can be retrieved for a user |
1193 | + requester = self.factory.makePerson() |
1194 | + repository = removeSecurityProxy( |
1195 | + self.factory.makeGitRepository( |
1196 | + owner=requester, information_type=InformationType.USERDATA)) |
1197 | + |
1198 | + rule = self.factory.makeGitRule(repository) |
1199 | + self.factory.makeGitRuleGrant( |
1200 | + rule=rule, grantee=requester, can_push=True, can_create=True) |
1201 | + |
1202 | + results = self.git_api.listRefRules( |
1203 | + repository.getInternalPath(), |
1204 | + {'uid': requester.id}) |
1205 | + self.assertEqual(len(results), 2) |
1206 | + self.assertEqual(results[0]['ref_pattern'], 'refs/heads/*') |
1207 | + self.assertEqual(results[0]['permissions'], ['create', 'push']) |
1208 | + |
1209 | + def test_listRefRules_no_grants(self): |
1210 | + # User that has no grants and is not the owner |
1211 | + requester = self.factory.makePerson() |
1212 | + owner = self.factory.makePerson() |
1213 | + repository = removeSecurityProxy( |
1214 | + self.factory.makeGitRepository( |
1215 | + owner=owner, information_type=InformationType.USERDATA)) |
1216 | + |
1217 | + rule = self.factory.makeGitRule(repository) |
1218 | + self.factory.makeGitRuleGrant( |
1219 | + rule=rule, grantee=owner, can_push=True, can_create=True) |
1220 | + |
1221 | + results = self.git_api.listRefRules( |
1222 | + repository.getInternalPath(), |
1223 | + {'uid': requester.id}) |
1224 | + self.assertEqual(len(results), 0) |
1225 | + |
1226 | + def test_listRefRules_owner_has_default(self): |
1227 | + owner = self.factory.makePerson() |
1228 | + repository = removeSecurityProxy( |
1229 | + self.factory.makeGitRepository( |
1230 | + owner=owner, information_type=InformationType.USERDATA)) |
1231 | + |
1232 | + rule = self.factory.makeGitRule( |
1233 | + repository=repository, ref_pattern=u'refs/heads/master') |
1234 | + self.factory.makeGitRuleGrant( |
1235 | + rule=rule, grantee=owner, can_push=True, can_create=True) |
1236 | + |
1237 | + results = self.git_api.listRefRules( |
1238 | + repository.getInternalPath(), |
1239 | + {'uid': owner.id}) |
1240 | + self.assertEqual(len(results), 2) |
1241 | + # Default grant should be last in pattern |
1242 | + self.assertEqual(results[-1]['ref_pattern'], '*') |
1243 | + |
1244 | + def test_listRefRules_owner_is_team(self): |
1245 | + member = self.factory.makePerson() |
1246 | + owner = self.factory.makeTeam(members=[member]) |
1247 | + repository = removeSecurityProxy( |
1248 | + self.factory.makeGitRepository( |
1249 | + owner=owner, information_type=InformationType.USERDATA)) |
1250 | + |
1251 | + results = self.git_api.listRefRules( |
1252 | + repository.getInternalPath(), |
1253 | + {'uid': member.id}) |
1254 | + |
1255 | + # Should have default grant as member of owning team |
1256 | + self.assertEqual(len(results), 1) |
1257 | + self.assertEqual(results[-1]['ref_pattern'], '*') |
1258 | + |
1259 | + def test_listRefRules_owner_is_team_with_grants(self): |
1260 | + member = self.factory.makePerson() |
1261 | + owner = self.factory.makeTeam(members=[member]) |
1262 | + repository = removeSecurityProxy( |
1263 | + self.factory.makeGitRepository( |
1264 | + owner=owner, information_type=InformationType.USERDATA)) |
1265 | + |
1266 | + rule = self.factory.makeGitRule( |
1267 | + repository=repository, ref_pattern=u'refs/heads/master') |
1268 | + self.factory.makeGitRuleGrant( |
1269 | + rule=rule, grantee=owner, can_push=True, can_create=True) |
1270 | + |
1271 | + results = self.git_api.listRefRules( |
1272 | + repository.getInternalPath(), |
1273 | + {'uid': member.id}) |
1274 | + |
1275 | + # Should have default grant as member of owning team |
1276 | + self.assertEqual(len(results), 2) |
1277 | + self.assertEqual(results[-1]['ref_pattern'], '*') |
1278 | + |
1279 | + def test_listRefRules_owner_is_team_with_grants_to_person(self): |
1280 | + member = self.factory.makePerson() |
1281 | + other_member = self.factory.makePerson() |
1282 | + owner = self.factory.makeTeam(members=[member, other_member]) |
1283 | + repository = removeSecurityProxy( |
1284 | + self.factory.makeGitRepository( |
1285 | + owner=owner, information_type=InformationType.USERDATA)) |
1286 | + |
1287 | + rule = self.factory.makeGitRule( |
1288 | + repository=repository, ref_pattern=u'refs/heads/master') |
1289 | + self.factory.makeGitRuleGrant( |
1290 | + rule=rule, grantee=owner, can_push=True, can_create=True) |
1291 | + |
1292 | + rule = self.factory.makeGitRule( |
1293 | + repository=repository, ref_pattern=u'refs/heads/tags') |
1294 | + self.factory.makeGitRuleGrant( |
1295 | + rule=rule, grantee=member, can_create=True) |
1296 | + |
1297 | + # This should not appear |
1298 | + self.factory.makeGitRuleGrant( |
1299 | + rule=rule, grantee=other_member, can_push=True) |
1300 | + |
1301 | + results = self.git_api.listRefRules( |
1302 | + repository.getInternalPath(), |
1303 | + {'uid': member.id}) |
1304 | + |
1305 | + # Should have default grant as member of owning team |
1306 | + self.assertEqual(len(results), 3) |
1307 | + self.assertEqual(results[-1]['ref_pattern'], '*') |
1308 | + tags_rule = results[1] |
1309 | + self.assertEqual(tags_rule['permissions'], ['create']) |
1310 | |
1311 | class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory): |
1312 | """Tests for the implementation of `IGitAPI`.""" |
1313 | |
1314 | === modified file 'lib/lp/registry/personmerge.py' |
1315 | --- lib/lp/registry/personmerge.py 2015-09-16 13:30:33 +0000 |
1316 | +++ lib/lp/registry/personmerge.py 2018-09-26 15:35:28 +0000 |
1317 | @@ -1,4 +1,4 @@ |
1318 | -# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
1319 | +# Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
1320 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1321 | |
1322 | """Person/team merger implementation.""" |
1323 | @@ -141,6 +141,25 @@ |
1324 | ''' % vars()) |
1325 | |
1326 | |
1327 | +def _mergeGitRuleGrant(cur, from_id, to_id): |
1328 | + # Update only the GitRuleGrants that will not conflict. |
1329 | + cur.execute(''' |
1330 | + UPDATE GitRuleGrant |
1331 | + SET grantee=%(to_id)d |
1332 | + WHERE |
1333 | + grantee = %(from_id)d |
1334 | + AND rule NOT IN ( |
1335 | + SELECT rule |
1336 | + FROM GitRuleGrant |
1337 | + WHERE grantee = %(to_id)d |
1338 | + ) |
1339 | + ''' % vars()) |
1340 | + # and delete those left over. |
1341 | + cur.execute(''' |
1342 | + DELETE FROM GitRuleGrant WHERE grantee = %(from_id)d |
1343 | + ''' % vars()) |
1344 | + |
1345 | + |
1346 | def _mergeBranches(from_person, to_person): |
1347 | # This shouldn't use removeSecurityProxy. |
1348 | branches = getUtility(IBranchCollection).ownedBy(from_person) |
1349 | @@ -771,8 +790,10 @@ |
1350 | |
1351 | _mergeAccessArtifactGrant(cur, from_id, to_id) |
1352 | _mergeAccessPolicyGrant(cur, from_id, to_id) |
1353 | + _mergeGitRuleGrant(cur, from_id, to_id) |
1354 | skip.append(('accessartifactgrant', 'grantee')) |
1355 | skip.append(('accesspolicygrant', 'grantee')) |
1356 | + skip.append(('gitrulegrant', 'grantee')) |
1357 | |
1358 | # Update the Branches that will not conflict, and fudge the names of |
1359 | # ones that *do* conflict. |
1360 | |
1361 | === modified file 'lib/lp/registry/tests/test_personmerge.py' |
1362 | --- lib/lp/registry/tests/test_personmerge.py 2016-06-28 21:10:18 +0000 |
1363 | +++ lib/lp/registry/tests/test_personmerge.py 2018-09-26 15:35:28 +0000 |
1364 | @@ -1,4 +1,4 @@ |
1365 | -# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
1366 | +# Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
1367 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1368 | |
1369 | """Tests for merge_people.""" |
1370 | @@ -494,6 +494,41 @@ |
1371 | grantee=person, |
1372 | date_created=person_grant_date)) |
1373 | |
1374 | + def test_merge_gitrulegrants(self): |
1375 | + # GitRuleGrants are transferred from the duplicate. |
1376 | + rule = self.factory.makeGitRule() |
1377 | + person = self.factory.makePerson() |
1378 | + grant = self.factory.makeGitRuleGrant(rule=rule) |
1379 | + self._do_premerge(grant.grantee, person) |
1380 | + |
1381 | + self.assertEqual(grant.grantee, rule.grants.one().grantee) |
1382 | + with person_logged_in(person): |
1383 | + self._do_merge(grant.grantee, person) |
1384 | + self.assertEqual(person, rule.grants.one().grantee) |
1385 | + |
1386 | + def test_merge_gitrulegrants_conflicts(self): |
1387 | + # Conflicting GitRuleGrants are deleted. |
1388 | + rule = self.factory.makeGitRule() |
1389 | + |
1390 | + person = self.factory.makePerson() |
1391 | + person_grant = self.factory.makeGitRuleGrant(rule=rule, grantee=person) |
1392 | + person_grant_date = person_grant.date_created |
1393 | + |
1394 | + duplicate = self.factory.makePerson() |
1395 | + self.factory.makeGitRuleGrant(rule=rule, grantee=duplicate) |
1396 | + |
1397 | + self._do_premerge(duplicate, person) |
1398 | + with person_logged_in(person): |
1399 | + self._do_merge(duplicate, person) |
1400 | + |
1401 | + # Only one grant for the rule exists: the retained person's. |
1402 | + self.assertThat( |
1403 | + rule.grants.one(), |
1404 | + MatchesStructure.byEquality( |
1405 | + rule=rule, |
1406 | + grantee=person, |
1407 | + date_created=person_grant_date)) |
1408 | + |
1409 | def test_mergeAsync(self): |
1410 | # mergeAsync() creates a new `PersonMergeJob`. |
1411 | from_person = self.factory.makePerson() |
1412 | |
1413 | === modified file 'lib/lp/security.py' |
1414 | --- lib/lp/security.py 2018-09-15 11:38:45 +0000 |
1415 | +++ lib/lp/security.py 2018-09-26 15:35:28 +0000 |
1416 | @@ -87,6 +87,10 @@ |
1417 | IGitRepository, |
1418 | user_has_special_git_repository_access, |
1419 | ) |
1420 | +from lp.code.interfaces.gitrule import ( |
1421 | + IGitRule, |
1422 | + IGitRuleGrant, |
1423 | + ) |
1424 | from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe |
1425 | from lp.code.interfaces.sourcepackagerecipebuild import ( |
1426 | ISourcePackageRecipeBuild, |
1427 | @@ -2343,6 +2347,42 @@ |
1428 | super(EditGitRef, self).__init__(obj, obj.repository) |
1429 | |
1430 | |
1431 | +class ViewGitRule(DelegatedAuthorization): |
1432 | + """Anyone who can see a Git repository can see its access rules.""" |
1433 | + permission = 'launchpad.View' |
1434 | + usedfor = IGitRule |
1435 | + |
1436 | + def __init__(self, obj): |
1437 | + super(ViewGitRule, self).__init__(obj, obj.repository) |
1438 | + |
1439 | + |
1440 | +class EditGitRule(DelegatedAuthorization): |
1441 | + """Anyone who can edit a Git repository can edit its access rules.""" |
1442 | + permission = 'launchpad.Edit' |
1443 | + usedfor = IGitRule |
1444 | + |
1445 | + def __init__(self, obj): |
1446 | + super(EditGitRule, self).__init__(obj, obj.repository) |
1447 | + |
1448 | + |
1449 | +class ViewGitRuleGrant(DelegatedAuthorization): |
1450 | + """Anyone who can see a Git repository can see its access grants.""" |
1451 | + permission = 'launchpad.View' |
1452 | + usedfor = IGitRuleGrant |
1453 | + |
1454 | + def __init__(self, obj): |
1455 | + super(ViewGitRuleGrant, self).__init__(obj, obj.repository) |
1456 | + |
1457 | + |
1458 | +class EditGitRuleGrant(DelegatedAuthorization): |
1459 | + """Anyone who can edit a Git repository can edit its access grants.""" |
1460 | + permission = 'launchpad.Edit' |
1461 | + usedfor = IGitRuleGrant |
1462 | + |
1463 | + def __init__(self, obj): |
1464 | + super(EditGitRuleGrant, self).__init__(obj, obj.repository) |
1465 | + |
1466 | + |
1467 | class AdminDistroSeriesTranslations(AuthorizationBase): |
1468 | permission = 'launchpad.TranslationsAdmin' |
1469 | usedfor = IDistroSeries |
1470 | |
1471 | === modified file 'lib/lp/testing/factory.py' |
1472 | --- lib/lp/testing/factory.py 2018-09-13 15:21:05 +0000 |
1473 | +++ lib/lp/testing/factory.py 2018-09-26 15:35:28 +0000 |
1474 | @@ -1828,6 +1828,31 @@ |
1475 | path = self.getUniqueString('refs/heads/path').decode('utf-8') |
1476 | return getUtility(IGitRefRemoteSet).new(repository_url, path) |
1477 | |
1478 | + def makeGitRule(self, repository=None, ref_pattern=u"refs/heads/*", |
1479 | + creator=None, position=None, **repository_kwargs): |
1480 | + """Create a Git repository access rule.""" |
1481 | + if repository is None: |
1482 | + repository = self.makeGitRepository(**repository_kwargs) |
1483 | + if creator is None: |
1484 | + creator = repository.owner |
1485 | + with person_logged_in(creator): |
1486 | + return repository.addRule(ref_pattern, creator, position=position) |
1487 | + |
1488 | + def makeGitRuleGrant(self, rule=None, grantee=None, grantor=None, |
1489 | + can_create=False, can_push=False, |
1490 | + can_force_push=False): |
1491 | + """Create a Git repository access grant.""" |
1492 | + if rule is None: |
1493 | + rule = self.makeGitRule() |
1494 | + if grantee is None: |
1495 | + grantee = self.makePerson() |
1496 | + if grantor is None: |
1497 | + grantor = rule.repository.owner |
1498 | + with person_logged_in(grantor): |
1499 | + return rule.addGrant( |
1500 | + grantee, grantor, can_create=can_create, can_push=can_push, |
1501 | + can_force_push=can_force_push) |
1502 | + |
1503 | def makeBug(self, target=None, owner=None, bug_watch_url=None, |
1504 | information_type=None, date_closed=None, title=None, |
1505 | date_created=None, description=None, comment=None, |
1506 | |
1507 | === modified file 'scripts/close-account.py' |
1508 | --- scripts/close-account.py 2018-05-08 13:44:21 +0000 |
1509 | +++ scripts/close-account.py 2018-09-26 15:35:28 +0000 |
1510 | @@ -1,6 +1,6 @@ |
1511 | #!/usr/bin/python -S |
1512 | # |
1513 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
1514 | +# Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
1515 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1516 | |
1517 | """Remove personal details of a user from the database, leaving a stub.""" |
1518 | @@ -162,6 +162,9 @@ |
1519 | |
1520 | # Pending items in queues |
1521 | ('POExportRequest', 'person'), |
1522 | + |
1523 | + # Access grants |
1524 | + ('GitRuleGrant', 'grantee'), |
1525 | ] |
1526 | for table, person_id_column in removals: |
1527 | table_notification(table) |