Merge lp:~adeuring/launchpad/specification-grants into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 15930
Proposed branch: lp:~adeuring/launchpad/specification-grants
Merge into: lp:launchpad
Diff against target: 483 lines (+129/-69)
11 files modified
lib/lp/blueprints/browser/tests/test_specification.py (+2/-0)
lib/lp/blueprints/configure.zcml (+0/-4)
lib/lp/blueprints/model/specification.py (+6/-1)
lib/lp/blueprints/model/tests/test_specification.py (+3/-0)
lib/lp/blueprints/tests/test_specification.py (+59/-52)
lib/lp/registry/interfaces/accesspolicy.py (+1/-0)
lib/lp/registry/interfaces/sharingservice.py (+3/-1)
lib/lp/registry/model/accesspolicy.py (+15/-5)
lib/lp/registry/services/sharingservice.py (+3/-1)
lib/lp/registry/services/tests/test_sharingservice.py (+30/-5)
lib/lp/registry/tests/test_accesspolicy.py (+7/-0)
To merge this branch: bzr merge lp:~adeuring/launchpad/specification-grants
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+123062@code.launchpad.net

Commit message

new parameter "specifications" for SharingService.ensureAccessGrants()

Description of the change

This branch makes it possible to grant access to a single specification that
is not publicly visible. (The security adapter for ISpecification does not
use yet these grants -- the MP would become overly long with this change...)

= Details =

lib/lp/blueprints/configure.zcml,
lib/lp/blueprints/model/specification.py,
lib/lp/blueprints/tests/test_specification.py:

The property Specification.information_type should not be set directly
because instances that are not publicly visible need a record in
the table AccessPolicyArtifact that links the specification to an
AccessPolicy. These links are maintained by calling
reconcile_access_for_artifact() in Specifcation.transitionToInformationType().

This change leads to a number of changes in
lib/lp/blueprints/tests/test_specification.py, where severeal tests
changed information_type with a simple assignment.

Furthermore, reconcile_access_for_artifact() obviously fails if no
matching AccessPolicy record exists; these records are created by
calling speicifcaton.target._ensurePolicies(). This is a bit hackish,
but the class Product does not yet have a method
setSpecifcationSharingPolicy() which would manage AccessPolicy records
"officially".

lib/lp/registry/interfaces/accesspolicy.py:

The attribute specification_id was simply missing in the interface class;
it is already used in the model.

lib/lp/registry/model/accesspolicy.py,
lib/lp/registry/services/sharingservice.py,
lib/lp/registry/services/tests/test_sharingservice.py:

The core of the change: AccessArtifact.ensure() and
SharingService.ensureAccessGrants() must be able to deal
not only with bugs and branches, but also with specifications.

= tests =

./bin/test -vvt lp.registry.services.tests.test_sharingservice.*test_ensureAccessGrants
./bin/test -vvt lp.blueprints.tests.test_specification.SpecificationTests.test.*access
./bin/test -vvt lp.registry.tests.test_accesspolicy.TestAccessArtifactSpecification

no lint

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

Abel--

