Merge lp:~cjwatson/launchpad/git-permissions-webservice-ref into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18798
Proposed branch: lp:~cjwatson/launchpad/git-permissions-webservice-ref
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-grant-limitedview
Diff against target: 1291 lines (+875/-24)
12 files modified
lib/lp/_schema_circular_imports.py (+4/-0)
lib/lp/app/webservice/marshallers.py (+62/-2)
lib/lp/app/webservice/tests/test_marshallers.py (+77/-3)
lib/lp/code/configure.zcml (+21/-4)
lib/lp/code/interfaces/gitref.py (+41/-8)
lib/lp/code/interfaces/gitrule.py (+26/-0)
lib/lp/code/model/gitref.py (+32/-1)
lib/lp/code/model/gitrule.py (+101/-3)
lib/lp/code/model/tests/test_gitref.py (+189/-0)
lib/lp/code/model/tests/test_gitrule.py (+301/-0)
lib/lp/services/fields/__init__.py (+14/-2)
lib/lp/services/webservice/configure.zcml (+7/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-permissions-webservice-ref
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+355608@code.launchpad.net

Commit message

Allow getting and setting grants for a single Git ref over the webservice.

Description of the change

Getting permissions is limited to people who can edit the repository; this is perhaps not a strictly necessary restriction, but it avoids needing to grant excessive LimitedView if somebody grants a private team access to a public repository.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) wrote :

