Merge ~pappacena/launchpad:snap-pillar into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: c7955b9825801193bb479a9d3528abff9a661144
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:snap-pillar
Merge into: launchpad:master
Diff against target: 372 lines (+119/-14)
5 files modified
database/schema/security.cfg (+3/-0)
lib/lp/snappy/interfaces/snap.py (+33/-4)
lib/lp/snappy/model/snap.py (+58/-5)
lib/lp/snappy/tests/test_snap.py (+16/-2)
lib/lp/testing/factory.py (+9/-3)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+397458@code.launchpad.net

Commit message

Adding Snap.project to be the (optional) pillar of snaps

Description of the change

This adds a project to be the optional pillar of Snaps (mandatory for private Snaps). Once we create the UI to set this, we should start validating and only allowing private Snaps that belongs to a given pillar (so we can use the pillar's sharing options to control Snap's privacy).

Database patch is available here: https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/397459.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested change.

I've also removed the XXX comment about validating (project + private) attributes at model level. It would break old snaps if we've added that validation in the future.

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Thiago F. Pappacena (pappacena) :
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/database/schema/security.cfg b/database/schema/security.cfg
index bf4b81c..e343a5f 100644
--- a/database/schema/security.cfg
+++ b/database/schema/security.cfg
@@ -302,6 +302,7 @@ public.snapbuild = SELECT, INSERT, UPDATE, DELETE
302public.snapbuildjob = SELECT, INSERT, UPDATE, DELETE302public.snapbuildjob = SELECT, INSERT, UPDATE, DELETE
303public.snapfile = SELECT, INSERT, UPDATE, DELETE303public.snapfile = SELECT, INSERT, UPDATE, DELETE
304public.snapjob = SELECT, INSERT, UPDATE, DELETE304public.snapjob = SELECT, INSERT, UPDATE, DELETE
305public.snapsubscription = SELECT, INSERT, UPDATE, DELETE
305public.snappydistroseries = SELECT, INSERT, UPDATE, DELETE306public.snappydistroseries = SELECT, INSERT, UPDATE, DELETE
306public.snappyseries = SELECT, INSERT, UPDATE, DELETE307public.snappyseries = SELECT, INSERT, UPDATE, DELETE
307public.sourcepackageformatselection = SELECT308public.sourcepackageformatselection = SELECT
@@ -2246,6 +2247,7 @@ type=user
22462247
2247[person-merge-job]2248[person-merge-job]
2248groups=script2249groups=script
2250public.accesspolicyartifact = SELECT
2249public.accessartifactgrant = SELECT, UPDATE, DELETE2251public.accessartifactgrant = SELECT, UPDATE, DELETE
2250public.accesspolicy = SELECT, UPDATE, DELETE2252public.accesspolicy = SELECT, UPDATE, DELETE
2251public.accesspolicygrant = SELECT, UPDATE, DELETE2253public.accesspolicygrant = SELECT, UPDATE, DELETE
@@ -2363,6 +2365,7 @@ public.signedcodeofconduct = SELECT, UPDATE
2363public.snap = SELECT, UPDATE2365public.snap = SELECT, UPDATE
2364public.snapbase = SELECT, UPDATE2366public.snapbase = SELECT, UPDATE
2365public.snapbuild = SELECT, UPDATE2367public.snapbuild = SELECT, UPDATE
2368public.snapsubscription = SELECT, UPDATE, DELETE
2366public.snappyseries = SELECT, UPDATE2369public.snappyseries = SELECT, UPDATE
2367public.sourcepackagename = SELECT2370public.sourcepackagename = SELECT
2368public.sourcepackagepublishinghistory = SELECT, UPDATE2371public.sourcepackagepublishinghistory = SELECT, UPDATE
diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
index 3835b23..9b19b1b 100644
--- a/lib/lp/snappy/interfaces/snap.py
+++ b/lib/lp/snappy/interfaces/snap.py
@@ -1,4 +1,4 @@
1# Copyright 2015-2020 Canonical Ltd. This software is licensed under the1# Copyright 2015-2021 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"""Snap package interfaces."""4"""Snap package interfaces."""
@@ -36,6 +36,7 @@ __all__ = [
36 'SnapBuildRequestStatus',36 'SnapBuildRequestStatus',
37 'SnapNotOwner',37 'SnapNotOwner',
38 'SnapPrivacyMismatch',38 'SnapPrivacyMismatch',
39 'SnapPrivacyPillarError',
39 'SnapPrivateFeatureDisabled',40 'SnapPrivateFeatureDisabled',
40 ]41 ]
4142
@@ -88,7 +89,9 @@ from zope.security.interfaces import (
88 )89 )
8990
90from lp import _91from lp import _
92from lp.app.enums import InformationType
91from lp.app.errors import NameLookupFailed93from lp.app.errors import NameLookupFailed
94from lp.app.interfaces.informationtype import IInformationType
92from lp.app.interfaces.launchpad import IPrivacy95from lp.app.interfaces.launchpad import IPrivacy
93from lp.app.validators.name import name_validator96from lp.app.validators.name import name_validator
94from lp.buildmaster.interfaces.processor import IProcessor97from lp.buildmaster.interfaces.processor import IProcessor
@@ -98,6 +101,7 @@ from lp.code.interfaces.gitrepository import IGitRepository
98from lp.registry.interfaces.distroseries import IDistroSeries101from lp.registry.interfaces.distroseries import IDistroSeries
99from lp.registry.interfaces.person import IPerson102from lp.registry.interfaces.person import IPerson
100from lp.registry.interfaces.pocket import PackagePublishingPocket103from lp.registry.interfaces.pocket import PackagePublishingPocket
104from lp.registry.interfaces.product import IProduct
101from lp.registry.interfaces.role import IHasOwner105from lp.registry.interfaces.role import IHasOwner
102from lp.services.fields import (106from lp.services.fields import (
103 PersonChoice,107 PersonChoice,
@@ -212,7 +216,16 @@ class SnapPrivacyMismatch(Exception):
212 def __init__(self, message=None):216 def __init__(self, message=None):
213 super(SnapPrivacyMismatch, self).__init__(217 super(SnapPrivacyMismatch, self).__init__(
214 message or218 message or
215 "Snap contains private information and cannot be public.")219 "Snap recipe contains private information and cannot be public.")
220
221
222@error_status(http_client.BAD_REQUEST)
223class SnapPrivacyPillarError(Exception):
224 """Private Snaps should be based in a pillar."""
225
226 def __init__(self, message=None):
227 super(SnapPrivacyPillarError, self).__init__(
228 message or "Private Snap recipes should have a pillar.")
216229
217230
218class BadSnapSearchContext(Exception):231class BadSnapSearchContext(Exception):
@@ -658,6 +671,11 @@ class ISnapEditableAttributes(IHasOwner):
658 vocabulary="AllUserTeamsParticipationPlusSelf",671 vocabulary="AllUserTeamsParticipationPlusSelf",
659 description=_("The owner of this snap package.")))672 description=_("The owner of this snap package.")))
660673
674 project = ReferenceChoice(
675 title=_('The project that this Snap is associated with.'),
676 schema=IProduct, vocabulary='Product',
677 required=False, readonly=False)
678
661 distro_series = exported(Reference(679 distro_series = exported(Reference(
662 IDistroSeries, title=_("Distro Series"),680 IDistroSeries, title=_("Distro Series"),
663 required=False, readonly=False,681 required=False, readonly=False,
@@ -825,6 +843,12 @@ class ISnapAdminAttributes(Interface):
825 title=_("Private"), required=False, readonly=False,843 title=_("Private"), required=False, readonly=False,
826 description=_("Whether or not this snap is private.")))844 description=_("Whether or not this snap is private.")))
827845
846 information_type = exported(Choice(
847 title=_("Information type"), vocabulary=InformationType,
848 required=True, readonly=True, default=InformationType.PUBLIC,
849 description=_(
850 "The type of information contained in this Snap recipe.")))
851
828 require_virtualized = exported(Bool(852 require_virtualized = exported(Bool(
829 title=_("Require virtualized builders"), required=True, readonly=False,853 title=_("Require virtualized builders"), required=True, readonly=False,
830 description=_("Only build this snap package on virtual builders.")))854 description=_("Only build this snap package on virtual builders.")))
@@ -850,7 +874,7 @@ class ISnapAdminAttributes(Interface):
850@exported_as_webservice_entry(as_of="beta")874@exported_as_webservice_entry(as_of="beta")
851class ISnap(875class ISnap(
852 ISnapView, ISnapEdit, ISnapEditableAttributes, ISnapAdminAttributes,876 ISnapView, ISnapEdit, ISnapEditableAttributes, ISnapAdminAttributes,
853 IPrivacy):877 IPrivacy, IInformationType):
854 """A buildable snap package."""878 """A buildable snap package."""
855879
856880
@@ -876,7 +900,8 @@ class ISnapSet(Interface):
876 auto_build_archive=None, auto_build_pocket=None,900 auto_build_archive=None, auto_build_pocket=None,
877 require_virtualized=True, processors=None, date_created=None,901 require_virtualized=True, processors=None, date_created=None,
878 private=False, store_upload=False, store_series=None,902 private=False, store_upload=False, store_series=None,
879 store_name=None, store_secrets=None, store_channels=None):903 store_name=None, store_secrets=None, store_channels=None,
904 project=None):
880 """Create an `ISnap`."""905 """Create an `ISnap`."""
881906
882 def exists(owner, name):907 def exists(owner, name):
@@ -885,6 +910,10 @@ class ISnapSet(Interface):
885 def isValidPrivacy(private, owner, branch=None, git_ref=None):910 def isValidPrivacy(private, owner, branch=None, git_ref=None):
886 """Whether or not the privacy context is valid."""911 """Whether or not the privacy context is valid."""
887912
913 def isValidInformationType(
914 information_type, owner, branch=None, git_ref=None):
915 """Whether or not the information type context is valid."""
916
888 @operation_parameters(917 @operation_parameters(
889 owner=Reference(IPerson, title=_("Owner"), required=True),918 owner=Reference(IPerson, title=_("Owner"), required=True),
890 name=TextLine(title=_("Snap name"), required=True))919 name=TextLine(title=_("Snap name"), required=True))
diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
index 7e3d14c..b298a86 100644
--- a/lib/lp/snappy/model/snap.py
+++ b/lib/lp/snappy/model/snap.py
@@ -1,4 +1,4 @@
1# Copyright 2015-2020 Canonical Ltd. This software is licensed under the1# Copyright 2015-2021 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
4from __future__ import absolute_import, print_function, unicode_literals4from __future__ import absolute_import, print_function, unicode_literals
@@ -57,6 +57,10 @@ from lp.app.browser.tales import (
57 ArchiveFormatterAPI,57 ArchiveFormatterAPI,
58 DateTimeFormatterAPI,58 DateTimeFormatterAPI,
59 )59 )
60from lp.app.enums import (
61 InformationType,
62 PRIVATE_INFORMATION_TYPES,
63 )
60from lp.app.errors import IncompatibleArguments64from lp.app.errors import IncompatibleArguments
61from lp.app.interfaces.security import IAuthorization65from lp.app.interfaces.security import IAuthorization
62from lp.buildmaster.enums import BuildStatus66from lp.buildmaster.enums import BuildStatus
@@ -298,6 +302,9 @@ class Snap(Storm, WebhookTargetMixin):
298 owner_id = Int(name='owner', allow_none=False, validator=_validate_owner)302 owner_id = Int(name='owner', allow_none=False, validator=_validate_owner)
299 owner = Reference(owner_id, 'Person.id')303 owner = Reference(owner_id, 'Person.id')
300304
305 project_id = Int(name='project', allow_none=True)
306 project = Reference(project_id, 'Product.id')
307
301 distro_series_id = Int(name='distro_series', allow_none=True)308 distro_series_id = Int(name='distro_series', allow_none=True)
302 distro_series = Reference(distro_series_id, 'DistroSeries.id')309 distro_series = Reference(distro_series_id, 'DistroSeries.id')
303310
@@ -352,6 +359,17 @@ class Snap(Storm, WebhookTargetMixin):
352359
353 private = Bool(name='private', validator=_validate_private)360 private = Bool(name='private', validator=_validate_private)
354361
362 def _valid_information_type(self, attr, value):
363 if not getUtility(ISnapSet).isValidInformationType(
364 value, self.owner, self.branch, self.git_ref):
365 raise SnapPrivacyMismatch
366 return value
367
368 _information_type = DBEnum(
369 enum=InformationType, default=InformationType.PUBLIC,
370 name="information_type",
371 validator=_valid_information_type)
372
355 allow_internet = Bool(name='allow_internet', allow_none=False)373 allow_internet = Bool(name='allow_internet', allow_none=False)
356374
357 build_source_tarball = Bool(name='build_source_tarball', allow_none=False)375 build_source_tarball = Bool(name='build_source_tarball', allow_none=False)
@@ -374,13 +392,17 @@ class Snap(Storm, WebhookTargetMixin):
374 date_created=DEFAULT, private=False, allow_internet=True,392 date_created=DEFAULT, private=False, allow_internet=True,
375 build_source_tarball=False, store_upload=False,393 build_source_tarball=False, store_upload=False,
376 store_series=None, store_name=None, store_secrets=None,394 store_series=None, store_name=None, store_secrets=None,
377 store_channels=None):395 store_channels=None, project=None):
378 """Construct a `Snap`."""396 """Construct a `Snap`."""
379 super(Snap, self).__init__()397 super(Snap, self).__init__()
380398
381 # Set the private flag first so that other validators can perform399 # Set the private flag first so that other validators can perform
382 # suitable privacy checks.400 # suitable privacy checks, but pillar should also be set, since it's
401 # mandatory for private snaps.
402 self.project = project
383 self.private = private403 self.private = private
404 self.information_type = (InformationType.PROPRIETARY if private else
405 InformationType.PUBLIC)
384406
385 self.registrant = registrant407 self.registrant = registrant
386 self.owner = owner408 self.owner = owner
@@ -408,6 +430,17 @@ class Snap(Storm, WebhookTargetMixin):
408 return "<Snap ~%s/+snap/%s>" % (self.owner.name, self.name)430 return "<Snap ~%s/+snap/%s>" % (self.owner.name, self.name)
409431
410 @property432 @property
433 def information_type(self):
434 if self._information_type is None:
435 return (InformationType.PROPRIETARY if self.private
436 else InformationType.PUBLIC)
437 return self._information_type
438
439 @information_type.setter
440 def information_type(self, information_type):
441 self._information_type = information_type
442
443 @property
411 def valid_webhook_event_types(self):444 def valid_webhook_event_types(self):
412 return ["snap:build:0.1"]445 return ["snap:build:0.1"]
413446
@@ -461,6 +494,21 @@ class Snap(Storm, WebhookTargetMixin):
461 return None494 return None
462495
463 @property496 @property
497 def pillar(self):
498 """See `ISnap`."""
499 return self.project
500
501 @pillar.setter
502 def pillar(self, pillar):
503 if pillar is None:
504 self.project = None
505 elif IProduct.providedBy(pillar):
506 self.project = pillar
507 else:
508 raise ValueError(
509 'The pillar of a Snap must be an IProduct instance.')
510
511 @property
464 def available_processors(self):512 def available_processors(self):
465 """See `ISnap`."""513 """See `ISnap`."""
466 clauses = [Processor.id == DistroArchSeries.processor_id]514 clauses = [Processor.id == DistroArchSeries.processor_id]
@@ -1098,7 +1146,7 @@ class SnapSet:
1098 processors=None, date_created=DEFAULT, private=False,1146 processors=None, date_created=DEFAULT, private=False,
1099 allow_internet=True, build_source_tarball=False,1147 allow_internet=True, build_source_tarball=False,
1100 store_upload=False, store_series=None, store_name=None,1148 store_upload=False, store_series=None, store_name=None,
1101 store_secrets=None, store_channels=None):1149 store_secrets=None, store_channels=None, project=None):
1102 """See `ISnapSet`."""1150 """See `ISnapSet`."""
1103 if not registrant.inTeam(owner):1151 if not registrant.inTeam(owner):
1104 if owner.is_team:1152 if owner.is_team:
@@ -1150,7 +1198,7 @@ class SnapSet:
1150 build_source_tarball=build_source_tarball,1198 build_source_tarball=build_source_tarball,
1151 store_upload=store_upload, store_series=store_series,1199 store_upload=store_upload, store_series=store_series,
1152 store_name=store_name, store_secrets=store_secrets,1200 store_name=store_name, store_secrets=store_secrets,
1153 store_channels=store_channels)1201 store_channels=store_channels, project=project)
1154 store.add(snap)1202 store.add(snap)
11551203
1156 if processors is None:1204 if processors is None:
@@ -1180,6 +1228,11 @@ class SnapSet:
11801228
1181 return True1229 return True
11821230
1231 def isValidInformationType(self, information_type, owner, branch=None,
1232 git_ref=None):
1233 private = information_type in PRIVATE_INFORMATION_TYPES
1234 return self.isValidPrivacy(private, owner, branch, git_ref)
1235
1183 def _getByName(self, owner, name):1236 def _getByName(self, owner, name):
1184 return IStore(Snap).find(1237 return IStore(Snap).find(
1185 Snap, Snap.owner == owner, Snap.name == name).one()1238 Snap, Snap.owner == owner, Snap.name == name).one()
diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
index 706255a..9c2fda4 100644
--- a/lib/lp/snappy/tests/test_snap.py
+++ b/lib/lp/snappy/tests/test_snap.py
@@ -1,4 +1,4 @@
1# Copyright 2015-2020 Canonical Ltd. This software is licensed under the1# Copyright 2015-2021 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"""Test snap packages."""4"""Test snap packages."""
@@ -131,6 +131,7 @@ from lp.testing import (
131 ANONYMOUS,131 ANONYMOUS,
132 api_url,132 api_url,
133 login,133 login,
134 login_admin,
134 logout,135 logout,
135 person_logged_in,136 person_logged_in,
136 record_two_runs,137 record_two_runs,
@@ -1416,11 +1417,24 @@ class TestSnapSet(TestCaseWithFactory):
1416 self.assertEqual(ref.path, snap.git_path)1417 self.assertEqual(ref.path, snap.git_path)
1417 self.assertEqual(ref, snap.git_ref)1418 self.assertEqual(ref, snap.git_ref)
14181419
1420 def test_private_snap_information_type_compatibility(self):
1421 login_admin()
1422 private_snap = getUtility(ISnapSet).new(
1423 private=True, **self.makeSnapComponents())
1424 self.assertEqual(
1425 InformationType.PROPRIETARY, private_snap.information_type)
1426
1427 public_snap = getUtility(ISnapSet).new(
1428 private=False, **self.makeSnapComponents())
1429 self.assertEqual(
1430 InformationType.PUBLIC, public_snap.information_type)
1431
1419 def test_private_snap_for_public_sources(self):1432 def test_private_snap_for_public_sources(self):
1420 # Creating private snaps for public sources is allowed.1433 # Creating private snaps for public sources is allowed.
1421 [ref] = self.factory.makeGitRefs()1434 [ref] = self.factory.makeGitRefs()
1422 components = self.makeSnapComponents(git_ref=ref)1435 components = self.makeSnapComponents(git_ref=ref)
1423 components['private'] = True1436 components['private'] = True
1437 components['project'] = self.factory.makeProduct()
1424 snap = getUtility(ISnapSet).new(**components)1438 snap = getUtility(ISnapSet).new(**components)
1425 with person_logged_in(components['owner']):1439 with person_logged_in(components['owner']):
1426 self.assertTrue(snap.private)1440 self.assertTrue(snap.private)
@@ -2646,7 +2660,7 @@ class TestSnapWebservice(TestCaseWithFactory):
2646 snap_url, "application/json", json.dumps({"private": False}))2660 snap_url, "application/json", json.dumps({"private": False}))
2647 self.assertEqual(400, response.status)2661 self.assertEqual(400, response.status)
2648 self.assertEqual(2662 self.assertEqual(
2649 b"Snap contains private information and cannot be public.",2663 b"Snap recipe contains private information and cannot be public.",
2650 response.body)2664 response.body)
26512665
2652 def test_cannot_set_private_components_of_public_snap(self):2666 def test_cannot_set_private_components_of_public_snap(self):
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index c833ddf..9914cc8 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -2,7 +2,7 @@
2# NOTE: The first line above must stay first; do not move the copyright2# NOTE: The first line above must stay first; do not move the copyright
3# notice to the top. See http://www.python.org/dev/peps/pep-0263/.3# notice to the top. See http://www.python.org/dev/peps/pep-0263/.
4#4#
5# Copyright 2009-2020 Canonical Ltd. This software is licensed under the5# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
6# GNU Affero General Public License version 3 (see the file LICENSE).6# GNU Affero General Public License version 3 (see the file LICENSE).
77
8"""Testing infrastructure for the Launchpad application.8"""Testing infrastructure for the Launchpad application.
@@ -4754,7 +4754,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
4754 date_created=DEFAULT, private=False, allow_internet=True,4754 date_created=DEFAULT, private=False, allow_internet=True,
4755 build_source_tarball=False, store_upload=False,4755 build_source_tarball=False, store_upload=False,
4756 store_series=None, store_name=None, store_secrets=None,4756 store_series=None, store_name=None, store_secrets=None,
4757 store_channels=None):4757 store_channels=None, project=_DEFAULT):
4758 """Make a new Snap."""4758 """Make a new Snap."""
4759 if registrant is None:4759 if registrant is None:
4760 registrant = self.makePerson()4760 registrant = self.makePerson()
@@ -4772,6 +4772,12 @@ class BareLaunchpadObjectFactory(ObjectFactory):
4772 distribution=distroseries.distribution, owner=owner)4772 distribution=distroseries.distribution, owner=owner)
4773 if auto_build_pocket is None:4773 if auto_build_pocket is None:
4774 auto_build_pocket = PackagePublishingPocket.UPDATES4774 auto_build_pocket = PackagePublishingPocket.UPDATES
4775 if private and project is _DEFAULT:
4776 # If we are creating a private snap and didn't explictly set a
4777 # pillar for it, we must create a pillar.
4778 project = self.makeProduct()
4779 if project is _DEFAULT:
4780 project = None
4775 snap = getUtility(ISnapSet).new(4781 snap = getUtility(ISnapSet).new(
4776 registrant, owner, distroseries, name,4782 registrant, owner, distroseries, name,
4777 require_virtualized=require_virtualized, processors=processors,4783 require_virtualized=require_virtualized, processors=processors,
@@ -4783,7 +4789,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
4783 build_source_tarball=build_source_tarball,4789 build_source_tarball=build_source_tarball,
4784 store_upload=store_upload, store_series=store_series,4790 store_upload=store_upload, store_series=store_series,
4785 store_name=store_name, store_secrets=store_secrets,4791 store_name=store_name, store_secrets=store_secrets,
4786 store_channels=store_channels)4792 store_channels=store_channels, project=project)
4787 if is_stale is not None:4793 if is_stale is not None:
4788 removeSecurityProxy(snap).is_stale = is_stale4794 removeSecurityProxy(snap).is_stale = is_stale
4789 IStore(snap).flush()4795 IStore(snap).flush()