Looks good, thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/blueprints/browser/tests/test_specification.py'
2--- lib/lp/blueprints/browser/tests/test_specification.py 2012-09-06 00:01:38 +0000
3+++ lib/lp/blueprints/browser/tests/test_specification.py 2012-09-10 10:23:19 +0000
4@@ -227,6 +227,8 @@
5 """Setting the value via '+secrecy' works."""
6 owner = self.factory.makePerson()
7 spec = self.factory.makeSpecification(owner=owner)
8+ removeSecurityProxy(spec.target)._ensurePolicies(
9+ [InformationType.PROPRIETARY])
10 self.set_secrecy(spec, owner)
11 with person_logged_in(owner):
12 self.assertEqual(InformationType.PROPRIETARY,
13
14=== modified file 'lib/lp/blueprints/configure.zcml'
15--- lib/lp/blueprints/configure.zcml 2012-08-20 16:38:10 +0000
16+++ lib/lp/blueprints/configure.zcml 2012-09-10 10:23:19 +0000
17@@ -150,10 +150,6 @@
18 <class class="lp.blueprints.model.specification.Specification">
19 <allow interface="lp.blueprints.interfaces.specification.ISpecificationPublic"/>
20 <require
21- permission="launchpad.Edit"
22- set_attributes="information_type" />
23-
24- <require
25 permission="launchpad.LimitedView"
26 interface="lp.blueprints.interfaces.specification.ISpecificationView" />
27
28
29=== modified file 'lib/lp/blueprints/model/specification.py'
30--- lib/lp/blueprints/model/specification.py 2012-09-06 19:05:05 +0000
31+++ lib/lp/blueprints/model/specification.py 2012-09-10 10:23:19 +0000
32@@ -821,12 +821,17 @@
33 return set(PUBLIC_PROPRIETARY_INFORMATION_TYPES)
34
35 def transitionToInformationType(self, information_type, who):
36- """See `IBug`."""
37+ """See ISpecification."""
38+ # avoid circular imports.
39+ from lp.registry.model.accesspolicy import (
40+ reconcile_access_for_artifact,
41+ )
42 if self.information_type == information_type:
43 return False
44 if information_type not in self.getAllowedInformationTypes(who):
45 raise CannotChangeInformationType("Forbidden by project policy.")
46 self.information_type = information_type
47+ reconcile_access_for_artifact(self, information_type, [self.target])
48 return True
49
50 @property
51
52=== modified file 'lib/lp/blueprints/model/tests/test_specification.py'
53--- lib/lp/blueprints/model/tests/test_specification.py 2012-09-06 19:06:11 +0000
54+++ lib/lp/blueprints/model/tests/test_specification.py 2012-09-10 10:23:19 +0000
55@@ -16,6 +16,7 @@
56 from zope.event import notify
57 from zope.interface import providedBy
58 from zope.security.interfaces import Unauthorized
59+from zope.security.proxy import removeSecurityProxy
60
61 from lp.app.validators import LaunchpadValidationError
62 from lp.blueprints.interfaces.specification import ISpecification
63@@ -619,6 +620,8 @@
64 """Ensure transitionToInformationType works."""
65 spec = self.factory.makeSpecification()
66 self.assertEqual(InformationType.PUBLIC, spec.information_type)
67+ removeSecurityProxy(spec.target)._ensurePolicies(
68+ [InformationType.EMBARGOED])
69 with person_logged_in(spec.owner):
70 result = spec.transitionToInformationType(
71 InformationType.EMBARGOED, spec.owner)
72
73=== modified file 'lib/lp/blueprints/tests/test_specification.py'
74--- lib/lp/blueprints/tests/test_specification.py 2012-09-06 00:01:38 +0000
75+++ lib/lp/blueprints/tests/test_specification.py 2012-09-10 10:23:19 +0000
76@@ -193,9 +193,9 @@
77 'launchpad.AnyAllowedPerson': set(('whiteboard', )),
78 'launchpad.Edit': set((
79 'approver', 'assignee', 'definition_status', 'distribution',
80- 'drafter', 'implementation_status', 'information_type',
81- 'man_days', 'milestone', 'name', 'product', 'specurl',
82- 'summary', 'superseded_by', 'title')),
83+ 'drafter', 'implementation_status', 'man_days', 'milestone',
84+ 'name', 'product', 'specurl', 'summary', 'superseded_by',
85+ 'title')),
86 }
87 specification = self.factory.makeSpecification()
88 checker = getChecker(specification)
89@@ -249,27 +249,30 @@
90 setattr(specification, attribute, value)
91
92 def test_anon_read_access(self):
93- # Anonymous users have access to public specifications...
94+ # Anonymous users have access to public specifications but not
95+ # to private specifications.
96 specification = self.factory.makeSpecification()
97- for information_type in PUBLIC_INFORMATION_TYPES:
98- with person_logged_in(specification.owner):
99- specification.information_type = information_type
100- self.read_access_to_ISpecificationView(
101- ANONYMOUS, specification, error_expected=False)
102- # ...but not to private specifications.
103- for information_type in PRIVATE_INFORMATION_TYPES:
104- with person_logged_in(specification.owner):
105- specification.information_type = information_type
106- self.read_access_to_ISpecificationView(
107- ANONYMOUS, specification, error_expected=True)
108+ removeSecurityProxy(specification.target)._ensurePolicies(
109+ PRIVATE_INFORMATION_TYPES)
110+ all_types = specification.getAllowedInformationTypes(ANONYMOUS)
111+ for information_type in all_types:
112+ with person_logged_in(specification.owner):
113+ specification.transitionToInformationType(
114+ information_type, specification.owner)
115+ error_expected = information_type not in PUBLIC_INFORMATION_TYPES
116+ self.read_access_to_ISpecificationView(
117+ ANONYMOUS, specification, error_expected)
118
119 def test_anon_write_access(self):
120 # Anonymous users do not have write access to specifications.
121 specification = self.factory.makeSpecification()
122- for information_type in (PUBLIC_INFORMATION_TYPES +
123- PRIVATE_INFORMATION_TYPES):
124+ removeSecurityProxy(specification.target)._ensurePolicies(
125+ PRIVATE_INFORMATION_TYPES)
126+ all_types = specification.getAllowedInformationTypes(ANONYMOUS)
127+ for information_type in all_types:
128 with person_logged_in(specification.owner):
129- specification.information_type = information_type
130+ specification.transitionToInformationType(
131+ information_type, specification.owner)
132 self.write_access_to_ISpecificationView(
133 ANONYMOUS, specification, error_expected=True,
134 attribute='whiteboard', value='foo')
135@@ -278,41 +281,37 @@
136 attribute='name', value='foo')
137
138 def test_ordinary_user_read_access(self):
139- # Oridnary users have access to public specifications...
140+ # Oridnary users have access to public specifications but not
141+ # to private specifications.
142 specification = self.factory.makeSpecification()
143+ removeSecurityProxy(specification.target)._ensurePolicies(
144+ PRIVATE_INFORMATION_TYPES)
145 user = self.factory.makePerson()
146- for information_type in PUBLIC_INFORMATION_TYPES:
147- with person_logged_in(specification.owner):
148- specification.information_type = information_type
149- self.read_access_to_ISpecificationView(
150- user, specification, error_expected=False)
151- # ...but not to private specifications.
152- for information_type in PRIVATE_INFORMATION_TYPES:
153- with person_logged_in(specification.owner):
154- specification.information_type = information_type
155- self.read_access_to_ISpecificationView(
156- user, specification, error_expected=True)
157+ all_types = specification.getAllowedInformationTypes(user)
158+ for information_type in all_types:
159+ with person_logged_in(specification.owner):
160+ specification.transitionToInformationType(
161+ information_type, specification.owner)
162+ error_expected = information_type not in PUBLIC_INFORMATION_TYPES
163+ self.read_access_to_ISpecificationView(
164+ user, specification, error_expected)
165
166 def test_ordinary_user_write_access(self):
167 # Oridnary users can change the whiteborad of public specifications.
168- # They cannot change other attributes.
169+ # They cannot change other attributes of public speicifcaitons and
170+ # no attributes of private specifications.
171 specification = self.factory.makeSpecification()
172+ removeSecurityProxy(specification.target)._ensurePolicies(
173+ PRIVATE_INFORMATION_TYPES)
174 user = self.factory.makePerson()
175- for information_type in PUBLIC_INFORMATION_TYPES:
176- with person_logged_in(specification.owner):
177- specification.information_type = information_type
178- self.write_access_to_ISpecificationView(
179- user, specification, error_expected=False,
180- attribute='whiteboard', value='foo')
181- self.write_access_to_ISpecificationView(
182- user, specification, error_expected=True,
183- attribute='name', value='foo')
184- # The cannot change any attribute of private specifcations.
185- for information_type in PRIVATE_INFORMATION_TYPES:
186- with person_logged_in(specification.owner):
187- specification.information_type = information_type
188- self.write_access_to_ISpecificationView(
189- user, specification, error_expected=True,
190+ all_types = specification.getAllowedInformationTypes(user)
191+ for information_type in all_types:
192+ with person_logged_in(specification.owner):
193+ specification.transitionToInformationType(
194+ information_type, specification.owner)
195+ error_expected = information_type not in PUBLIC_INFORMATION_TYPES
196+ self.write_access_to_ISpecificationView(
197+ user, specification, error_expected,
198 attribute='whiteboard', value='foo')
199 self.write_access_to_ISpecificationView(
200 user, specification, error_expected=True,
201@@ -322,10 +321,14 @@
202 # Users with special privileges can aceess the attributes
203 # of public and private specifcations.
204 specification = self.factory.makeSpecification()
205- for information_type in (PUBLIC_INFORMATION_TYPES +
206- PRIVATE_INFORMATION_TYPES):
207+ removeSecurityProxy(specification.target)._ensurePolicies(
208+ PRIVATE_INFORMATION_TYPES)
209+ all_types = specification.getAllowedInformationTypes(
210+ specification.owner)
211+ for information_type in all_types:
212 with person_logged_in(specification.owner):
213- specification.information_type = information_type
214+ specification.transitionToInformationType(
215+ information_type, specification.owner)
216 self.read_access_to_ISpecificationView(
217 specification.owner, specification, error_expected=False)
218
219@@ -333,10 +336,14 @@
220 # Users with special privileges can change the attributes
221 # of public and private specifcations.
222 specification = self.factory.makeSpecification()
223- for information_type in (PUBLIC_INFORMATION_TYPES +
224- PRIVATE_INFORMATION_TYPES):
225+ removeSecurityProxy(specification.target)._ensurePolicies(
226+ PRIVATE_INFORMATION_TYPES)
227+ all_types = specification.getAllowedInformationTypes(
228+ specification.owner)
229+ for information_type in all_types:
230 with person_logged_in(specification.owner):
231- specification.information_type = information_type
232+ specification.transitionToInformationType(
233+ information_type, specification.owner)
234 self.write_access_to_ISpecificationView(
235 specification.owner, specification, error_expected=False,
236 attribute='whiteboard', value='foo')
237
238=== modified file 'lib/lp/registry/interfaces/accesspolicy.py'
239--- lib/lp/registry/interfaces/accesspolicy.py 2012-09-03 11:22:25 +0000
240+++ lib/lp/registry/interfaces/accesspolicy.py 2012-09-10 10:23:19 +0000
241@@ -35,6 +35,7 @@
242 concrete_artifact = Attribute("Concrete artifact")
243 bug_id = Attribute("bug_id")
244 branch_id = Attribute("branch_id")
245+ specification_id = Attribute("specification_id")
246
247
248 class IAccessArtifactGrant(Interface):
249
250=== modified file 'lib/lp/registry/interfaces/sharingservice.py'
251--- lib/lp/registry/interfaces/sharingservice.py 2012-09-05 03:39:50 +0000
252+++ lib/lp/registry/interfaces/sharingservice.py 2012-09-10 10:23:19 +0000
253@@ -247,13 +247,15 @@
254 branches=List(
255 Reference(schema=IBranch), title=_('Branches'), required=False))
256 @operation_for_version('devel')
257- def ensureAccessGrants(grantees, user, branches=None, bugs=None):
258+ def ensureAccessGrants(grantees, user, branches=None, bugs=None,
259+ specifications=None):
260 """Ensure a grantee has an access grant to the specified artifacts.
261
262 :param grantees: the people or teams for whom to grant access
263 :param user: the user making the request
264 :param bugs: the bugs for which to grant access
265 :param branches: the branches for which to grant access
266+ :param specifications: the specifications for which to grant access
267 """
268
269 @export_write_operation()
270
271=== modified file 'lib/lp/registry/model/accesspolicy.py'
272--- lib/lp/registry/model/accesspolicy.py 2012-09-03 11:22:25 +0000
273+++ lib/lp/registry/model/accesspolicy.py 2012-09-10 10:23:19 +0000
274@@ -70,7 +70,7 @@
275 (pillar, information_type) for pillar in pillars)
276 missing_pillars = set(pillars) - set([ap.pillar for ap in aps])
277 if len(missing_pillars):
278- pillar_str = ', '.join([p.name for p in missing_pillars])
279+ pillar_str = ', '.join([p.name for p in missing_pillars])
280 raise AssertionError(
281 "Pillar(s) %s require an access policy for information type "
282 "%s." % (pillar_str, information_type.title))
283@@ -97,20 +97,25 @@
284 bug = Reference(bug_id, 'Bug.id')
285 branch_id = Int(name='branch')
286 branch = Reference(branch_id, 'Branch.id')
287+ specification_id = Int(name='specification')
288+ specification = Reference(specification_id, 'Specification.id')
289
290 @property
291 def concrete_artifact(self):
292- artifact = self.bug or self.branch
293+ artifact = self.bug or self.branch or self.specification
294 return artifact
295
296 @classmethod
297 def _constraintForConcrete(cls, concrete_artifact):
298+ from lp.blueprints.interfaces.specification import ISpecification
299 from lp.bugs.interfaces.bug import IBug
300 from lp.code.interfaces.branch import IBranch
301 if IBug.providedBy(concrete_artifact):
302 col = cls.bug
303 elif IBranch.providedBy(concrete_artifact):
304 col = cls.branch
305+ elif ISpecification.providedBy(concrete_artifact):
306+ col = cls.specification
307 else:
308 raise ValueError(
309 "%r is not a valid artifact" % concrete_artifact)
310@@ -128,6 +133,7 @@
311 @classmethod
312 def ensure(cls, concrete_artifacts):
313 """See `IAccessArtifactSource`."""
314+ from lp.blueprints.interfaces.specification import ISpecification
315 from lp.bugs.interfaces.bug import IBug
316 from lp.code.interfaces.branch import IBranch
317
318@@ -143,12 +149,16 @@
319 insert_values = []
320 for concrete in needed:
321 if IBug.providedBy(concrete):
322- insert_values.append((concrete, None))
323+ insert_values.append((concrete, None, None))
324 elif IBranch.providedBy(concrete):
325- insert_values.append((None, concrete))
326+ insert_values.append((None, concrete, None))
327+ elif ISpecification.providedBy(concrete):
328+ insert_values.append((None, None, concrete))
329 else:
330 raise ValueError("%r is not a supported artifact" % concrete)
331- new = create((cls.bug, cls.branch), insert_values, get_objects=True)
332+ new = create(
333+ (cls.bug, cls.branch, cls.specification),
334+ insert_values, get_objects=True)
335 return list(existing) + new
336
337 @classmethod
338
339=== modified file 'lib/lp/registry/services/sharingservice.py'
340--- lib/lp/registry/services/sharingservice.py 2012-09-05 22:02:39 +0000
341+++ lib/lp/registry/services/sharingservice.py 2012-09-10 10:23:19 +0000
342@@ -523,7 +523,7 @@
343 user, artifacts, grantee=grantee, pillar=pillar)
344
345 def ensureAccessGrants(self, grantees, user, branches=None, bugs=None,
346- ignore_permissions=False):
347+ specifications=None, ignore_permissions=False):
348 """See `ISharingService`."""
349
350 artifacts = []
351@@ -531,6 +531,8 @@
352 artifacts.extend(branches)
353 if bugs:
354 artifacts.extend(bugs)
355+ if specifications:
356+ artifacts.extend(specifications)
357 if not ignore_permissions:
358 # The user needs to have launchpad.Edit permission on all supplied
359 # bugs and branches or else we raise an Unauthorized exception.
360
361=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
362--- lib/lp/registry/services/tests/test_sharingservice.py 2012-09-05 22:02:39 +0000
363+++ lib/lp/registry/services/tests/test_sharingservice.py 2012-09-10 10:23:19 +0000
364@@ -10,9 +10,11 @@
365 import transaction
366 from zope.component import getUtility
367 from zope.security.interfaces import Unauthorized
368+from zope.security.proxy import removeSecurityProxy
369 from zope.traversing.browser.absoluteurl import absoluteURL
370
371 from lp.app.interfaces.services import IService
372+from lp.blueprints.interfaces.specification import ISpecification
373 from lp.bugs.interfaces.bug import IBug
374 from lp.code.enums import (
375 BranchSubscriptionNotificationLevel,
376@@ -43,6 +45,7 @@
377 admin_logged_in,
378 login,
379 login_person,
380+ person_logged_in,
381 StormStatementRecorder,
382 TestCaseWithFactory,
383 WebServiceTestCase,
384@@ -1011,22 +1014,26 @@
385 ValueError, self.service.revokeAccessGrants,
386 product, grantee, product.owner)
387
388- def _assert_ensureAccessGrants(self, user, bugs, branches,
389+ def _assert_ensureAccessGrants(self, user, bugs, branches, specifications,
390 grantee=None):
391 # Creating access grants works as expected.
392 if not grantee:
393 grantee = self.factory.makePerson()
394 self.service.ensureAccessGrants(
395- [grantee], user, bugs=bugs, branches=branches)
396+ [grantee], user, bugs=bugs, branches=branches,
397+ specifications=specifications)
398
399 # Check that grantee has expected access grants.
400 shared_bugs = []
401 shared_branches = []
402+ shared_specifications = []
403 all_pillars = []
404 for bug in bugs or []:
405 all_pillars.extend(bug.affected_pillars)
406 for branch in branches or []:
407 all_pillars.append(branch.target.context)
408+ for specification in specifications or []:
409+ all_pillars.append(specification.target)
410 policies = getUtility(IAccessPolicySource).findByPillar(all_pillars)
411
412 apgfs = getUtility(IAccessPolicyGrantFlatSource)
413@@ -1036,8 +1043,11 @@
414 shared_bugs.append(a.concrete_artifact)
415 elif IBranch.providedBy(a.concrete_artifact):
416 shared_branches.append(a.concrete_artifact)
417+ elif ISpecification.providedBy(a.concrete_artifact):
418+ shared_specifications.append(a.concrete_artifact)
419 self.assertContentEqual(bugs or [], shared_bugs)
420 self.assertContentEqual(branches or [], shared_branches)
421+ self.assertContentEqual(specifications or [], shared_specifications)
422
423 def test_ensureAccessGrantsBugs(self):
424 # Access grants can be created for bugs.
425@@ -1047,7 +1057,7 @@
426 bug = self.factory.makeBug(
427 target=distro, owner=owner,
428 information_type=InformationType.USERDATA)
429- self._assert_ensureAccessGrants(owner, [bug], None)
430+ self._assert_ensureAccessGrants(owner, [bug], None, None)
431
432 def test_ensureAccessGrantsBranches(self):
433 # Access grants can be created for branches.
434@@ -1057,7 +1067,21 @@
435 branch = self.factory.makeBranch(
436 product=product, owner=owner,
437 information_type=InformationType.USERDATA)
438- self._assert_ensureAccessGrants(owner, None, [branch])
439+ self._assert_ensureAccessGrants(owner, None, [branch], None)
440+
441+ def test_ensureAccessGrantsSpecifications(self):
442+ # Access grants can be created for branches.
443+ owner = self.factory.makePerson()
444+ product = self.factory.makeProduct(owner=owner)
445+ login_person(owner)
446+ specification = self.factory.makeSpecification(
447+ product=product, owner=owner)
448+ removeSecurityProxy(specification.target)._ensurePolicies(
449+ [InformationType.EMBARGOED])
450+ with person_logged_in(owner):
451+ specification.transitionToInformationType(
452+ InformationType.EMBARGOED, owner)
453+ self._assert_ensureAccessGrants(owner, None, None, [specification])
454
455 def test_ensureAccessGrantsExisting(self):
456 # Any existing access grants are retained and new ones created.
457@@ -1075,7 +1099,8 @@
458 self.service.ensureAccessGrants([grantee], owner, bugs=[bug])
459 # Test with a new bug as well as the one for which access is already
460 # granted.
461- self._assert_ensureAccessGrants(owner, [bug, bug2], None, grantee)
462+ self._assert_ensureAccessGrants(
463+ owner, [bug, bug2], None, None, grantee)
464
465 def _assert_ensureAccessGrantsUnauthorized(self, user):
466 # ensureAccessGrants raises an Unauthorized exception if the user
467
468=== modified file 'lib/lp/registry/tests/test_accesspolicy.py'
469--- lib/lp/registry/tests/test_accesspolicy.py 2012-09-03 11:22:25 +0000
470+++ lib/lp/registry/tests/test_accesspolicy.py 2012-09-10 10:23:19 +0000
471@@ -289,6 +289,13 @@
472 return self.factory.makeBug()
473
474
475+class TestAccessArtifactSpecification(BaseAccessArtifactTests,
476+ TestCaseWithFactory):
477+
478+ def getConcreteArtifact(self):
479+ return self.factory.makeSpecification()
480+
481+
482 class TestAccessArtifactGrant(TestCaseWithFactory):
483 layer = DatabaseFunctionalLayer
484