I've applied most of your suggestions; thanks.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/_schema_circular_imports.py'
--- lib/lp/_schema_circular_imports.py 2018-08-23 17:03:05 +0000
+++ lib/lp/_schema_circular_imports.py 2018-10-16 15:29:23 +0000
@@ -68,6 +68,7 @@
68from lp.code.interfaces.diff import IPreviewDiff68from lp.code.interfaces.diff import IPreviewDiff
69from lp.code.interfaces.gitref import IGitRef69from lp.code.interfaces.gitref import IGitRef
70from lp.code.interfaces.gitrepository import IGitRepository70from lp.code.interfaces.gitrepository import IGitRepository
71from lp.code.interfaces.gitrule import IGitNascentRuleGrant
71from lp.code.interfaces.gitsubscription import IGitSubscription72from lp.code.interfaces.gitsubscription import IGitSubscription
72from lp.code.interfaces.hasbranches import (73from lp.code.interfaces.hasbranches import (
73 IHasBranches,74 IHasBranches,
@@ -150,6 +151,7 @@
150from lp.registry.interfaces.teammembership import ITeamMembership151from lp.registry.interfaces.teammembership import ITeamMembership
151from lp.registry.interfaces.wikiname import IWikiName152from lp.registry.interfaces.wikiname import IWikiName
152from lp.services.comments.interfaces.conversation import IComment153from lp.services.comments.interfaces.conversation import IComment
154from lp.services.fields import InlineObject
153from lp.services.messages.interfaces.message import (155from lp.services.messages.interfaces.message import (
154 IIndexedMessage,156 IIndexedMessage,
155 IMessage,157 IMessage,
@@ -506,6 +508,8 @@
506patch_entry_return_type(IGitRef, 'createMergeProposal', IBranchMergeProposal)508patch_entry_return_type(IGitRef, 'createMergeProposal', IBranchMergeProposal)
507patch_collection_return_type(509patch_collection_return_type(
508 IGitRef, 'getMergeProposals', IBranchMergeProposal)510 IGitRef, 'getMergeProposals', IBranchMergeProposal)
511patch_list_parameter_type(
512 IGitRef, 'setGrants', 'grants', InlineObject(schema=IGitNascentRuleGrant))
509513
510# IGitRepository514# IGitRepository
511patch_collection_property(IGitRepository, 'branches', IGitRef)515patch_collection_property(IGitRepository, 'branches', IGitRef)
512516
=== modified file 'lib/lp/app/webservice/marshallers.py'
--- lib/lp/app/webservice/marshallers.py 2012-01-01 02:58:52 +0000
+++ lib/lp/app/webservice/marshallers.py 2018-10-16 15:29:23 +0000
@@ -1,4 +1,4 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the1# Copyright 2011-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Launchpad-specific field marshallers for the web service."""4"""Launchpad-specific field marshallers for the web service."""
@@ -10,10 +10,23 @@
10 ]10 ]
1111
1212
13from lazr.restful.interfaces import (
14 IEntry,
15 IFieldMarshaller,
16 )
13from lazr.restful.marshallers import (17from lazr.restful.marshallers import (
18 SimpleFieldMarshaller,
14 TextFieldMarshaller as LazrTextFieldMarshaller,19 TextFieldMarshaller as LazrTextFieldMarshaller,
15 )20 )
16from zope.component import getUtility21from zope.component import (
22 getMultiAdapter,
23 getUtility,
24 )
25from zope.component.interfaces import ComponentLookupError
26from zope.schema.interfaces import (
27 IField,
28 RequiredMissing,
29 )
1730
18from lp.services.utils import obfuscate_email31from lp.services.utils import obfuscate_email
19from lp.services.webapp.interfaces import ILaunchBag32from lp.services.webapp.interfaces import ILaunchBag
@@ -31,3 +44,50 @@
31 if (value is not None and getUtility(ILaunchBag).user is None):44 if (value is not None and getUtility(ILaunchBag).user is None):
32 return obfuscate_email(value)45 return obfuscate_email(value)
33 return value46 return value
47
48
49class InlineObjectFieldMarshaller(SimpleFieldMarshaller):
50 """A marshaller that represents an object as a dict.
51
52 lazr.restful represents objects as URL references by default, but that
53 isn't what we want in all cases.
54
55 To use this marshaller to read JSON input data, you must register an
56 adapter from the expected top-level type of the loaded JSON data
57 (usually `dict`) to the `InlineObject` field's schema. The adapter will
58 be called with the deserialised input data, with all inner fields
59 already converted as indicated by the schema.
60 """
61
62 def unmarshall(self, entry, value):
63 """See `IFieldMarshaller`."""
64 result = {}
65 for name in self.field.schema.names(all=True):
66 field = self.field.schema[name]
67 if IField.providedBy(field):
68 marshaller = getMultiAdapter(
69 (field, self.request), IFieldMarshaller)
70 sub_value = getattr(value, name, field.default)
71 try:
72 sub_entry = getMultiAdapter(
73 (sub_value, self.request), IEntry)
74 except ComponentLookupError:
75 sub_entry = entry
76 result[marshaller.representation_name] = marshaller.unmarshall(
77 sub_entry, sub_value)
78 return result
79
80 def _marshall_from_json_data(self, value):
81 """See `SimpleFieldMarshaller`."""
82 template = {}
83 for name in self.field.schema.names(all=True):
84 field = self.field.schema[name]
85 if IField.providedBy(field):
86 marshaller = getMultiAdapter(
87 (field, self.request), IFieldMarshaller)
88 if marshaller.representation_name in value:
89 template[name] = marshaller.marshall_from_json_data(
90 value[marshaller.representation_name])
91 elif field.required:
92 raise RequiredMissing(name)
93 return self.field.schema(template)
3494
=== modified file 'lib/lp/app/webservice/tests/test_marshallers.py'
--- lib/lp/app/webservice/tests/test_marshallers.py 2012-01-01 02:58:52 +0000
+++ lib/lp/app/webservice/tests/test_marshallers.py 2018-10-16 15:29:23 +0000
@@ -1,19 +1,40 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the1# Copyright 2011-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for the webservice marshallers."""4"""Tests for the webservice marshallers."""
55
6__metaclass__ = type6__metaclass__ = type
77
8from testtools.matchers import (
9 Equals,
10 MatchesDict,
11 MatchesStructure,
12 )
8import transaction13import transaction
14from zope.component import adapter
15from zope.interface import (
16 implementer,
17 Interface,
18 )
19from zope.schema import Choice
920
10from lp.app.webservice.marshallers import TextFieldMarshaller21from lp.app.webservice.marshallers import (
22 InlineObjectFieldMarshaller,
23 TextFieldMarshaller,
24 )
25from lp.services.fields import (
26 InlineObject,
27 PersonChoice,
28 )
29from lp.services.job.interfaces.job import JobStatus
30from lp.services.webapp.publisher import canonical_url
11from lp.services.webapp.servers import WebServiceTestRequest31from lp.services.webapp.servers import WebServiceTestRequest
12from lp.testing import (32from lp.testing import (
13 logout,33 logout,
14 person_logged_in,34 person_logged_in,
15 TestCaseWithFactory,35 TestCaseWithFactory,
16 )36 )
37from lp.testing.fixture import ZopeAdapterFixture
17from lp.testing.layers import DatabaseFunctionalLayer38from lp.testing.layers import DatabaseFunctionalLayer
18from lp.testing.pages import (39from lp.testing.pages import (
19 LaunchpadWebServiceCaller,40 LaunchpadWebServiceCaller,
@@ -37,7 +58,7 @@
37 self.assertEqual(u"<email address hidden>", result)58 self.assertEqual(u"<email address hidden>", result)
3859
39 def test_unmarshall_not_obfuscated(self):60 def test_unmarshall_not_obfuscated(self):
40 # Data is not obfuccated if the user is authenticated.61 # Data is not obfuscated if the user is authenticated.
41 marshaller = TextFieldMarshaller(None, WebServiceTestRequest())62 marshaller = TextFieldMarshaller(None, WebServiceTestRequest())
42 with person_logged_in(self.factory.makePerson()):63 with person_logged_in(self.factory.makePerson()):
43 result = marshaller.unmarshall(None, u"foo@example.com")64 result = marshaller.unmarshall(None, u"foo@example.com")
@@ -128,3 +149,56 @@
128 webservice = LaunchpadWebServiceCaller()149 webservice = LaunchpadWebServiceCaller()
129 etag_logged_out = webservice(ws_url(bug)).getheader('etag')150 etag_logged_out = webservice(ws_url(bug)).getheader('etag')
130 self.assertNotEqual(etag_logged_in, etag_logged_out)151 self.assertNotEqual(etag_logged_in, etag_logged_out)
152
153
154class IInlineExample(Interface):
155
156 person = PersonChoice(vocabulary="ValidPersonOrTeam")
157
158 status = Choice(vocabulary=JobStatus)
159
160
161@implementer(IInlineExample)
162class InlineExample:
163
164 def __init__(self, person, status):
165 self.person = person
166 self.status = status
167
168
169@adapter(dict)
170@implementer(IInlineExample)
171def inline_example_from_dict(template):
172 return InlineExample(**template)
173
174
175class TestInlineObjectFieldMarshaller(TestCaseWithFactory):
176
177 layer = DatabaseFunctionalLayer
178
179 def test_unmarshall(self):
180 field = InlineObject(schema=IInlineExample)
181 request = WebServiceTestRequest()
182 request.setVirtualHostRoot(names=["devel"])
183 marshaller = InlineObjectFieldMarshaller(field, request)
184 obj = InlineExample(self.factory.makePerson(), JobStatus.WAITING)
185 result = marshaller.unmarshall(None, obj)
186 self.assertThat(result, MatchesDict({
187 "person_link": Equals(canonical_url(obj.person, request=request)),
188 "status": Equals("Waiting"),
189 }))
190
191 def test_marshall_from_json_data(self):
192 self.useFixture(ZopeAdapterFixture(inline_example_from_dict))
193 field = InlineObject(schema=IInlineExample)
194 request = WebServiceTestRequest()
195 request.setVirtualHostRoot(names=["devel"])
196 marshaller = InlineObjectFieldMarshaller(field, request)
197 person = self.factory.makePerson()
198 data = {
199 "person_link": canonical_url(person, request=request),
200 "status": "Running",
201 }
202 obj = marshaller.marshall_from_json_data(data)
203 self.assertThat(obj, MatchesStructure.byEquality(
204 person=person, status=JobStatus.RUNNING))
131205
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2018-10-15 14:44:25 +0000
+++ lib/lp/code/configure.zcml 2018-10-16 15:29:23 +0000
@@ -896,22 +896,34 @@
896 <class class="lp.code.model.gitref.GitRef">896 <class class="lp.code.model.gitref.GitRef">
897 <require897 <require
898 permission="launchpad.View"898 permission="launchpad.View"
899 interface="lp.code.interfaces.gitref.IGitRef" />899 interface="lp.code.interfaces.gitref.IGitRefView" />
900 <require
901 permission="launchpad.Edit"
902 interface="lp.code.interfaces.gitref.IGitRefEdit" />
900 </class>903 </class>
901 <class class="lp.code.model.gitref.GitRefDefault">904 <class class="lp.code.model.gitref.GitRefDefault">
902 <require905 <require
903 permission="launchpad.View"906 permission="launchpad.View"
904 interface="lp.code.interfaces.gitref.IGitRef" />907 interface="lp.code.interfaces.gitref.IGitRefView" />
908 <require
909 permission="launchpad.Edit"
910 interface="lp.code.interfaces.gitref.IGitRefEdit" />
905 </class>911 </class>
906 <class class="lp.code.model.gitref.GitRefFrozen">912 <class class="lp.code.model.gitref.GitRefFrozen">
907 <require913 <require
908 permission="launchpad.View"914 permission="launchpad.View"
909 interface="lp.code.interfaces.gitref.IGitRef" />915 interface="lp.code.interfaces.gitref.IGitRefView" />
916 <require
917 permission="launchpad.Edit"
918 interface="lp.code.interfaces.gitref.IGitRefEdit" />
910 </class>919 </class>
911 <class class="lp.code.model.gitref.GitRefRemote">920 <class class="lp.code.model.gitref.GitRefRemote">
912 <require921 <require
913 permission="launchpad.View"922 permission="launchpad.View"
914 interface="lp.code.interfaces.gitref.IGitRef" />923 interface="lp.code.interfaces.gitref.IGitRefView" />
924 <require
925 permission="launchpad.Edit"
926 interface="lp.code.interfaces.gitref.IGitRefEdit" />
915 </class>927 </class>
916 <securedutility928 <securedutility
917 component="lp.code.model.gitref.GitRefRemote"929 component="lp.code.model.gitref.GitRefRemote"
@@ -943,10 +955,15 @@
943 permission="launchpad.Edit"955 permission="launchpad.Edit"
944 interface="lp.code.interfaces.gitrule.IGitRuleGrantEdit"956 interface="lp.code.interfaces.gitrule.IGitRuleGrantEdit"
945 set_schema="lp.code.interfaces.gitrule.IGitRuleGrantEditableAttributes" />957 set_schema="lp.code.interfaces.gitrule.IGitRuleGrantEditableAttributes" />
958 <allow interface="lazr.restful.interfaces.IJSONPublishable" />
946 </class>959 </class>
947 <subscriber960 <subscriber
948 for="lp.code.interfaces.gitrule.IGitRuleGrant zope.lifecycleevent.interfaces.IObjectModifiedEvent"961 for="lp.code.interfaces.gitrule.IGitRuleGrant zope.lifecycleevent.interfaces.IObjectModifiedEvent"
949 handler="lp.code.model.gitrule.git_rule_grant_modified"/>962 handler="lp.code.model.gitrule.git_rule_grant_modified"/>
963 <class class="lp.code.model.gitrule.GitNascentRuleGrant">
964 <allow interface="lp.code.interfaces.gitrule.IGitNascentRuleGrant" />
965 </class>
966 <adapter factory="lp.code.model.gitrule.nascent_rule_grant_from_dict" />
950967
951 <!-- GitActivity -->968 <!-- GitActivity -->
952969
953970
=== modified file 'lib/lp/code/interfaces/gitref.py'
--- lib/lp/code/interfaces/gitref.py 2018-08-20 23:33:01 +0000
+++ lib/lp/code/interfaces/gitref.py 2018-10-16 15:29:23 +0000
@@ -16,6 +16,7 @@
16 export_as_webservice_entry,16 export_as_webservice_entry,
17 export_factory_operation,17 export_factory_operation,
18 export_read_operation,18 export_read_operation,
19 export_write_operation,
19 exported,20 exported,
20 operation_for_version,21 operation_for_version,
21 operation_parameters,22 operation_parameters,
@@ -50,16 +51,12 @@
50from lp.code.interfaces.hasbranches import IHasMergeProposals51from lp.code.interfaces.hasbranches import IHasMergeProposals
51from lp.code.interfaces.hasrecipes import IHasRecipes52from lp.code.interfaces.hasrecipes import IHasRecipes
52from lp.registry.interfaces.person import IPerson53from lp.registry.interfaces.person import IPerson
54from lp.services.fields import InlineObject
53from lp.services.webapp.interfaces import ITableBatchNavigator55from lp.services.webapp.interfaces import ITableBatchNavigator
5456
5557
56class IGitRef(IHasMergeProposals, IHasRecipes, IPrivacy, IInformationType):58class IGitRefView(IHasMergeProposals, IHasRecipes, IPrivacy, IInformationType):
57 """A reference in a Git repository."""59 """IGitRef attributes that require launchpad.View permission."""
58
59 # XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL
60 # generation working. Individual attributes must set their version to
61 # "devel".
62 export_as_webservice_entry(as_of="beta")
6360
64 repository = exported(ReferenceChoice(61 repository = exported(ReferenceChoice(
65 title=_("Repository"), required=True, readonly=True,62 title=_("Repository"), required=True, readonly=True,
@@ -119,7 +116,7 @@
119116
120 commit_message_first_line = TextLine(117 commit_message_first_line = TextLine(
121 title=_("The first line of the commit message."),118 title=_("The first line of the commit message."),
122 required=True, readonly=True)119 required=False, readonly=True)
123120
124 identity = Attribute(121 identity = Attribute(
125 "The identity of this reference. This will be the shortened path to "122 "The identity of this reference. This will be the shortened path to "
@@ -392,6 +389,42 @@
392 """389 """
393390
394391
392class IGitRefEdit(Interface):
393 """IGitRef methods that require launchpad.Edit permission."""
394
395 @export_read_operation()
396 @operation_for_version("devel")
397 def getGrants():
398 """Get the access grants specific to this reference.
399
400 Other grants may apply via wildcard rules.
401 """
402
403 @operation_parameters(
404 grants=List(
405 title=_("Grants"),
406 # Really IGitNascentRuleGrant, patched in
407 # _schema_circular_imports.py.
408 value_type=InlineObject(schema=Interface)))
409 @call_with(user=REQUEST_USER)
410 @export_write_operation()
411 @operation_for_version("devel")
412 def setGrants(grants, user):
413 """Set the access grants specific to this reference.
414
415 Other grants may apply via wildcard rules.
416 """
417
418
419class IGitRef(IGitRefView, IGitRefEdit):
420 """A reference in a Git repository."""
421
422 # XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL
423 # generation working. Individual attributes must set their version to
424 # "devel".
425 export_as_webservice_entry(as_of="beta")
426
427
395class IGitRefBatchNavigator(ITableBatchNavigator):428class IGitRefBatchNavigator(ITableBatchNavigator):
396 pass429 pass
397430
398431
=== modified file 'lib/lp/code/interfaces/gitrule.py'
--- lib/lp/code/interfaces/gitrule.py 2018-10-12 16:41:14 +0000
+++ lib/lp/code/interfaces/gitrule.py 2018-10-16 15:29:23 +0000
@@ -7,11 +7,13 @@
77
8__metaclass__ = type8__metaclass__ = type
9__all__ = [9__all__ = [
10 'IGitNascentRuleGrant',
10 'IGitRule',11 'IGitRule',
11 'IGitRuleGrant',12 'IGitRuleGrant',
12 ]13 ]
1314
14from lazr.restful.fields import Reference15from lazr.restful.fields import Reference
16from lazr.restful.interface import copy_field
15from zope.interface import (17from zope.interface import (
16 Attribute,18 Attribute,
17 Interface,19 Interface,
@@ -100,6 +102,9 @@
100 matching this rule.102 matching this rule.
101 """103 """
102104
105 def setGrants(grants, user):
106 """Set the access grants for this rule."""
107
103 def destroySelf(user):108 def destroySelf(user):
104 """Delete this rule.109 """Delete this rule.
105110
@@ -183,3 +188,24 @@
183class IGitRuleGrant(IGitRuleGrantView, IGitRuleGrantEditableAttributes,188class IGitRuleGrant(IGitRuleGrantView, IGitRuleGrantEditableAttributes,
184 IGitRuleGrantEdit):189 IGitRuleGrantEdit):
185 """An access grant for a Git repository rule."""190 """An access grant for a Git repository rule."""
191
192
193class IGitNascentRuleGrant(Interface):
194 """An access grant in the process of being created.
195
196 This represents parameters for a grant that have been deserialised from
197 a webservice request, but that have not yet been attached to a rule.
198 """
199
200 grantee_type = copy_field(IGitRuleGrant["grantee_type"])
201
202 grantee = copy_field(IGitRuleGrant["grantee"])
203
204 can_create = copy_field(
205 IGitRuleGrant["can_create"], required=False, default=False)
206
207 can_push = copy_field(
208 IGitRuleGrant["can_push"], required=False, default=False)
209
210 can_force_push = copy_field(
211 IGitRuleGrant["can_force_push"], required=False, default=False)
186212
=== modified file 'lib/lp/code/model/gitref.py'
--- lib/lp/code/model/gitref.py 2018-09-27 13:50:06 +0000
+++ lib/lp/code/model/gitref.py 2018-10-16 15:29:23 +0000
@@ -69,6 +69,10 @@
69 BranchMergeProposal,69 BranchMergeProposal,
70 BranchMergeProposalGetter,70 BranchMergeProposalGetter,
71 )71 )
72from lp.code.model.gitrule import (
73 GitRule,
74 GitRuleGrant,
75 )
72from lp.services.config import config76from lp.services.config import config
73from lp.services.database.constants import UTC_NOW77from lp.services.database.constants import UTC_NOW
74from lp.services.database.decoratedresultset import DecoratedResultSet78from lp.services.database.decoratedresultset import DecoratedResultSet
@@ -421,6 +425,24 @@
421 hook = SourcePackageRecipe.preLoadDataForSourcePackageRecipes425 hook = SourcePackageRecipe.preLoadDataForSourcePackageRecipes
422 return DecoratedResultSet(recipes, pre_iter_hook=hook)426 return DecoratedResultSet(recipes, pre_iter_hook=hook)
423427
428 def getGrants(self):
429 """See `IGitRef`."""
430 return list(Store.of(self).find(
431 GitRuleGrant, GitRuleGrant.rule_id == GitRule.id,
432 GitRule.repository_id == self.repository_id,
433 GitRule.ref_pattern == self.path))
434
435 def setGrants(self, grants, user):
436 """See `IGitRef`."""
437 rule = Store.of(self).find(
438 GitRule, GitRule.repository_id == self.repository_id,
439 GitRule.ref_pattern == self.path).one()
440 if rule is None:
441 # We don't need to worry about position, since this is an
442 # exact-match rule and therefore has a canonical position.
443 rule = self.repository.addRule(self.path, user)
444 rule.setGrants(grants, user)
445
424446
425@implementer(IGitRef)447@implementer(IGitRef)
426class GitRef(StormBase, GitRefMixin):448class GitRef(StormBase, GitRefMixin):
@@ -452,7 +474,10 @@
452474
453 @property475 @property
454 def commit_message_first_line(self):476 def commit_message_first_line(self):
455 return self.commit_message.split("\n", 1)[0]477 if self.commit_message is not None:
478 return self.commit_message.split("\n", 1)[0]
479 else:
480 return None
456481
457 @property482 @property
458 def has_commits(self):483 def has_commits(self):
@@ -795,6 +820,12 @@
795 """See `IHasRecipes`."""820 """See `IHasRecipes`."""
796 return []821 return []
797822
823 def getGrants(self):
824 """See `IGitRef`."""
825 return []
826
827 setGrants = _unimplemented
828
798 def __eq__(self, other):829 def __eq__(self, other):
799 return (830 return (
800 self.repository_url == other.repository_url and831 self.repository_url == other.repository_url and
801832
=== modified file 'lib/lp/code/model/gitrule.py'
--- lib/lp/code/model/gitrule.py 2018-10-12 16:41:14 +0000
+++ lib/lp/code/model/gitrule.py 2018-10-16 15:29:23 +0000
@@ -11,7 +11,16 @@
11 'GitRuleGrant',11 'GitRuleGrant',
12 ]12 ]
1313
14from collections import OrderedDict
15
14from lazr.enum import DBItem16from lazr.enum import DBItem
17from lazr.lifecycle.event import ObjectModifiedEvent
18from lazr.lifecycle.snapshot import Snapshot
19from lazr.restful.interfaces import (
20 IFieldMarshaller,
21 IJSONPublishable,
22 )
23from lazr.restful.utils import get_current_browser_request
15import pytz24import pytz
16from storm.locals import (25from storm.locals import (
17 Bool,26 Bool,
@@ -21,13 +30,22 @@
21 Store,30 Store,
22 Unicode,31 Unicode,
23 )32 )
24from zope.component import getUtility33from zope.component import (
25from zope.interface import implementer34 adapter,
35 getMultiAdapter,
36 getUtility,
37 )
38from zope.event import notify
39from zope.interface import (
40 implementer,
41 providedBy,
42 )
26from zope.security.proxy import removeSecurityProxy43from zope.security.proxy import removeSecurityProxy
2744
28from lp.code.enums import GitGranteeType45from lp.code.enums import GitGranteeType
29from lp.code.interfaces.gitactivity import IGitActivitySet46from lp.code.interfaces.gitactivity import IGitActivitySet
30from lp.code.interfaces.gitrule import (47from lp.code.interfaces.gitrule import (
48 IGitNascentRuleGrant,
31 IGitRule,49 IGitRule,
32 IGitRuleGrant,50 IGitRuleGrant,
33 )51 )
@@ -42,6 +60,7 @@
42 )60 )
43from lp.services.database.enumcol import DBEnum61from lp.services.database.enumcol import DBEnum
44from lp.services.database.stormbase import StormBase62from lp.services.database.stormbase import StormBase
63from lp.services.fields import InlineObject
4564
4665
47def git_rule_modified(rule, event):66def git_rule_modified(rule, event):
@@ -118,6 +137,58 @@
118 getUtility(IGitActivitySet).logGrantAdded(grant, grantor)137 getUtility(IGitActivitySet).logGrantAdded(grant, grantor)
119 return grant138 return grant
120139
140 def _validateGrants(self, grants):
141 """Validate a new iterable of access grants."""
142 for grant in grants:
143 if grant.grantee_type == GitGranteeType.PERSON:
144 if grant.grantee is None:
145 raise ValueError(
146 "Permission grant for %s has grantee_type 'Person' "
147 "but no grantee" % self.ref_pattern)
148 else:
149 if grant.grantee is not None:
150 raise ValueError(
151 "Permission grant for %s has grantee_type '%s', "
152 "contradicting grantee ~%s" %
153 (self.ref_pattern, grant.grantee_type,
154 grant.grantee.name))
155
156 def setGrants(self, grants, user):
157 """See `IGitRule`."""
158 self._validateGrants(grants)
159 existing_grants = {
160 (grant.grantee_type, grant.grantee): grant
161 for grant in self.grants}
162 new_grants = OrderedDict(
163 ((grant.grantee_type, grant.grantee), grant)
164 for grant in grants)
165
166 for grant_key, grant in existing_grants.items():
167 if grant_key not in new_grants:
168 grant.destroySelf(user)
169
170 for grant_key, new_grant in new_grants.items():
171 grant = existing_grants.get(grant_key)
172 if grant is None:
173 new_grantee = (
174 new_grant.grantee
175 if new_grant.grantee_type == GitGranteeType.PERSON
176 else new_grant.grantee_type)
177 grant = self.addGrant(
178 new_grantee, user, can_create=new_grant.can_create,
179 can_push=new_grant.can_push,
180 can_force_push=new_grant.can_force_push)
181 else:
182 grant_before_modification = Snapshot(
183 grant, providing=providedBy(grant))
184 edited_fields = []
185 for field in ("can_create", "can_push", "can_force_push"):
186 if getattr(grant, field) != getattr(new_grant, field):
187 setattr(grant, field, getattr(new_grant, field))
188 edited_fields.append(field)
189 notify(ObjectModifiedEvent(
190 grant, grant_before_modification, edited_fields))
191
121 def destroySelf(self, user):192 def destroySelf(self, user):
122 """See `IGitRule`."""193 """See `IGitRule`."""
123 getUtility(IGitActivitySet).logRuleRemoved(self, user)194 getUtility(IGitActivitySet).logRuleRemoved(self, user)
@@ -142,7 +213,7 @@
142 removeSecurityProxy(grant).date_last_modified = UTC_NOW213 removeSecurityProxy(grant).date_last_modified = UTC_NOW
143214
144215
145@implementer(IGitRuleGrant)216@implementer(IGitRuleGrant, IJSONPublishable)
146class GitRuleGrant(StormBase):217class GitRuleGrant(StormBase):
147 """See `IGitRuleGrant`."""218 """See `IGitRuleGrant`."""
148219
@@ -215,8 +286,35 @@
215 ", ".join(permissions), grantee_name, self.repository.unique_name,286 ", ".join(permissions), grantee_name, self.repository.unique_name,
216 self.rule.ref_pattern)287 self.rule.ref_pattern)
217288
289 def toDataForJSON(self, media_type):
290 """See `IJSONPublishable`."""
291 if media_type != "application/json":
292 raise ValueError("Unhandled media type %s" % media_type)
293 request = get_current_browser_request()
294 field = InlineObject(schema=IGitNascentRuleGrant).bind(self)
295 marshaller = getMultiAdapter((field, request), IFieldMarshaller)
296 return marshaller.unmarshall(None, self)
297
218 def destroySelf(self, user=None):298 def destroySelf(self, user=None):
219 """See `IGitRuleGrant`."""299 """See `IGitRuleGrant`."""
220 if user is not None:300 if user is not None:
221 getUtility(IGitActivitySet).logGrantRemoved(self, user)301 getUtility(IGitActivitySet).logGrantRemoved(self, user)
222 Store.of(self).remove(self)302 Store.of(self).remove(self)
303
304
305@implementer(IGitNascentRuleGrant)
306class GitNascentRuleGrant:
307
308 def __init__(self, grantee_type, grantee=None, can_create=False,
309 can_push=False, can_force_push=False):
310 self.grantee_type = grantee_type
311 self.grantee = grantee
312 self.can_create = can_create
313 self.can_push = can_push
314 self.can_force_push = can_force_push
315
316
317@adapter(dict)
318@implementer(IGitNascentRuleGrant)
319def nascent_rule_grant_from_dict(template):
320 return GitNascentRuleGrant(**template)
223321
=== modified file 'lib/lp/code/model/tests/test_gitref.py'
--- lib/lp/code/model/tests/test_gitref.py 2018-09-27 13:50:06 +0000
+++ lib/lp/code/model/tests/test_gitref.py 2018-10-16 15:29:23 +0000
@@ -17,20 +17,25 @@
17from bzrlib import urlutils17from bzrlib import urlutils
18import pytz18import pytz
19import responses19import responses
20from storm.store import Store
20from testtools.matchers import (21from testtools.matchers import (
21 ContainsDict,22 ContainsDict,
22 EndsWith,23 EndsWith,
23 Equals,24 Equals,
24 Is,25 Is,
25 LessThan,26 LessThan,
27 MatchesDict,
26 MatchesListwise,28 MatchesListwise,
29 MatchesSetwise,
27 MatchesStructure,30 MatchesStructure,
28 )31 )
32import transaction
29from zope.component import getUtility33from zope.component import getUtility
3034
31from lp.app.enums import InformationType35from lp.app.enums import InformationType
32from lp.app.interfaces.informationtype import IInformationType36from lp.app.interfaces.informationtype import IInformationType
33from lp.app.interfaces.launchpad import IPrivacy37from lp.app.interfaces.launchpad import IPrivacy
38from lp.code.enums import GitGranteeType
34from lp.code.errors import (39from lp.code.errors import (
35 GitRepositoryBlobNotFound,40 GitRepositoryBlobNotFound,
36 GitRepositoryBlobUnsupportedRemote,41 GitRepositoryBlobUnsupportedRemote,
@@ -38,8 +43,10 @@
38 InvalidBranchMergeProposal,43 InvalidBranchMergeProposal,
39 )44 )
40from lp.code.interfaces.gitrepository import IGitRepositorySet45from lp.code.interfaces.gitrepository import IGitRepositorySet
46from lp.code.interfaces.gitrule import IGitNascentRuleGrant
41from lp.code.tests.helpers import GitHostingFixture47from lp.code.tests.helpers import GitHostingFixture
42from lp.services.config import config48from lp.services.config import config
49from lp.services.database.sqlbase import get_transaction_timestamp
43from lp.services.features.testing import FeatureFixture50from lp.services.features.testing import FeatureFixture
44from lp.services.memcache.interfaces import IMemcacheClient51from lp.services.memcache.interfaces import IMemcacheClient
45from lp.services.utils import seconds_since_epoch52from lp.services.utils import seconds_since_epoch
@@ -570,6 +577,106 @@
570 self.assertEqual({(person1, "review1"), (person2, "review2")}, votes)577 self.assertEqual({(person1, "review1"), (person2, "review2")}, votes)
571578
572579
580class TestGitRefGrants(TestCaseWithFactory):
581 """Test handling of access grants for refs.
582
583 Most of the hard work here is done by GitRule, but we ensure that
584 getting and setting grants via GitRef operates only on the appropriate
585 exact-match rule.
586 """
587
588 layer = DatabaseFunctionalLayer
589
590 def test_getGrants(self):
591 repository = self.factory.makeGitRepository()
592 [ref] = self.factory.makeGitRefs(repository=repository)
593 rule = self.factory.makeGitRule(
594 repository=repository, ref_pattern=ref.path)
595 grants = [
596 self.factory.makeGitRuleGrant(
597 rule=rule, can_create=True, can_force_push=True),
598 self.factory.makeGitRuleGrant(rule=rule, can_push=True),
599 ]
600 wildcard_rule = self.factory.makeGitRule(
601 repository=repository, ref_pattern="refs/heads/*")
602 self.factory.makeGitRuleGrant(rule=wildcard_rule)
603 self.assertThat(ref.getGrants(), MatchesSetwise(
604 MatchesStructure(
605 rule=Equals(rule),
606 grantee_type=Equals(GitGranteeType.PERSON),
607 grantee=Equals(grants[0].grantee),
608 can_create=Is(True),
609 can_push=Is(False),
610 can_force_push=Is(True)),
611 MatchesStructure(
612 rule=Equals(rule),
613 grantee_type=Equals(GitGranteeType.PERSON),
614 grantee=Equals(grants[1].grantee),
615 can_create=Is(False),
616 can_push=Is(True),
617 can_force_push=Is(False))))
618
619 def test_setGrants_no_matching_rule(self):
620 repository = self.factory.makeGitRepository()
621 [ref] = self.factory.makeGitRefs(repository=repository)
622 self.factory.makeGitRule(
623 repository=repository, ref_pattern="refs/heads/*")
624 other_repository = self.factory.makeGitRepository()
625 self.factory.makeGitRule(
626 repository=other_repository, ref_pattern=ref.path)
627 with person_logged_in(repository.owner):
628 ref.setGrants([
629 IGitNascentRuleGrant({
630 "grantee_type": GitGranteeType.REPOSITORY_OWNER,
631 "can_force_push": True,
632 }),
633 ], repository.owner)
634 self.assertThat(list(repository.rules), MatchesListwise([
635 MatchesStructure(
636 repository=Equals(repository),
637 ref_pattern=Equals(ref.path),
638 grants=MatchesSetwise(
639 MatchesStructure(
640 grantee_type=Equals(GitGranteeType.REPOSITORY_OWNER),
641 grantee=Is(None),
642 can_create=Is(False),
643 can_push=Is(False),
644 can_force_push=Is(True)))),
645 MatchesStructure(
646 repository=Equals(repository),
647 ref_pattern=Equals("refs/heads/*"),
648 grants=MatchesSetwise()),
649 ]))
650
651 def test_setGrants_matching_rule(self):
652 repository = self.factory.makeGitRepository()
653 [ref] = self.factory.makeGitRefs(repository=repository)
654 rule = self.factory.makeGitRule(
655 repository=repository, ref_pattern=ref.path)
656 date_created = get_transaction_timestamp(Store.of(rule))
657 transaction.commit()
658 with person_logged_in(repository.owner):
659 ref.setGrants([
660 IGitNascentRuleGrant({
661 "grantee_type": GitGranteeType.REPOSITORY_OWNER,
662 "can_force_push": True,
663 }),
664 ], repository.owner)
665 self.assertThat(list(repository.rules), MatchesListwise([
666 MatchesStructure(
667 repository=Equals(repository),
668 ref_pattern=Equals(ref.path),
669 date_created=Equals(date_created),
670 grants=MatchesSetwise(
671 MatchesStructure(
672 grantee_type=Equals(GitGranteeType.REPOSITORY_OWNER),
673 grantee=Is(None),
674 can_create=Is(False),
675 can_push=Is(False),
676 can_force_push=Is(True)))),
677 ]))
678
679
573class TestGitRefWebservice(TestCaseWithFactory):680class TestGitRefWebservice(TestCaseWithFactory):
574 """Tests for the webservice."""681 """Tests for the webservice."""
575682
@@ -686,3 +793,85 @@
686 self.assertEqual(1, len(dependent_landings["entries"]))793 self.assertEqual(1, len(dependent_landings["entries"]))
687 self.assertThat(794 self.assertThat(
688 dependent_landings["entries"][0]["self_link"], EndsWith(bmp_url))795 dependent_landings["entries"][0]["self_link"], EndsWith(bmp_url))
796
797 def test_getGrants(self):
798 [ref] = self.factory.makeGitRefs()
799 rule = self.factory.makeGitRule(
800 repository=ref.repository, ref_pattern=ref.path)
801 self.factory.makeGitRuleGrant(
802 rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER,
803 can_create=True, can_force_push=True)
804 grantee = self.factory.makePerson()
805 self.factory.makeGitRuleGrant(
806 rule=rule, grantee=grantee, can_push=True)
807 with person_logged_in(ref.owner):
808 ref_url = api_url(ref)
809 grantee_url = api_url(grantee)
810 webservice = webservice_for_person(
811 ref.owner, permission=OAuthPermission.WRITE_PUBLIC)
812 webservice.default_api_version = "devel"
813 response = webservice.named_get(ref_url, "getGrants")
814 self.assertThat(json.loads(response.body), MatchesSetwise(
815 MatchesDict({
816 "grantee_type": Equals("Repository owner"),
817 "grantee_link": Is(None),
818 "can_create": Is(True),
819 "can_push": Is(False),
820 "can_force_push": Is(True),
821 }),
822 MatchesDict({
823 "grantee_type": Equals("Person"),
824 "grantee_link": Equals(webservice.getAbsoluteUrl(grantee_url)),
825 "can_create": Is(False),
826 "can_push": Is(True),
827 "can_force_push": Is(False),
828 })))
829
830 def test_setGrants(self):
831 [ref] = self.factory.makeGitRefs()
832 owner = ref.owner
833 grantee = self.factory.makePerson()
834 with person_logged_in(owner):
835 ref_url = api_url(ref)
836 grantee_url = api_url(grantee)
837 webservice = webservice_for_person(
838 owner, permission=OAuthPermission.WRITE_PUBLIC)
839 webservice.default_api_version = "devel"
840 response = webservice.named_post(
841 ref_url, "setGrants",
842 grants=[
843 {
844 "grantee_type": "Repository owner",
845 "can_create": True,
846 "can_force_push": True,
847 },
848 {
849 "grantee_type": "Person",
850 "grantee_link": grantee_url,
851 "can_push": True,
852 },
853 ])
854 self.assertEqual(200, response.status)
855 with person_logged_in(owner):
856 self.assertThat(list(ref.repository.rules), MatchesListwise([
857 MatchesStructure(
858 repository=Equals(ref.repository),
859 ref_pattern=Equals(ref.path),
860 creator=Equals(owner),
861 grants=MatchesSetwise(
862 MatchesStructure(
863 grantor=Equals(owner),
864 grantee_type=Equals(
865 GitGranteeType.REPOSITORY_OWNER),
866 grantee=Is(None),
867 can_create=Is(True),
868 can_push=Is(False),
869 can_force_push=Is(True)),
870 MatchesStructure(
871 grantor=Equals(owner),
872 grantee_type=Equals(GitGranteeType.PERSON),
873 grantee=Equals(grantee),
874 can_create=Is(False),
875 can_push=Is(True),
876 can_force_push=Is(False)))),
877 ]))
689878
=== modified file 'lib/lp/code/model/tests/test_gitrule.py'
--- lib/lp/code/model/tests/test_gitrule.py 2018-10-12 16:41:14 +0000
+++ lib/lp/code/model/tests/test_gitrule.py 2018-10-16 15:29:23 +0000
@@ -14,17 +14,21 @@
14 Equals,14 Equals,
15 Is,15 Is,
16 MatchesDict,16 MatchesDict,
17 MatchesListwise,
17 MatchesSetwise,18 MatchesSetwise,
18 MatchesStructure,19 MatchesStructure,
19 )20 )
21import transaction
20from zope.event import notify22from zope.event import notify
21from zope.interface import providedBy23from zope.interface import providedBy
24from zope.security.proxy import removeSecurityProxy
2225
23from lp.code.enums import (26from lp.code.enums import (
24 GitActivityType,27 GitActivityType,
25 GitGranteeType,28 GitGranteeType,
26 )29 )
27from lp.code.interfaces.gitrule import (30from lp.code.interfaces.gitrule import (
31 IGitNascentRuleGrant,
28 IGitRule,32 IGitRule,
29 IGitRuleGrant,33 IGitRuleGrant,
30 )34 )
@@ -122,6 +126,303 @@
122 can_push=Is(False),126 can_push=Is(False),
123 can_force_push=Is(True))))127 can_force_push=Is(True))))
124128
129 def test__validateGrants_ok(self):
130 rule = self.factory.makeGitRule()
131 grants = [
132 IGitNascentRuleGrant({
133 "grantee_type": GitGranteeType.REPOSITORY_OWNER,
134 "can_force_push": True,
135 }),
136 ]
137 removeSecurityProxy(rule)._validateGrants(grants)
138
139 def test__validateGrants_grantee_type_person_but_no_grantee(self):
140 rule = self.factory.makeGitRule(ref_pattern="refs/heads/*")
141 grants = [
142 IGitNascentRuleGrant({
143 "grantee_type": GitGranteeType.PERSON,
144 "can_force_push": True,
145 }),
146 ]
147 self.assertRaisesWithContent(
148 ValueError,
149 "Permission grant for refs/heads/* has grantee_type 'Person' but "
150 "no grantee",
151 removeSecurityProxy(rule)._validateGrants, grants)
152
153 def test__validateGrants_grantee_but_wrong_grantee_type(self):
154 rule = self.factory.makeGitRule(ref_pattern="refs/heads/*")
155 grantee = self.factory.makePerson()
156 grants = [
157 IGitNascentRuleGrant({
158 "grantee_type": GitGranteeType.REPOSITORY_OWNER,
159 "grantee": grantee,
160 "can_force_push": True,
161 }),
162 ]
163 self.assertRaisesWithContent(
164 ValueError,
165 "Permission grant for refs/heads/* has grantee_type "
166 "'Repository owner', contradicting grantee ~%s" % grantee.name,
167 removeSecurityProxy(rule)._validateGrants, grants)
168
169 def test_setGrants_add(self):
170 owner = self.factory.makeTeam()
171 member = self.factory.makePerson(member_of=[owner])
172 rule = self.factory.makeGitRule(owner=owner)
173 grantee = self.factory.makePerson()
174 removeSecurityProxy(rule.repository.getActivity()).remove()
175 with person_logged_in(member):
176 rule.setGrants([
177 IGitNascentRuleGrant({
178 "grantee_type": GitGranteeType.REPOSITORY_OWNER,
179 "can_create": True,
180 "can_force_push": True,
181 }),
182 IGitNascentRuleGrant({
183 "grantee_type": GitGranteeType.PERSON,
184 "grantee": grantee,
185 "can_push": True,
186 }),
187 ], member)
188 self.assertThat(rule.grants, MatchesSetwise(
189 MatchesStructure(
190 rule=Equals(rule),
191 grantor=Equals(member),
192 grantee_type=Equals(GitGranteeType.REPOSITORY_OWNER),
193 grantee=Is(None),
194 can_create=Is(True),
195 can_push=Is(False),
196 can_force_push=Is(True)),
197 MatchesStructure(
198 rule=Equals(rule),
199 grantor=Equals(member),
200 grantee_type=Equals(GitGranteeType.PERSON),
201 grantee=Equals(grantee),
202 can_create=Is(False),
203 can_push=Is(True),
204 can_force_push=Is(False))))
205 self.assertThat(list(rule.repository.getActivity()), MatchesListwise([
206 MatchesStructure(
207 repository=Equals(rule.repository),
208 changer=Equals(member),
209 changee=Equals(grantee),
210 what_changed=Equals(GitActivityType.GRANT_ADDED),
211 old_value=Is(None),
212 new_value=MatchesDict({
213 "changee_type": Equals("Person"),
214 "ref_pattern": Equals(rule.ref_pattern),
215 "can_create": Is(False),
216 "can_push": Is(True),
217 "can_force_push": Is(False),
218 })),
219 MatchesStructure(
220 repository=Equals(rule.repository),
221 changer=Equals(member),
222 changee=Is(None),
223 what_changed=Equals(GitActivityType.GRANT_ADDED),
224 old_value=Is(None),
225 new_value=MatchesDict({
226 "changee_type": Equals("Repository owner"),
227 "ref_pattern": Equals(rule.ref_pattern),
228 "can_create": Is(True),
229 "can_push": Is(False),
230 "can_force_push": Is(True),
231 })),
232 ]))
233
234 def test_setGrants_modify(self):
235 owner = self.factory.makeTeam()
236 members = [
237 self.factory.makePerson(member_of=[owner]) for _ in range(2)]
238 rule = self.factory.makeGitRule(owner=owner)
239 grantees = [self.factory.makePerson() for _ in range(2)]
240 self.factory.makeGitRuleGrant(
241 rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER,
242 grantor=members[0], can_create=True)
243 self.factory.makeGitRuleGrant(
244 rule=rule, grantee=grantees[0], grantor=members[0], can_push=True)
245 self.factory.makeGitRuleGrant(
246 rule=rule, grantee=grantees[1], grantor=members[0],
247 can_force_push=True)
248 date_created = get_transaction_timestamp(Store.of(rule))
249 transaction.commit()
250 removeSecurityProxy(rule.repository.getActivity()).remove()
251 with person_logged_in(members[1]):
252 rule.setGrants([
253 IGitNascentRuleGrant({
254 "grantee_type": GitGranteeType.REPOSITORY_OWNER,
255 "can_force_push": True,
256 }),
257 IGitNascentRuleGrant({
258 "grantee_type": GitGranteeType.PERSON,
259 "grantee": grantees[1],
260 "can_create": True,
261 }),
262 IGitNascentRuleGrant({
263 "grantee_type": GitGranteeType.PERSON,
264 "grantee": grantees[0],
265 "can_push": True,
266 "can_force_push": True,
267 }),
268 ], members[1])
269 date_modified = get_transaction_timestamp(Store.of(rule))
270 self.assertThat(rule.grants, MatchesSetwise(
271 MatchesStructure(
272 rule=Equals(rule),
273 grantor=Equals(members[0]),
274 grantee_type=Equals(GitGranteeType.REPOSITORY_OWNER),
275 grantee=Is(None),
276 can_create=Is(False),
277 can_push=Is(False),
278 can_force_push=Is(True),
279 date_created=Equals(date_created),
280 date_last_modified=Equals(date_modified)),
281 MatchesStructure(
282 rule=Equals(rule),
283 grantor=Equals(members[0]),
284 grantee_type=Equals(GitGranteeType.PERSON),
285 grantee=Equals(grantees[0]),
286 can_create=Is(False),
287 can_push=Is(True),
288 can_force_push=Is(True),
289 date_created=Equals(date_created),
290 date_last_modified=Equals(date_modified)),
291 MatchesStructure(
292 rule=Equals(rule),
293 grantor=Equals(members[0]),
294 grantee_type=Equals(GitGranteeType.PERSON),
295 grantee=Equals(grantees[1]),
296 can_create=Is(True),
297 can_push=Is(False),
298 can_force_push=Is(False),
299 date_created=Equals(date_created),
300 date_last_modified=Equals(date_modified))))
301 self.assertThat(list(rule.repository.getActivity()), MatchesListwise([
302 MatchesStructure(
303 repository=Equals(rule.repository),
304 changer=Equals(members[1]),
305 changee=Equals(grantees[0]),
306 what_changed=Equals(GitActivityType.GRANT_CHANGED),
307 old_value=MatchesDict({
308 "changee_type": Equals("Person"),
309 "ref_pattern": Equals(rule.ref_pattern),
310 "can_create": Is(False),
311 "can_push": Is(True),
312 "can_force_push": Is(False),
313 }),
314 new_value=MatchesDict({
315 "changee_type": Equals("Person"),
316 "ref_pattern": Equals(rule.ref_pattern),
317 "can_create": Is(False),
318 "can_push": Is(True),
319 "can_force_push": Is(True),
320 })),
321 MatchesStructure(
322 repository=Equals(rule.repository),
323 changer=Equals(members[1]),
324 changee=Equals(grantees[1]),
325 what_changed=Equals(GitActivityType.GRANT_CHANGED),
326 old_value=MatchesDict({
327 "changee_type": Equals("Person"),
328 "ref_pattern": Equals(rule.ref_pattern),
329 "can_create": Is(False),
330 "can_push": Is(False),
331 "can_force_push": Is(True),
332 }),
333 new_value=MatchesDict({
334 "changee_type": Equals("Person"),
335 "ref_pattern": Equals(rule.ref_pattern),
336 "can_create": Is(True),
337 "can_push": Is(False),
338 "can_force_push": Is(False),
339 })),
340 MatchesStructure(
341 repository=Equals(rule.repository),
342 changer=Equals(members[1]),
343 changee=Is(None),
344 what_changed=Equals(GitActivityType.GRANT_CHANGED),
345 old_value=MatchesDict({
346 "changee_type": Equals("Repository owner"),
347 "ref_pattern": Equals(rule.ref_pattern),
348 "can_create": Is(True),
349 "can_push": Is(False),
350 "can_force_push": Is(False),
351 }),
352 new_value=MatchesDict({
353 "changee_type": Equals("Repository owner"),
354 "ref_pattern": Equals(rule.ref_pattern),
355 "can_create": Is(False),
356 "can_push": Is(False),
357 "can_force_push": Is(True),
358 })),
359 ]))
360
361 def test_setGrants_remove(self):
362 owner = self.factory.makeTeam()
363 members = [
364 self.factory.makePerson(member_of=[owner]) for _ in range(2)]
365 rule = self.factory.makeGitRule(owner=owner)
366 grantees = [self.factory.makePerson() for _ in range(2)]
367 self.factory.makeGitRuleGrant(
368 rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER,
369 grantor=members[0], can_create=True)
370 self.factory.makeGitRuleGrant(
371 rule=rule, grantee=grantees[0], grantor=members[0], can_push=True)
372 self.factory.makeGitRuleGrant(
373 rule=rule, grantee=grantees[1], grantor=members[0],
374 can_force_push=True)
375 date_created = get_transaction_timestamp(Store.of(rule))
376 transaction.commit()
377 removeSecurityProxy(rule.repository.getActivity()).remove()
378 with person_logged_in(members[1]):
379 rule.setGrants([
380 IGitNascentRuleGrant({
381 "grantee_type": GitGranteeType.PERSON,
382 "grantee": grantees[0],
383 "can_push": True,
384 }),
385 ], members[1])
386 self.assertThat(rule.grants, MatchesSetwise(
387 MatchesStructure(
388 rule=Equals(rule),
389 grantor=Equals(members[0]),
390 grantee_type=Equals(GitGranteeType.PERSON),
391 grantee=Equals(grantees[0]),
392 can_create=Is(False),
393 can_push=Is(True),
394 can_force_push=Is(False),
395 date_created=Equals(date_created),
396 date_last_modified=Equals(date_created))))
397 self.assertThat(list(rule.repository.getActivity()), MatchesSetwise(
398 MatchesStructure(
399 repository=Equals(rule.repository),
400 changer=Equals(members[1]),
401 changee=Is(None),
402 what_changed=Equals(GitActivityType.GRANT_REMOVED),
403 old_value=MatchesDict({
404 "changee_type": Equals("Repository owner"),
405 "ref_pattern": Equals(rule.ref_pattern),
406 "can_create": Is(True),
407 "can_push": Is(False),
408 "can_force_push": Is(False),
409 }),
410 new_value=Is(None)),
411 MatchesStructure(
412 repository=Equals(rule.repository),
413 changer=Equals(members[1]),
414 changee=Equals(grantees[1]),
415 what_changed=Equals(GitActivityType.GRANT_REMOVED),
416 old_value=MatchesDict({
417 "changee_type": Equals("Person"),
418 "ref_pattern": Equals(rule.ref_pattern),
419 "can_create": Is(False),
420 "can_push": Is(False),
421 "can_force_push": Is(True),
422 }),
423 new_value=Is(None)),
424 ))
425
125 def test_activity_rule_added(self):426 def test_activity_rule_added(self):
126 owner = self.factory.makeTeam()427 owner = self.factory.makeTeam()
127 member = self.factory.makePerson(member_of=[owner])428 member = self.factory.makePerson(member_of=[owner])
128429
=== modified file 'lib/lp/services/fields/__init__.py'
--- lib/lp/services/fields/__init__.py 2015-09-28 17:38:45 +0000
+++ lib/lp/services/fields/__init__.py 2018-10-16 15:29:23 +0000
@@ -1,10 +1,9 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
5__all__ = [5__all__ = [
6 'AnnouncementDate',6 'AnnouncementDate',
7 'FormattableDate',
8 'BaseImageUpload',7 'BaseImageUpload',
9 'BlacklistableContentNameField',8 'BlacklistableContentNameField',
10 'BugField',9 'BugField',
@@ -13,10 +12,12 @@
13 'Datetime',12 'Datetime',
14 'DuplicateBug',13 'DuplicateBug',
15 'FieldNotBoundError',14 'FieldNotBoundError',
15 'FormattableDate',
16 'IAnnouncementDate',16 'IAnnouncementDate',
17 'IBaseImageUpload',17 'IBaseImageUpload',
18 'IBugField',18 'IBugField',
19 'IDescription',19 'IDescription',
20 'IInlineObject',
20 'INoneableTextLine',21 'INoneableTextLine',
21 'IPersonChoice',22 'IPersonChoice',
22 'IStrippedTextLine',23 'IStrippedTextLine',
@@ -26,6 +27,7 @@
26 'IURIField',27 'IURIField',
27 'IWhiteboard',28 'IWhiteboard',
28 'IconImageUpload',29 'IconImageUpload',
30 'InlineObject',
29 'KEEP_SAME_IMAGE',31 'KEEP_SAME_IMAGE',
30 'LogoImageUpload',32 'LogoImageUpload',
31 'MugshotImageUpload',33 'MugshotImageUpload',
@@ -71,6 +73,7 @@
71 Date,73 Date,
72 Datetime,74 Datetime,
73 Int,75 Int,
76 Object,
74 Text,77 Text,
75 TextLine,78 TextLine,
76 Tuple,79 Tuple,
@@ -909,3 +912,12 @@
909 "for the target '%s'." % \912 "for the target '%s'." % \
910 (milestone_name, target.name))913 (milestone_name, target.name))
911 return milestone914 return milestone
915
916
917class IInlineObject(IObject):
918 """A marker for an object represented as a dict."""
919
920
921@implementer(IInlineObject)
922class InlineObject(Object):
923 """An object that is represented as a dict rather than a URL reference."""
912924
=== modified file 'lib/lp/services/webservice/configure.zcml'
--- lib/lp/services/webservice/configure.zcml 2015-04-28 15:22:46 +0000
+++ lib/lp/services/webservice/configure.zcml 2018-10-16 15:29:23 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2011 Canonical Ltd. This software is licensed under the1<!-- Copyright 2011-2018 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -84,6 +84,12 @@
84 provides="lazr.restful.interfaces.IFieldMarshaller"84 provides="lazr.restful.interfaces.IFieldMarshaller"
85 factory="lazr.restful.marshallers.ObjectLookupFieldMarshaller"85 factory="lazr.restful.marshallers.ObjectLookupFieldMarshaller"
86 />86 />
87 <adapter
88 for="lp.services.fields.IInlineObject
89 zope.publisher.interfaces.http.IHTTPRequest"
90 provides="lazr.restful.interfaces.IFieldMarshaller"
91 factory="lp.app.webservice.marshallers.InlineObjectFieldMarshaller"
92 />
8793
88 <!-- The API documentation -->94 <!-- The API documentation -->
89 <browser:page95 <browser:page