launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #07989
[Merge] lp:~wallyworld/launchpad/subscribe-grants-access into lp:launchpad
Curtis Hovey has proposed merging lp:~wallyworld/launchpad/subscribe-grants-access into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1000045 in Launchpad itself: "Ensure access when subscribing a user to a bug "
https://bugs.launchpad.net/launchpad/+bug/1000045
For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/subscribe-grants-access/+merge/106277
let me merge
--
https://code.launchpad.net/~wallyworld/launchpad/subscribe-grants-access/+merge/106277
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/subscribe-grants-access into lp:launchpad.
=== modified file 'configs/development/launchpad-lazr.conf'
--- configs/development/launchpad-lazr.conf 2012-05-02 01:31:53 +0000
+++ configs/development/launchpad-lazr.conf 2012-05-17 22:13:25 +0000
@@ -221,6 +221,9 @@
password: guest
virtual_host: /
+[sharing_jobs]
+error_dir: /var/tmp/sharing.test
+
[txlongpoll]
launch: True
frontend_port: 22435
=== modified file 'configs/testrunner/launchpad-lazr.conf'
--- configs/testrunner/launchpad-lazr.conf 2012-05-02 01:31:53 +0000
+++ configs/testrunner/launchpad-lazr.conf 2012-05-17 22:13:25 +0000
@@ -162,6 +162,9 @@
[packaging_translations]
error_dir: /var/tmp/lperr.test
+[sharing_jobs]
+error_dir: /var/tmp/sharing.test
+
[upgrade_branches]
error_dir: /var/tmp/codehosting.test
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2012-05-09 13:50:03 +0000
+++ database/schema/security.cfg 2012-05-17 22:13:25 +0000
@@ -1882,13 +1882,17 @@
[sharing-jobs]
groups=script
public.accessartifactgrant = SELECT, UPDATE, DELETE
+public.accesspolicy = SELECT
public.accesspolicygrant = SELECT, UPDATE, DELETE
+public.accesspolicygrantflat = SELECT
public.branch = SELECT
public.branchsubscription = SELECT, UPDATE, DELETE
public.bug = SELECT, UPDATE
public.bugactivity = SELECT, INSERT
public.bugbranch = SELECT
public.bugsubscription = SELECT, UPDATE, DELETE
+public.bugtask = SELECT
+public.bugtaskflat = SELECT
public.distribution = SELECT
public.emailaddress = SELECT
public.job = SELECT, INSERT, UPDATE
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2012-05-11 13:31:27 +0000
+++ lib/lp/bugs/model/bug.py 2012-05-17 22:13:25 +0000
@@ -4,6 +4,7 @@
# pylint: disable-msg=E0611,W0212
"""Launchpad bug-related database table classes."""
+from lp.app.interfaces.services import IService
__metaclass__ = type
@@ -162,6 +163,10 @@
PRIVATE_INFORMATION_TYPES,
SECURITY_INFORMATION_TYPES,
)
+from lp.registry.interfaces.accesspolicy import (
+ IAccessArtifactGrantSource,
+ IAccessArtifactSource,
+ )
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.distroseries import IDistroSeries
from lp.registry.interfaces.person import (
@@ -838,6 +843,15 @@
# Ensure that the subscription has been flushed.
Store.of(sub).flush()
+ # Grant the subscriber access if they can't see the bug (if the
+ # database triggers aren't going to do it for us).
+ trigger_flag = 'disclosure.access_mirror_triggers.removed'
+ if bool(getFeatureFlag(trigger_flag)):
+ service = getUtility(IService, 'sharing')
+ bugs, ignored = service.getVisibleArtifacts(person, bugs=[self])
+ if not bugs:
+ service.createAccessGrants(subscribed_by, person, bugs=[self])
+
# In some cases, a subscription should be created without
# email notifications. suppress_notify determines if
# notifications are sent.
@@ -880,6 +894,14 @@
store.flush()
self.updateHeat()
del get_property_cache(self)._known_viewers
+
+ # Revoke access to bug if feature flag is on.
+ flag = 'disclosure.legacy_subscription_visibility.enabled'
+ if bool(getFeatureFlag(flag)):
+ artifacts_to_delete = getUtility(
+ IAccessArtifactSource).find([self])
+ getUtility(IAccessArtifactGrantSource).revokeByArtifact(
+ artifacts_to_delete, [person])
return
def unsubscribeFromDupes(self, person, unsubscribed_by):
=== modified file 'lib/lp/bugs/tests/test_bugvisibility.py'
--- lib/lp/bugs/tests/test_bugvisibility.py 2012-05-09 06:12:52 +0000
+++ lib/lp/bugs/tests/test_bugvisibility.py 2012-05-17 22:13:25 +0000
@@ -4,21 +4,28 @@
"""Tests for visibility of a bug."""
from lp.registry.enums import InformationType
+from lp.services.features.testing import FeatureFixture
from lp.testing import (
celebrity_logged_in,
TestCaseWithFactory,
)
+from lp.testing.fixture import DisableTriggerFixture
from lp.testing.layers import (
DatabaseFunctionalLayer,
- LaunchpadFunctionalLayer,
+ ZopelessDatabaseLayer,
)
+LEGACY_VISIBILITY_FLAG = {
+ u"disclosure.legacy_subscription_visibility.enabled": u"true"}
+TRIGGERS_REMOVED_FLAG = {
+ u"disclosure.access_mirror_triggers.removed": u"true"}
+
+
class TestPublicBugVisibility(TestCaseWithFactory):
"""Test visibility for a public bug."""
layer = DatabaseFunctionalLayer
- #layer = LaunchpadFunctionalLayer
def setUp(self):
super(TestPublicBugVisibility, self).setUp()
@@ -38,7 +45,7 @@
class TestPrivateBugVisibility(TestCaseWithFactory):
"""Test visibility for a private bug."""
- layer = LaunchpadFunctionalLayer
+ layer = ZopelessDatabaseLayer
def setUp(self):
super(TestPrivateBugVisibility, self).setUp()
@@ -78,6 +85,63 @@
self.bug.subscribe(user, self.owner)
self.assertTrue(self.bug.userCanView(user))
- def test_publicBugAnonUser(self):
+ def test_privateBugAnonUser(self):
# Since the bug is private, the anonymous user cannot see it.
self.assertFalse(self.bug.userCanView(None))
+
+ @property
+ def disable_trigger_fixture(self):
+ return DisableTriggerFixture(
+ {'bugsubscription':
+ 'bugsubscription_mirror_legacy_access_t',
+ 'bug': 'bug_mirror_legacy_access_t',
+ 'bugtask': 'bugtask_mirror_legacy_access_t',
+ })
+
+ def test_privateBugUnsubscribeRevokesVisibility(self):
+ # A person unsubscribed from a private bug can no longer see it.
+ # Requires feature flag since the default model behaviour is to leave
+ # any access grants untouched.
+ # This test disables the current temporary database triggers which
+ # mirror subscription status into visibility.
+ with FeatureFixture(LEGACY_VISIBILITY_FLAG):
+ user = self.factory.makePerson()
+ with celebrity_logged_in('admin'):
+ self.bug.subscribe(user, self.owner)
+ self.assertTrue(self.bug.userCanView(user))
+ with self.disable_trigger_fixture:
+ self.bug.unsubscribe(user, self.owner)
+ self.assertFalse(self.bug.userCanView(user))
+
+ def test_privateBugUnsubscribeRetainsVisibility(self):
+ # A person unsubscribed from a private bug can still see it if the
+ # feature flag to enable legacy subscription visibility is not set.
+ # This test disables the current temporary database triggers which
+ # mirror subscription status into visibility.
+ user = self.factory.makePerson()
+ with celebrity_logged_in('admin'):
+ self.bug.subscribe(user, self.owner)
+ self.assertTrue(self.bug.userCanView(user))
+ with self.disable_trigger_fixture:
+ self.bug.unsubscribe(user, self.owner)
+ self.assertTrue(self.bug.userCanView(user))
+
+ def test_subscribeGrantsVisibilityWithTriggersRemoved(self):
+ # When a user is subscribed to a bug, they are granted access. In this
+ # test, the database triggers are removed and so model code is used.
+ with FeatureFixture(TRIGGERS_REMOVED_FLAG):
+ with self.disable_trigger_fixture:
+ user = self.factory.makePerson()
+ self.assertFalse(self.bug.userCanView(user))
+ with celebrity_logged_in('admin'):
+ self.bug.subscribe(user, self.owner)
+ self.assertTrue(self.bug.userCanView(user))
+
+ def test_subscribeGrantsVisibilityUsingTriggers(self):
+ # When a user is subscribed to a bug, they are granted access. In this
+ # test, the database triggers are used.
+ user = self.factory.makePerson()
+ self.assertFalse(self.bug.userCanView(user))
+ with celebrity_logged_in('admin'):
+ self.bug.subscribe(user, self.owner)
+ self.assertTrue(self.bug.userCanView(user))
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2012-05-04 04:52:24 +0000
+++ lib/lp/code/model/branch.py 2012-05-17 22:13:25 +0000
@@ -825,13 +825,15 @@
"""See `IBranch`."""
return self.getSubscription(person) is not None
- def unsubscribe(self, person, unsubscribed_by):
+ def unsubscribe(self, person, unsubscribed_by, **kwargs):
"""See `IBranch`."""
subscription = self.getSubscription(person)
if subscription is None:
# Silent success seems order of the day (like bugs).
return
- if not subscription.canBeUnsubscribedByUser(unsubscribed_by):
+ ignore_permissions = kwargs.get('ignore_permissions', False)
+ if (not ignore_permissions
+ and not subscription.canBeUnsubscribedByUser(unsubscribed_by)):
raise UserCannotUnsubscribePerson(
'%s does not have permission to unsubscribe %s.' % (
unsubscribed_by.displayname,
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2012-04-12 04:39:58 +0000
+++ lib/lp/registry/configure.zcml 2012-05-17 22:13:25 +0000
@@ -1983,4 +1983,19 @@
<allow
interface="lp.registry.interfaces.accesspolicy.IAccessPolicyGrantFlatSource"/>
</securedutility>
+
+ <!-- Sharing jobs -->
+ <class class=".model.sharingjob.RemoveSubscriptionsJob">
+ <allow interface=".interfaces.sharingjob.IRemoveSubscriptionsJob"/>
+ <allow attributes="
+ context
+ log_name"/>
+ </class>
+
+ <securedutility
+ component=".model.sharingjob.RemoveSubscriptionsJob"
+ provides=".interfaces.sharingjob.IRemoveSubscriptionsJobSource">
+ <allow interface=".interfaces.sharingjob.IRemoveSubscriptionsJobSource"/>
+ </securedutility>
+
</configure>
=== modified file 'lib/lp/registry/interfaces/accesspolicy.py'
--- lib/lp/registry/interfaces/accesspolicy.py 2012-05-15 08:16:09 +0000
+++ lib/lp/registry/interfaces/accesspolicy.py 2012-05-17 22:13:25 +0000
@@ -254,9 +254,34 @@
type.
"""
+ def findIndirectGranteePermissionsByPolicy(policies, grantees=None):
+ """Find teams or users with access grants for the policies.
+
+ This method is similar to findGranteePermissionsByPolicy, but the
+ results contain people who have access by virtue of team membership.
+
+ :param policies: a collection of `IAccessPolicy`s.
+ :param grantees: if not None, the result only includes people in the
+ specified list of grantees.
+ :return: a collection of
+ (`IPerson` sharee, `IAccessPolicy`, permission,
+ [`ITeam`] via_teams, shared_artifact_types)
+ where
+ sharee is the person or team with access
+ permission is a SharingPermission enum value.
+ ALL means the person has an access policy grant and can see all
+ artifacts for the associated pillar.
+ SOME means the person only has specified access artifact grants.
+ via_teams is the team the sharee belongs to in order to gain
+ access. If via is None, then the sharee has direct access.
+ shared_artifact_types contains the information_types for which the
+ user has been granted access for one or more artifacts of that
+ type.
+ """
+
def findArtifactsByGrantee(grantee, policies):
"""Find the `IAccessArtifact`s for grantee and policies.
:param grantee: the access artifact grantee.
- :param policies: a collection of `IAccesPolicy`s.
+ :param policies: a collection of `IAccessPolicy`s.
"""
=== added file 'lib/lp/registry/interfaces/sharingjob.py'
--- lib/lp/registry/interfaces/sharingjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/interfaces/sharingjob.py 2012-05-17 22:13:25 +0000
@@ -0,0 +1,88 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Interfaces for sharing jobs."""
+
+__metaclass__ = type
+
+__all__ = [
+ 'IRemoveSubscriptionsJob',
+ 'IRemoveSubscriptionsJobSource',
+ 'ISharingJob',
+ 'ISharingJobSource',
+ ]
+
+from zope.interface import Attribute
+from zope.schema import (
+ Int,
+ Object,
+ )
+
+from lp import _
+from lp.registry.interfaces.distribution import IDistribution
+from lp.registry.interfaces.person import IPerson
+from lp.registry.interfaces.product import IProduct
+from lp.services.job.interfaces.job import (
+ IJob,
+ IJobSource,
+ IRunnableJob,
+ )
+
+
+class ISharingJob(IRunnableJob):
+ """A Job for sharing related tasks."""
+
+ id = Int(
+ title=_('DB ID'), required=True, readonly=True,
+ description=_("The tracking number for this job."))
+
+ job = Object(title=_('The common Job attributes'), schema=IJob,
+ required=True)
+
+ product = Object(
+ title=_('The product the job is for'),
+ schema=IProduct)
+
+ distro = Object(
+ title=_('The distribution the job is for'),
+ schema=IDistribution)
+
+ grantee = Object(
+ title=_('The grantee the job is for'),
+ schema=IPerson)
+
+ metadata = Attribute('A dict of data about the job.')
+
+ def destroySelf():
+ """Destroy this object."""
+
+ def getErrorRecipients(self):
+ """See `BaseRunnableJob`."""
+
+ def pillar():
+ """Either product or distro, whichever is not None."""
+
+ def requestor():
+ """The person who initiated the job."""
+
+
+class IRemoveSubscriptionsJob(ISharingJob):
+ """Job to remove subscriptions to artifacts for which access is revoked."""
+
+
+class ISharingJobSource(IJobSource):
+ """Base interface for acquiring ISharingJobs."""
+
+ def create(pillar, grantee, metadata):
+ """Create a new ISharingJob."""
+
+
+class IRemoveSubscriptionsJobSource(ISharingJobSource):
+ """An interface for acquiring IRemoveSubscriptionsJobs."""
+
+ def create(pillar, grantee, requestor, bugs=None, branches=None):
+ """Create a new job to revoke access to the specified artifacts.
+
+ If bug and branches are both None, then all subscriptions the grantee
+ may have to any pillar artifacts are removed.
+ """
=== modified file 'lib/lp/registry/interfaces/sharingservice.py'
--- lib/lp/registry/interfaces/sharingservice.py 2012-05-15 08:37:14 +0000
+++ lib/lp/registry/interfaces/sharingservice.py 2012-05-17 22:13:25 +0000
@@ -58,6 +58,18 @@
:return: a (bugtasks, branches) tuple
"""
+ def getVisibleArtifacts(person, branches=None, bugs=None):
+ """Return the artifacts shared with person.
+
+ Given lists of artifacts, return those a person has access to either
+ via a policy grant or artifact grant.
+
+ :param person: the person whose access is being checked.
+ :param branches: the branches to check for which a person has access.
+ :param bugs: the bugs to check for which a person has access.
+ :return: a collection of artifacts the person can see.
+ """
+
def getInformationTypes(pillar):
"""Return the allowed information types for the given pillar."""
@@ -113,22 +125,25 @@
"""
@export_write_operation()
+ @call_with(user=REQUEST_USER)
@operation_parameters(
pillar=Reference(IPillar, title=_('Pillar'), required=True),
sharee=Reference(IPerson, title=_('Sharee'), required=True),
information_types=List(
Choice(vocabulary=InformationType), required=False))
@operation_for_version('devel')
- def deletePillarSharee(pillar, sharee, information_types):
+ def deletePillarSharee(pillar, sharee, user, information_types):
"""Remove a sharee from a pillar.
:param pillar: the pillar from which to remove access
+ :param user: the user making the request
:param sharee: the person or team to remove
:param information_types: if None, remove all access, otherwise just
remove the specified access_policies
"""
@export_write_operation()
+ @call_with(user=REQUEST_USER)
@operation_parameters(
pillar=Reference(IPillar, title=_('Pillar'), required=True),
sharee=Reference(IPerson, title=_('Sharee'), required=True),
@@ -137,11 +152,30 @@
branches=List(
Reference(schema=IBranch), title=_('Branches'), required=False))
@operation_for_version('devel')
- def revokeAccessGrants(pillar, sharee, branches=None, bugs=None):
+ def revokeAccessGrants(pillar, user, sharee, branches=None, bugs=None):
"""Remove a sharee's access to the specified artifacts.
:param pillar: the pillar from which to remove access
+ :param user: the user making the request
:param sharee: the person or team for whom to revoke access
:param bugs: the bugs for which to revoke access
:param branches: the branches for which to revoke access
"""
+
+ @export_write_operation()
+ @call_with(user=REQUEST_USER)
+ @operation_parameters(
+ sharee=Reference(IPerson, title=_('Sharee'), required=True),
+ bugs=List(
+ Reference(schema=IBug), title=_('Bugs'), required=False),
+ branches=List(
+ Reference(schema=IBranch), title=_('Branches'), required=False))
+ @operation_for_version('devel')
+ def createAccessGrants(user, sharee, branches=None, bugs=None):
+ """Grant a sharee access to the specified artifacts.
+
+ :param user: the user making the request
+ :param sharee: the person or team for whom to grant access
+ :param bugs: the bugs for which to grant access
+ :param branches: the branches for which to grant access
+ """
=== modified file 'lib/lp/registry/model/accesspolicy.py'
--- lib/lp/registry/model/accesspolicy.py 2012-04-26 08:12:23 +0000
+++ lib/lp/registry/model/accesspolicy.py 2012-05-17 22:13:25 +0000
@@ -14,12 +14,15 @@
from collections import defaultdict
import pytz
+from storm import Undef
from storm.expr import (
And,
In,
+ Join,
Or,
Select,
SQL,
+ With,
)
from storm.properties import (
DateTime,
@@ -43,6 +46,7 @@
IAccessPolicyGrant,
)
from lp.registry.model.person import Person
+from lp.registry.model.teammembership import TeamParticipation
from lp.services.database.bulk import create
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.enumcol import DBEnum
@@ -359,39 +363,21 @@
Person, Person.id == cls.grantee_id, cls.policy_id.is_in(ids))
@classmethod
- def findGranteePermissionsByPolicy(cls, policies, grantees=None):
- """See `IAccessPolicyGrantFlatSource`."""
- policies_by_id = dict((policy.id, policy) for policy in policies)
-
- # A cache for the sharing permissions, keyed on grantee
- permissions_cache = defaultdict(dict)
- # Information types for which there are shared artifacts.
- shared_artifact_info_types = defaultdict(list)
-
- def set_permission(person):
- # Lookup the permissions from the previously loaded cache.
- return (
- person[0],
- permissions_cache[person[0]],
- shared_artifact_info_types[person[0]])
-
- def load_permissions(people):
- # We now have the grantees and policies we want in the result so
- # load any corresponding permissions and cache them.
- people_by_id = dict(
- (person[0].id, person[0]) for person in people)
+ def _populatePermissionsCache(cls, permissions_cache,
+ shared_artifact_info_types, grantee_ids,
+ policies_by_id, persons_by_id):
all_permission_term = SQL("bool_or(artifact IS NULL) as all")
some_permission_term = SQL(
"bool_or(artifact IS NOT NULL) as some")
constraints = [
- cls.grantee_id.is_in(people_by_id.keys()),
+ cls.grantee_id.is_in(grantee_ids),
cls.policy_id.is_in(policies_by_id.keys())]
result_set = IStore(cls).find(
(cls.grantee_id, cls.policy_id, all_permission_term,
some_permission_term),
*constraints).group_by(cls.grantee_id, cls.policy_id)
for (person_id, policy_id, has_all, has_some) in result_set:
- person = people_by_id[person_id]
+ person = persons_by_id[person_id]
policy = policies_by_id[policy_id]
permissions_cache[person][policy] = (
SharingPermission.ALL if has_all
@@ -399,6 +385,32 @@
if has_some:
shared_artifact_info_types[person].append(policy.type)
+ @classmethod
+ def findGranteePermissionsByPolicy(cls, policies, grantees=None):
+ """See `IAccessPolicyGrantFlatSource`."""
+ policies_by_id = dict((policy.id, policy) for policy in policies)
+
+ # A cache for the sharing permissions, keyed on grantee
+ permissions_cache = defaultdict(dict)
+ # Information types for which there are shared artifacts.
+ shared_artifact_info_types = defaultdict(list)
+
+ def set_permission(person):
+ # Lookup the permissions from the previously loaded cache.
+ return (
+ person[0],
+ permissions_cache[person[0]],
+ shared_artifact_info_types[person[0]])
+
+ def load_permissions(people):
+ # We now have the grantees and policies we want in the result so
+ # load any corresponding permissions and cache them.
+ people_by_id = dict(
+ (person[0].id, person[0]) for person in people)
+ cls._populatePermissionsCache(
+ permissions_cache, shared_artifact_info_types,
+ people_by_id.keys(), policies_by_id, people_by_id)
+
constraints = [cls.policy_id.is_in(policies_by_id.keys())]
if grantees:
grantee_ids = [grantee.id for grantee in grantees]
@@ -417,6 +429,121 @@
result_decorator=set_permission, pre_iter_hook=load_permissions)
@classmethod
+ def _populateIndirectGranteePermissions(cls,
+ policies_by_id, result_set):
+ # A cache for the sharing permissions, keyed on grantee.
+ permissions_cache = defaultdict(dict)
+ # A cache of teams belonged to, keyed by grantee.
+ via_teams_cache = defaultdict(list)
+ grantees_by_id = defaultdict()
+ # Information types for which there are shared artifacts.
+ shared_artifact_info_types = defaultdict(list)
+
+ def set_permission(grantee):
+ # Lookup the permissions from the previously loaded cache.
+ via_team_ids = via_teams_cache[grantee[0].id]
+ via_teams = sorted(
+ [grantees_by_id[team_id] for team_id in via_team_ids],
+ key=lambda x: x.displayname)
+ permissions = permissions_cache[grantee[0]]
+ shared_info_types = shared_artifact_info_types[grantee[0]]
+ # For access via teams, we need to use the team permissions. If a
+ # person has access via more than one team, we use the most
+ # powerful permission of all that are there.
+ for team in via_teams:
+ team_permissions = permissions_cache[team]
+ shared_info_types = []
+ for info_type, permission in team_permissions.items():
+ permission_to_use = permissions.get(info_type, permission)
+ if permission == SharingPermission.ALL:
+ permission_to_use = permission
+ elif permission == SharingPermission.SOME:
+ shared_info_types.append(info_type.type)
+ permissions[info_type] = permission_to_use
+ result = (
+ grantee[0], permissions, via_teams or None,
+ shared_info_types)
+ return result
+
+ def load_teams_and_permissions(grantees):
+ # We now have the grantees we want in the result so load any
+ # associated team memberships and permissions and cache them.
+ if permissions_cache:
+ return
+ store = IStore(cls)
+ for grantee in grantees:
+ grantees_by_id[grantee[0].id] = grantee[0]
+ # Find any teams associated with the grantees. If grantees is a
+ # sliced list (for batching), it may contain indirect grantees but
+ # not the team they belong to so that needs to be fixed below.
+ with_expr = With("grantees", store.find(
+ cls.grantee_id, cls.policy_id.is_in(policies_by_id.keys())
+ ).config(distinct=True)._get_select())
+ result_set = store.with_(with_expr).find(
+ (TeamParticipation.teamID, TeamParticipation.personID),
+ TeamParticipation.personID.is_in(grantees_by_id.keys()),
+ TeamParticipation.teamID.is_in(
+ Select(
+ (SQL("grantees.grantee"),),
+ tables="grantees",
+ distinct=True)))
+ team_ids = set()
+ direct_grantee_ids = set()
+ for team_id, team_member_id in result_set:
+ if team_member_id == team_id:
+ direct_grantee_ids.add(team_member_id)
+ else:
+ via_teams_cache[team_member_id].append(team_id)
+ team_ids.add(team_id)
+ # Remove from the via_teams cache all the direct grantees.
+ for direct_grantee_id in direct_grantee_ids:
+ if direct_grantee_id in via_teams_cache:
+ del via_teams_cache[direct_grantee_id]
+ # Load and cache the additional required teams.
+ persons = store.find(Person, Person.id.is_in(team_ids))
+ for person in persons:
+ grantees_by_id[person.id] = person
+
+ cls._populatePermissionsCache(
+ permissions_cache, shared_artifact_info_types,
+ grantees_by_id.keys(), policies_by_id, grantees_by_id)
+
+ return DecoratedResultSet(
+ result_set,
+ result_decorator=set_permission,
+ pre_iter_hook=load_teams_and_permissions)
+
+ @classmethod
+ def findIndirectGranteePermissionsByPolicy(cls, policies,
+ grantees=None):
+ """See `IAccessPolicyGrantFlatSource`."""
+ policies_by_id = dict((policy.id, policy) for policy in policies)
+
+ grantee_filter = Undef
+ if grantees:
+ grantee_ids = [grantee.id for grantee in grantees]
+ grantee_filter = TeamParticipation.personID.is_in(grantee_ids)
+
+ store = IStore(cls)
+ with_expr = With("grantees", store.find(
+ cls.grantee_id,
+ cls.policy_id.is_in(policies_by_id.keys()))
+ .config(distinct=True)._get_select())
+ result_set = store.with_(with_expr).find(
+ (Person,),
+ In(
+ Person.id,
+ Select(
+ (TeamParticipation.personID,),
+ tables=(TeamParticipation, Join("grantees",
+ SQL("grantees.grantee = TeamParticipation.team"))),
+ where=grantee_filter,
+ distinct=True)))
+
+ return cls._populateIndirectGranteePermissions(
+ policies_by_id, result_set)
+
+ @classmethod
def findArtifactsByGrantee(cls, grantee, policies):
"""See `IAccessPolicyGrantFlatSource`."""
ids = [policy.id for policy in policies]
=== added file 'lib/lp/registry/model/sharingjob.py'
--- lib/lp/registry/model/sharingjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/model/sharingjob.py 2012-05-17 22:13:25 +0000
@@ -0,0 +1,355 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+
+"""Job classes related to the sharing feature are in here."""
+
+__metaclass__ = type
+
+
+__all__ = [
+ 'RemoveSubscriptionsJob',
+ ]
+
+import contextlib
+import logging
+
+from lazr.delegates import delegates
+from lazr.enum import (
+ DBEnumeratedType,
+ DBItem,
+ enumerated_type_registry,
+ )
+import simplejson
+from sqlobject import SQLObjectNotFound
+from storm.expr import (
+ And,
+ In,
+ Not,
+ Select,
+ )
+from storm.locals import (
+ Int,
+ Reference,
+ Unicode,
+ )
+from storm.store import Store
+from zope.component import getUtility
+from zope.interface import (
+ classProvides,
+ implements,
+ )
+
+from lp.bugs.interfaces.bug import IBugSet
+from lp.bugs.model.bug import Bug
+from lp.bugs.model.bugsubscription import BugSubscription
+from lp.bugs.model.bugtaskflat import BugTaskFlat
+from lp.bugs.model.bugtasksearch import get_bug_privacy_filter
+from lp.code.interfaces.branchlookup import IBranchLookup
+from lp.registry.enums import InformationType
+from lp.registry.interfaces.person import IPersonSet
+from lp.registry.interfaces.product import IProduct
+from lp.registry.interfaces.sharingjob import (
+ IRemoveSubscriptionsJob,
+ IRemoveSubscriptionsJobSource,
+ ISharingJob,
+ ISharingJobSource,
+ )
+from lp.registry.model.distribution import Distribution
+from lp.registry.model.person import Person
+from lp.registry.model.product import Product
+from lp.services.config import config
+from lp.services.database.enumcol import EnumCol
+from lp.services.database.lpstorm import IStore
+from lp.services.database.stormbase import StormBase
+from lp.services.job.model.job import (
+ EnumeratedSubclass,
+ Job,
+ )
+from lp.services.job.runner import (
+ BaseRunnableJob,
+ )
+from lp.services.mail.sendmail import format_address_for_person
+from lp.services.webapp import errorlog
+
+
+class SharingJobType(DBEnumeratedType):
+ """Values that ISharingJob.job_type can take."""
+
+ REMOVE_SUBSCRIPTIONS = DBItem(0, """
+ Remove subscriptions of artifacts which are inaccessible.
+
+ This job removes subscriptions to artifacts when access is
+ no longer possible because a user no longer has an access
+ grant (either direct or indirect via team membership).
+ """)
+
+
+class SharingJob(StormBase):
+ """Base class for jobs related to branch merge proposals."""
+
+ implements(ISharingJob)
+
+ __storm_table__ = 'SharingJob'
+
+ id = Int(primary=True)
+
+ job_id = Int('job')
+ job = Reference(job_id, Job.id)
+
+ product_id = Int(name='product')
+ product = Reference(product_id, Product.id)
+
+ distro_id = Int(name='distro')
+ distro = Reference(distro_id, Distribution.id)
+
+ grantee_id = Int(name='grantee')
+ grantee = Reference(grantee_id, Person.id)
+
+ job_type = EnumCol(enum=SharingJobType, notNull=True)
+
+ _json_data = Unicode('json_data')
+
+ @property
+ def metadata(self):
+ return simplejson.loads(self._json_data)
+
+ def __init__(self, job_type, pillar, grantee, metadata):
+ """Constructor.
+
+ :param job_type: The BranchMergeProposalJobType of this job.
+ :param metadata: The type-specific variables, as a JSON-compatible
+ dict.
+ """
+ super(SharingJob, self).__init__()
+ json_data = simplejson.dumps(metadata)
+ self.job = Job()
+ self.job_type = job_type
+ self.grantee = grantee
+ self.product = self.distro = None
+ if IProduct.providedBy(pillar):
+ self.product = pillar
+ else:
+ self.distro = pillar
+ # XXX AaronBentley 2009-01-29 bug=322819: This should be a bytestring,
+ # but the DB representation is unicode.
+ self._json_data = json_data.decode('utf-8')
+
+ def destroySelf(self):
+ Store.of(self).remove(self)
+
+ def makeDerived(self):
+ return SharingJobDerived.makeSubclass(self)
+
+
+class SharingJobDerived(BaseRunnableJob):
+ """Intermediate class for deriving from SharingJob."""
+
+ __metaclass__ = EnumeratedSubclass
+
+ delegates(ISharingJob)
+ classProvides(ISharingJobSource)
+
+ @staticmethod
+ @contextlib.contextmanager
+ def contextManager():
+ """See `IJobSource`."""
+ errorlog.globalErrorUtility.configure('sharing_jobs')
+ yield
+
+ def __init__(self, job):
+ self.context = job
+
+ def __repr__(self):
+ return '<%(job_type)s job for %(grantee)s and %(pillar)s>' % {
+ 'job_type': self.context.job_type.name,
+ 'grantee': self.grantee.displayname,
+ 'pillar': self.pillar_text,
+ }
+
+ @property
+ def pillar(self):
+ if self.product:
+ return self.product
+ else:
+ return self.distro
+
+ @property
+ def pillar_text(self):
+ return self.pillar.displayname if self.pillar else 'all pillars'
+
+ @property
+ def log_name(self):
+ return self.__class__.__name__
+
+ @classmethod
+ def create(cls, pillar, grantee, metadata):
+ base_job = SharingJob(cls.class_job_type, pillar, grantee, metadata)
+ job = cls(base_job)
+ job.celeryRunOnCommit()
+ return job
+
+ @classmethod
+ def get(cls, job_id):
+ """Get a job by id.
+
+ :return: the SharingJob with the specified id, as the
+ current SharingJobDereived subclass.
+ :raises: SQLObjectNotFound if there is no job with the specified id,
+ or its job_type does not match the desired subclass.
+ """
+ job = SharingJob.get(job_id)
+ if job.job_type != cls.class_job_type:
+ raise SQLObjectNotFound(
+ 'No object found with id %d and type %s' % (job_id,
+ cls.class_job_type.title))
+ return cls(job)
+
+ @classmethod
+ def iterReady(cls):
+ """See `IJobSource`.
+
+ This version will emit any ready job based on SharingJob.
+ """
+ store = IStore(SharingJob)
+ jobs = store.find(
+ SharingJob,
+ And(SharingJob.job_type == cls.class_job_type,
+ SharingJob.job_id.is_in(Job.ready_jobs)))
+ return (cls.makeSubclass(job) for job in jobs)
+
+ def getOopsVars(self):
+ """See `IRunnableJob`."""
+ vars = BaseRunnableJob.getOopsVars(self)
+ vars.extend([
+ ('sharing_job_id', self.context.id),
+ ('sharing_job_type', self.context.job_type.title),
+ ('grantee', self.grantee.name)])
+ if self.product:
+ vars.append(('product', self.product.name))
+ if self.distro:
+ vars.append(('distro', self.distro.name))
+ return vars
+
+
+class RemoveSubscriptionsJob(SharingJobDerived):
+ """See `IRemoveSubscriptionsJob`."""
+
+ implements(IRemoveSubscriptionsJob)
+ classProvides(IRemoveSubscriptionsJobSource)
+ class_job_type = SharingJobType.REMOVE_SUBSCRIPTIONS
+
+ config = config.sharing_jobs
+
+ @classmethod
+ def create(cls, pillar, grantee, requestor, information_types=None,
+ bugs=None, branches=None):
+ """See `IRemoveSubscriptionsJob`."""
+
+ bug_ids = [
+ bug.id for bug in bugs or []
+ ]
+ branch_names = [
+ branch.unique_name for branch in branches or []
+ ]
+ information_types = [
+ info_type.value for info_type in information_types or []
+ ]
+ metadata = {
+ 'bug_ids': bug_ids,
+ 'branch_names': branch_names,
+ 'information_types': information_types,
+ 'requestor.id': requestor.id
+ }
+ return super(RemoveSubscriptionsJob, cls).create(
+ pillar, grantee, metadata)
+
+ @property
+ def requestor_id(self):
+ return self.metadata['requestor.id']
+
+ @property
+ def requestor(self):
+ return getUtility(IPersonSet).get(self.requestor_id)
+
+ @property
+ def bug_ids(self):
+ return self.metadata['bug_ids']
+
+ @property
+ def branch_names(self):
+ return self.metadata['branch_names']
+
+ @property
+ def information_types(self):
+ return [
+ enumerated_type_registry[InformationType.name].items[value]
+ for value in self.metadata['information_types']]
+
+ def getErrorRecipients(self):
+ # If something goes wrong we want to let the requestor know as well
+ # as the pillar maintainer (if there is a pillar).
+ result = set()
+ result.add(format_address_for_person(self.requestor))
+ if self.pillar and self.pillar.owner.preferredemail:
+ result.add(format_address_for_person(self.pillar.owner))
+ return list(result)
+
+ def getOperationDescription(self):
+ return ('removing subscriptions for artifacts '
+ 'for %s on %s' % (self.grantee.displayname, self.pillar_text))
+
+ def run(self):
+ """See `IRemoveSubscriptionsJob`."""
+
+ logger = logging.getLogger()
+ logger.info(self.getOperationDescription())
+
+ # Unsubscribe grantee from the specified bugs.
+ if self.bug_ids:
+ bugs = getUtility(IBugSet).getByNumbers(self.bug_ids)
+ for bug in bugs:
+ bug.unsubscribe(
+ self.grantee, self.requestor, ignore_permissions=True)
+
+ # Unsubscribe grantee from the specified branches.
+ if self.branch_names:
+ branches = [
+ getUtility(IBranchLookup).getByUniqueName(branch_name)
+ for branch_name in self.branch_names]
+ for branch in branches:
+ branch.unsubscribe(
+ self.grantee, self.requestor, ignore_permissions=True)
+
+ # If required, unsubscribe all pillar artifacts.
+ if not self.bug_ids and not self.branch_names:
+ self._unsubscribe_pillar_artifacts(self.information_types)
+
+ def _unsubscribe_pillar_artifacts(self, only_information_types):
+ # Unsubscribe grantee from pillar artifacts to which they no longer
+ # have access. If only_information_types is specified, filter by the
+ # specified information types, else unsubscribe from all artifacts.
+
+ # Branches are not handled until information_type is supported.
+
+ # Do the bugs.
+ privacy_filter = get_bug_privacy_filter(self.grantee, use_flat=True)
+ bug_filter = Not(In(
+ Bug.id,
+ Select(
+ (BugTaskFlat.bug_id,),
+ where=privacy_filter)))
+ if only_information_types:
+ bug_filter = And(
+ bug_filter,
+ Bug.information_type.is_in(only_information_types)
+ )
+ store = IStore(BugSubscription)
+ subscribed_invisible_bugs = store.find(
+ Bug,
+ BugSubscription.bug_id == Bug.id,
+ BugSubscription.person == self.grantee,
+ bug_filter)
+ for bug in subscribed_invisible_bugs:
+ bug.unsubscribe(
+ self.grantee, self.requestor, ignore_permissions=True)
=== modified file 'lib/lp/registry/model/teammembership.py'
--- lib/lp/registry/model/teammembership.py 2012-02-21 22:46:28 +0000
+++ lib/lp/registry/model/teammembership.py 2012-05-17 22:13:25 +0000
@@ -42,6 +42,7 @@
IMembershipNotificationJobSource,
)
from lp.registry.interfaces.role import IPersonRoles
+from lp.registry.interfaces.sharingjob import IRemoveSubscriptionsJobSource
from lp.registry.interfaces.teammembership import (
ACTIVE_STATES,
CyclicalTeamMembershipError,
@@ -384,6 +385,12 @@
_fillTeamParticipation(self.person, self.team)
elif old_status in ACTIVE_STATES:
_cleanTeamParticipation(self.person, self.team)
+ # A person has left the team so they may no longer have access to
+ # some artifacts shared with the team. We need to run a job to
+ # remove any subscriptions to such artifacts.
+ getUtility(IRemoveSubscriptionsJobSource).create(
+ None, self.person, user)
+
else:
# Changed from an inactive state to another inactive one, so no
# need to fill/clean the TeamParticipation table.
=== modified file 'lib/lp/registry/services/sharingservice.py'
--- lib/lp/registry/services/sharingservice.py 2012-05-16 02:36:30 +0000
+++ lib/lp/registry/services/sharingservice.py 2012-05-17 22:13:25 +0000
@@ -8,6 +8,8 @@
'SharingService',
]
+from itertools import product
+
from lazr.restful.interfaces import IWebBrowserOriginatingRequest
from lazr.restful.utils import get_current_web_service_request
from zope.component import getUtility
@@ -31,14 +33,17 @@
IAccessPolicyGrantSource,
IAccessPolicySource,
)
-from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.projectgroup import IProjectGroup
+from lp.registry.interfaces.sharingjob import IRemoveSubscriptionsJobSource
from lp.registry.interfaces.sharingservice import ISharingService
from lp.registry.model.person import Person
from lp.services.features import getFeatureFlag
from lp.services.searchbuilder import any
-from lp.services.webapp.authorization import available_with_permission
+from lp.services.webapp.authorization import (
+ available_with_permission,
+ check_permission,
+ )
class SharingService:
@@ -57,8 +62,11 @@
@property
def write_enabled(self):
- return bool(getFeatureFlag(
- 'disclosure.enhanced_sharing.writable'))
+ return (
+ bool(getFeatureFlag(
+ 'disclosure.enhanced_sharing.writable') or
+ bool(getFeatureFlag(
+ 'disclosure.access_mirror_triggers.removed'))))
def getSharedArtifacts(self, pillar, person, user):
"""See `ISharingService`."""
@@ -88,6 +96,33 @@
return bugtasks, branches
+ def getVisibleArtifacts(self, person, branches=None, bugs=None):
+ """See `ISharingService`."""
+ bugs_by_id = {}
+ branches_by_id = {}
+ for bug in bugs or []:
+ bugs_by_id[bug.id] = bug
+ for branch in branches or []:
+ branches_by_id[branch.id] = branch
+
+ # Load the bugs.
+ visible_bug_ids = []
+ if bugs_by_id:
+ param = BugTaskSearchParams(
+ user=person, bug=any(*bugs_by_id.keys()))
+ visible_bug_ids = list(getUtility(IBugTaskSet).searchBugIds(param))
+ visible_bugs = [bugs_by_id[bug_id] for bug_id in visible_bug_ids]
+
+ # Load the branches.
+ visible_branches = []
+ if branches_by_id:
+ all_branches = getUtility(IAllBranches)
+ wanted_branches = all_branches.visibleByUser(person).withIds(
+ *branches_by_id.keys())
+ visible_branches = list(wanted_branches.getBranches())
+
+ return visible_bugs, visible_branches
+
def getInformationTypes(self, pillar):
"""See `ISharingService`."""
allowed_types = [
@@ -246,7 +281,7 @@
return sharee
@available_with_permission('launchpad.Edit', 'pillar')
- def deletePillarSharee(self, pillar, sharee,
+ def deletePillarSharee(self, pillar, user, sharee,
information_types=None):
"""See `ISharingService`."""
@@ -282,8 +317,14 @@
IAccessArtifactGrantSource)
accessartifact_grant_source.revokeByArtifact(to_delete)
+ # Create a job to remove subscriptions for artifacts the sharee can no
+ # longer see.
+ getUtility(IRemoveSubscriptionsJobSource).create(
+ pillar, sharee, user, information_types=information_types)
+
@available_with_permission('launchpad.Edit', 'pillar')
- def revokeAccessGrants(self, pillar, sharee, branches=None, bugs=None):
+ def revokeAccessGrants(self, pillar, user, sharee, branches=None,
+ bugs=None):
"""See `ISharingService`."""
if not self.write_enabled:
@@ -301,3 +342,32 @@
accessartifact_grant_source = getUtility(IAccessArtifactGrantSource)
accessartifact_grant_source.revokeByArtifact(
artifacts_to_delete, [sharee])
+
+ # Create a job to remove subscriptions for artifacts the sharee can no
+ # longer see.
+ getUtility(IRemoveSubscriptionsJobSource).create(
+ pillar, sharee, user, bugs=bugs, branches=branches)
+
+ def createAccessGrants(self, user, sharee, branches=None, bugs=None):
+ """See `ISharingService`."""
+
+ if not self.write_enabled:
+ raise Unauthorized("This feature is not yet enabled.")
+
+ artifacts = []
+ if branches:
+ artifacts.extend(branches)
+ if bugs:
+ artifacts.extend(bugs)
+ # The user needs to have launchpad.Edit permission on all supplied
+ # bugs and branches or else we raise an Unauthorized exception.
+ for artifact in artifacts or []:
+ if not check_permission('launchpad.Edit', artifact):
+ raise Unauthorized
+
+ # Ensure there are access artifacts associated with the bugs and
+ # branches.
+ artifacts = getUtility(IAccessArtifactSource).ensure(artifacts)
+ # Create access to bugs/branches for the specified sharee.
+ getUtility(IAccessArtifactGrantSource).grant(
+ list(product(artifacts, [sharee], [user])))
=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
--- lib/lp/registry/services/tests/test_sharingservice.py 2012-05-15 08:16:09 +0000
+++ lib/lp/registry/services/tests/test_sharingservice.py 2012-05-17 22:13:25 +0000
@@ -13,11 +13,13 @@
from zope.traversing.browser.absoluteurl import absoluteURL
from lp.app.interfaces.services import IService
+from lp.bugs.interfaces.bug import IBug
from lp.code.enums import (
BranchSubscriptionDiffSize,
BranchSubscriptionNotificationLevel,
CodeReviewNotificationLevel,
)
+from lp.code.interfaces.branch import IBranch
from lp.registry.enums import (
InformationType,
SharingPermission,
@@ -30,6 +32,7 @@
)
from lp.registry.services.sharingservice import SharingService
from lp.services.features.testing import FeatureFixture
+from lp.services.job.tests import block_on_job
from lp.services.webapp.interaction import ANONYMOUS
from lp.services.webapp.interfaces import ILaunchpadRoot
from lp.services.webapp.publisher import canonical_url
@@ -43,7 +46,7 @@
)
from lp.testing.layers import (
AppServerLayer,
- DatabaseFunctionalLayer,
+ CeleryJobLayer,
)
from lp.testing.matchers import HasQueryCount
from lp.testing.pages import LaunchpadWebServiceCaller
@@ -51,14 +54,15 @@
WRITE_FLAG = {
'disclosure.enhanced_sharing.writable': 'true',
- 'disclosure.enhanced_sharing_details.enabled': 'true'}
+ 'disclosure.enhanced_sharing_details.enabled': 'true',
+ 'jobs.celery.enabled_classes': 'RemoveSubscriptionsJob'}
DETAILS_FLAG = {'disclosure.enhanced_sharing_details.enabled': 'true'}
class TestSharingService(TestCaseWithFactory):
"""Tests for the SharingService."""
- layer = DatabaseFunctionalLayer
+ layer = CeleryJobLayer
def setUp(self):
super(TestSharingService, self).setUp()
@@ -521,7 +525,8 @@
self.factory.makeAccessPolicyGrant(access_policies[0], another)
# Delete data for a specific information type.
with FeatureFixture(WRITE_FLAG):
- self.service.deletePillarSharee(pillar, grantee, types_to_delete)
+ self.service.deletePillarSharee(
+ pillar, pillar.owner, grantee, types_to_delete)
# Assemble the expected data for the remaining access grants for
# grantee.
expected_data = []
@@ -577,7 +582,7 @@
with FeatureFixture(WRITE_FLAG):
self.assertRaises(
Unauthorized, self.service.deletePillarSharee,
- pillar, [InformationType.USERDATA])
+ pillar, pillar.owner, [InformationType.USERDATA])
def test_deletePillarShareeAnonymous(self):
# Anonymous users are not allowed.
@@ -600,7 +605,69 @@
login_person(owner)
self.assertRaises(
Unauthorized, self.service.deletePillarSharee,
- product, [InformationType.USERDATA])
+ product, product.owner, [InformationType.USERDATA])
+
+ def _assert_deleteShareeRemoveSubscriptions(self,
+ types_to_delete=None):
+ product = self.factory.makeProduct()
+ access_policies = getUtility(IAccessPolicySource).findByPillar(
+ (product,))
+ information_types = [ap.type for ap in access_policies]
+ grantee = self.factory.makePerson()
+ # Make some access policy grants for our sharee.
+ for access_policy in access_policies:
+ self.factory.makeAccessPolicyGrant(access_policy, grantee)
+
+ login_person(product.owner)
+ # Make some bug artifact grants for our sharee.
+ # Branches will be done when information_type attribute is supported.
+ bugs = []
+ for access_policy in access_policies:
+ bug = self.factory.makeBug(
+ product=product, owner=product.owner,
+ information_type=access_policy.type)
+ bugs.append(bug)
+ artifact = self.factory.makeAccessArtifact(concrete=bug)
+ self.factory.makeAccessArtifactGrant(artifact, grantee)
+
+ # Make some access policy grants for another sharee.
+ another = self.factory.makePerson()
+ self.factory.makeAccessPolicyGrant(access_policies[0], another)
+
+ # Subscribe the grantee and other person to the artifacts.
+ for person in [grantee, another]:
+ for bug in bugs:
+ bug.subscribe(person, product.owner)
+
+ # Delete data for specified information types or all.
+ with FeatureFixture(WRITE_FLAG):
+ self.service.deletePillarSharee(
+ product, product.owner, grantee, types_to_delete)
+ with block_on_job(self):
+ transaction.commit()
+
+ expected_information_types = []
+ if types_to_delete is not None:
+ expected_information_types = (
+ set(information_types).difference(types_to_delete))
+ # Check that grantee is unsubscribed.
+ for bug in bugs:
+ if bug.information_type in expected_information_types:
+ self.assertIn(grantee, bug.getDirectSubscribers())
+ else:
+ self.assertNotIn(grantee, bug.getDirectSubscribers())
+ self.assertIn(another, bug.getDirectSubscribers())
+
+ def test_shareeUnsubscribedWhenDeleted(self):
+ # The sharee is unsubscribed from any inaccessible artifacts when their
+ # access is revoked.
+ self._assert_deleteShareeRemoveSubscriptions()
+
+ def test_shareeUnsubscribedWhenDeletedSelectedPolicies(self):
+ # The sharee is unsubscribed from any inaccessible artifacts when their
+ # access to selected policies is revoked.
+ self._assert_deleteShareeRemoveSubscriptions(
+ [InformationType.USERDATA])
def _assert_revokeAccessGrants(self, pillar, bugs, branches):
artifacts = []
@@ -624,6 +691,15 @@
artifact=access_artifact, grantee=person,
grantor=pillar.owner)
+ # Subscribe the grantee and other person to the artifacts.
+ for person in [grantee, someone]:
+ for bug in bugs or []:
+ bug.subscribe(person, pillar.owner)
+ for branch in branches or []:
+ branch.subscribe(grantee,
+ BranchSubscriptionNotificationLevel.NOEMAIL, None,
+ CodeReviewNotificationLevel.NOEMAIL, pillar.owner)
+
# Check that grantee has expected access grants.
accessartifact_grant_source = getUtility(IAccessArtifactGrantSource)
grants = accessartifact_grant_source.findByArtifact(
@@ -633,17 +709,29 @@
with FeatureFixture(WRITE_FLAG):
self.service.revokeAccessGrants(
- pillar, grantee, bugs=bugs, branches=branches)
+ pillar, pillar.owner, grantee, bugs=bugs, branches=branches)
+ with block_on_job(self):
+ transaction.commit()
# The grantee now has no access to anything.
permission_info = apgfs.findGranteePermissionsByPolicy(
[policy], [grantee])
self.assertEqual(0, permission_info.count())
+ # Check that the grantee's subscriptions have been removed.
+ # Branches will be done once they have the information_type attribute.
+ for bug in bugs:
+ self.assertNotIn(grantee, bug.getDirectSubscribers())
+
# Someone else still has access to the bugs and branches.
grants = accessartifact_grant_source.findByArtifact(
access_artifacts, [someone])
self.assertEqual(1, grants.count())
+ # Someone else still has subscriptions to the bugs and branches.
+ for bug in bugs:
+ self.assertIn(someone, bug.getDirectSubscribers())
+ for branch in branches:
+ self.assertIn(someone, branch.subscribers)
def test_revokeAccessGrantsBugs(self):
# Users with launchpad.Edit can delete all access for a sharee.
@@ -674,7 +762,7 @@
with FeatureFixture(WRITE_FLAG):
self.assertRaises(
Unauthorized, self.service.revokeAccessGrants,
- product, sharee, bugs=[bug])
+ product, product.owner, sharee, bugs=[bug])
def test_revokeAccessGrantsAnonymous(self):
# Anonymous users are not allowed.
@@ -698,7 +786,90 @@
login_person(owner)
self.assertRaises(
Unauthorized, self.service.revokeAccessGrants,
- product, sharee, bugs=[bug])
+ product, product.owner, sharee, bugs=[bug])
+
+ def _assert_createAccessGrants(self, user, bugs, branches):
+ # Creating access grants works as expected.
+ grantee = self.factory.makePerson()
+ with FeatureFixture(WRITE_FLAG):
+ self.service.createAccessGrants(
+ user, grantee, bugs=bugs, branches=branches)
+
+ # Check that grantee has expected access grants.
+ shared_bugs = []
+ shared_branches = []
+ all_pillars = []
+ for bug in bugs or []:
+ all_pillars.extend(bug.affected_pillars)
+ for branch in branches or []:
+ all_pillars.append(branch.target.context)
+ policies = getUtility(IAccessPolicySource).findByPillar(all_pillars)
+
+ apgfs = getUtility(IAccessPolicyGrantFlatSource)
+ access_artifacts = apgfs.findArtifactsByGrantee(grantee, policies)
+ for a in access_artifacts:
+ if IBug.providedBy(a.concrete_artifact):
+ shared_bugs.append(a.concrete_artifact)
+ elif IBranch.providedBy(a.concrete_artifact):
+ shared_branches.append(a.concrete_artifact)
+ self.assertContentEqual(bugs or [], shared_bugs)
+ self.assertContentEqual(branches or [], shared_branches)
+
+ def test_createAccessGrantsBugs(self):
+ # Access grants can be created for bugs.
+ owner = self.factory.makePerson()
+ distro = self.factory.makeDistribution(owner=owner)
+ login_person(owner)
+ bug = self.factory.makeBug(
+ distribution=distro, owner=owner,
+ information_type=InformationType.USERDATA)
+ self._assert_createAccessGrants(owner, [bug], None)
+
+ def test_createAccessGrantsBranches(self):
+ # Access grants can be created for branches.
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct(owner=owner)
+ login_person(owner)
+ branch = self.factory.makeBranch(
+ product=product, owner=owner, private=True)
+ self._assert_createAccessGrants(owner, None, [branch])
+
+ def _assert_createAccessGrantsUnauthorized(self, user):
+ # createAccessGrants raises an Unauthorized exception if the user
+ # is not permitted to do so.
+ product = self.factory.makeProduct()
+ bug = self.factory.makeBug(
+ product=product, information_type=InformationType.USERDATA)
+ sharee = self.factory.makePerson()
+ with FeatureFixture(WRITE_FLAG):
+ self.assertRaises(
+ Unauthorized, self.service.createAccessGrants,
+ user, sharee, bugs=[bug])
+
+ def test_createAccessGrantsAnonymous(self):
+ # Anonymous users are not allowed.
+ with FeatureFixture(WRITE_FLAG):
+ login(ANONYMOUS)
+ self._assert_createAccessGrantsUnauthorized(ANONYMOUS)
+
+ def test_createAccessGrantsAnyone(self):
+ # Unauthorized users are not allowed.
+ with FeatureFixture(WRITE_FLAG):
+ anyone = self.factory.makePerson()
+ login_person(anyone)
+ self._assert_createAccessGrantsUnauthorized(anyone)
+
+ def test_createAccessGrants_without_flag(self):
+ # The feature flag needs to be enabled.
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct(owner=owner)
+ bug = self.factory.makeBug(
+ product=product, information_type=InformationType.USERDATA)
+ sharee = self.factory.makePerson()
+ login_person(owner)
+ self.assertRaises(
+ Unauthorized, self.service.createAccessGrants,
+ product.owner, sharee, bugs=[bug])
def test_getSharedArtifacts(self):
# Test the getSharedArtifacts method.
@@ -762,6 +933,53 @@
self.assertContentEqual(bug_tasks[:9], shared_bugtasks)
self.assertContentEqual(branches[:9], shared_branches)
+ def test_getVisibleArtifacts(self):
+ # Test the getVisibleArtifacts method.
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct(owner=owner)
+ grantee = self.factory.makePerson()
+ login_person(owner)
+
+ bugs = []
+ for x in range(0, 10):
+ bug = self.factory.makeBug(
+ product=product, owner=owner,
+ information_type=InformationType.USERDATA)
+ bugs.append(bug)
+ branches = []
+ for x in range(0, 10):
+ branch = self.factory.makeBranch(
+ product=product, owner=owner, private=True)
+ branches.append(branch)
+
+ def grant_access(artifact):
+ access_artifact = self.factory.makeAccessArtifact(
+ concrete=artifact)
+ self.factory.makeAccessArtifactGrant(
+ artifact=access_artifact, grantee=grantee, grantor=owner)
+ return access_artifact
+
+ # Grant access to some of the bugs and branches.
+ for bug in bugs[:5]:
+ grant_access(bug)
+ for branch in branches[:5]:
+ grant_access(branch)
+ # XXX for now we need to subscribe users to the branch in order
+ # for the underlying BranchCollection to allow access. This will
+ # no longer be the case when BranchCollection supports the new
+ # access policy framework.
+ branch.subscribe(
+ grantee, BranchSubscriptionNotificationLevel.NOEMAIL,
+ BranchSubscriptionDiffSize.NODIFF,
+ CodeReviewNotificationLevel.NOEMAIL,
+ owner)
+
+ # Check the results.
+ shared_bugs, shared_branches = self.service.getVisibleArtifacts(
+ grantee, branches, bugs)
+ self.assertContentEqual(bugs[:5], shared_bugs)
+ self.assertContentEqual(branches[:5], shared_branches)
+
class ApiTestMixin:
"""Common tests for launchpadlib and webservice."""
=== modified file 'lib/lp/registry/tests/test_accesspolicy.py'
--- lib/lp/registry/tests/test_accesspolicy.py 2012-04-26 07:54:34 +0000
+++ lib/lp/registry/tests/test_accesspolicy.py 2012-05-17 22:13:25 +0000
@@ -475,62 +475,102 @@
class TestAccessPolicyGrantFlatSource(TestCaseWithFactory):
+
layer = DatabaseFunctionalLayer
+ def setUp(self):
+ super(TestAccessPolicyGrantFlatSource, self).setUp()
+ self.apgfs = getUtility(IAccessPolicyGrantFlatSource)
+
+ def _makePolicyGrants(self):
+ policy_with_no_grantees = self.factory.makeAccessPolicy()
+ policy = self.factory.makeAccessPolicy()
+ policy_grant = self.factory.makeAccessPolicyGrant(policy=policy)
+ return policy, policy_with_no_grantees, policy_grant
+
def test_findGranteesByPolicy(self):
# findGranteesByPolicy() returns anyone with a grant for any of
# the policies or the policies' artifacts.
- apgfs = getUtility(IAccessPolicyGrantFlatSource)
-
- # People with grants on the policy show up.
- policy_with_no_grantees = self.factory.makeAccessPolicy()
- policy = self.factory.makeAccessPolicy()
- policy_grant = self.factory.makeAccessPolicyGrant(policy=policy)
- self.assertContentEqual(
- [policy_grant.grantee],
- apgfs.findGranteesByPolicy([policy, policy_with_no_grantees]))
-
- # But not people with grants on artifacts.
+ # This test checks that people with policy grants are returned.
+ (policy, policy_with_no_grantees,
+ policy_grant) = self._makePolicyGrants()
+ self.assertContentEqual(
+ [policy_grant.grantee],
+ self.apgfs.findGranteesByPolicy([policy, policy_with_no_grantees]))
+
+ def test_findGranteesByPolicyIgnoreArtifactGrants(self):
+ # findGranteesByPolicy() returns anyone with a grant for any of
+ # the policies or the policies' artifacts.
+ # This test checks that people with grants on artifacts which are not
+ # linked to the access policy are ignored.
+ (policy, policy_with_no_grantees,
+ policy_grant) = self._makePolicyGrants()
+ self.assertContentEqual(
+ [policy_grant.grantee],
+ self.apgfs.findGranteesByPolicy([policy, policy_with_no_grantees]))
+ self.factory.makeAccessArtifactGrant()
+ self.assertContentEqual(
+ [policy_grant.grantee],
+ self.apgfs.findGranteesByPolicy([policy, policy_with_no_grantees]))
+
+ def test_findGranteesByPolicyIncludeArtifactGrants(self):
+ # findGranteesByPolicy() returns anyone with a grant for any of
+ # the policies or the policies' artifacts.
+ # This test checks that people with grants on artifacts which are
+ # linked to the access policy are included.
+ (policy, policy_with_no_grantees,
+ policy_grant) = self._makePolicyGrants()
artifact_grant = self.factory.makeAccessArtifactGrant()
self.assertContentEqual(
[policy_grant.grantee],
- apgfs.findGranteesByPolicy([policy, policy_with_no_grantees]))
-
- # Unless the artifacts are linked to the policy.
+ self.apgfs.findGranteesByPolicy([policy, policy_with_no_grantees]))
another_policy = self.factory.makeAccessPolicy()
self.factory.makeAccessPolicyArtifact(
artifact=artifact_grant.abstract_artifact, policy=another_policy)
self.assertContentEqual(
[policy_grant.grantee, artifact_grant.grantee],
- apgfs.findGranteesByPolicy([
+ self.apgfs.findGranteesByPolicy([
policy, another_policy, policy_with_no_grantees]))
def test_findGranteePermissionsByPolicy(self):
# findGranteePermissionsByPolicy() returns anyone with a grant for any
# of the policies or the policies' artifacts.
- apgfs = getUtility(IAccessPolicyGrantFlatSource)
-
- # People with grants on the policy show up.
- policy_with_no_grantees = self.factory.makeAccessPolicy()
- policy = self.factory.makeAccessPolicy()
- policy_grant = self.factory.makeAccessPolicyGrant(policy=policy)
- self.assertContentEqual(
- [(policy_grant.grantee, {policy: SharingPermission.ALL}, [])],
- apgfs.findGranteePermissionsByPolicy(
- [policy, policy_with_no_grantees]))
-
- # But not people with grants on artifacts.
+ # This test checks that people with policy grants are returned.
+ (policy, policy_with_no_grantees,
+ policy_grant) = self._makePolicyGrants()
+ self.assertContentEqual(
+ [(policy_grant.grantee, {policy: SharingPermission.ALL}, [])],
+ self.apgfs.findGranteePermissionsByPolicy(
+ [policy, policy_with_no_grantees]))
+
+ def test_findGranteePermissionsIgnoreArtifactGrants(self):
+ # findGranteePermissionsByPolicy() returns anyone with a grant for any
+ # of the policies or the policies' artifacts.
+ # This test checks that people with grants on artifacts which are not
+ # linked to the access policy are ignored.
+ (policy, policy_with_no_grantees,
+ policy_grant) = self._makePolicyGrants()
+ artifact = self.factory.makeAccessArtifact()
+ self.factory.makeAccessArtifactGrant(
+ artifact=artifact, grantee=policy_grant.grantee)
+ self.factory.makeAccessArtifactGrant(artifact=artifact)
+ self.assertContentEqual(
+ [(policy_grant.grantee, {policy: SharingPermission.ALL}, [])],
+ self.apgfs.findGranteePermissionsByPolicy(
+ [policy, policy_with_no_grantees]))
+
+ def test_findGranteePermissionsIncludeArtifactGrants(self):
+ # findGranteePermissionsByPolicy() returns anyone with a grant for any
+ # of the policies or the policies' artifacts.
+ # This test checks that people with grants on artifacts which are
+ # linked to the access policy are included.
+ (policy, policy_with_no_grantees,
+ policy_grant) = self._makePolicyGrants()
artifact = self.factory.makeAccessArtifact()
artifact_grant = self.factory.makeAccessArtifactGrant(
artifact=artifact, grantee=policy_grant.grantee)
other_artifact_grant = self.factory.makeAccessArtifactGrant(
artifact=artifact)
- self.assertContentEqual(
- [(policy_grant.grantee, {policy: SharingPermission.ALL}, [])],
- apgfs.findGranteePermissionsByPolicy(
- [policy, policy_with_no_grantees]))
-
- # Unless the artifacts are linked to the policy.
another_policy = self.factory.makeAccessPolicy()
self.factory.makeAccessPolicyArtifact(
artifact=artifact_grant.abstract_artifact, policy=another_policy)
@@ -542,16 +582,21 @@
(other_artifact_grant.grantee, {
another_policy: SharingPermission.SOME},
[another_policy.type])],
- apgfs.findGranteePermissionsByPolicy([
+ self.apgfs.findGranteePermissionsByPolicy([
policy, another_policy, policy_with_no_grantees]))
- # Slicing works by person, not by (person, policy).
+ def test_findGranteePermissionsByPolicySlicing(self):
+ # findGranteePermissionsByPolicy() returns anyone with a grant for any
+ # of the policies or the policies' artifacts.
+ # This test checks that slicing works by person, not by
+ # (person, policy).
+ (policy, policy_with_no_grantees,
+ policy_grant) = self._makePolicyGrants()
+ another_policy = self.factory.makeAccessPolicy()
self.assertContentEqual(
[(policy_grant.grantee, {
- policy: SharingPermission.ALL,
- another_policy: SharingPermission.SOME},
- [another_policy.type])],
- apgfs.findGranteePermissionsByPolicy([
+ policy: SharingPermission.ALL}, [])],
+ self.apgfs.findGranteePermissionsByPolicy([
policy, another_policy, policy_with_no_grantees]).order_by(
Person.id)[:1])
@@ -559,31 +604,24 @@
# findGranteePermissionsByPolicy() returns all information types for
# which grantees have been granted access one or more artifacts of that
# type.
- apgfs = getUtility(IAccessPolicyGrantFlatSource)
-
policy_with_no_grantees = self.factory.makeAccessPolicy()
policy = self.factory.makeAccessPolicy()
policy_grant = self.factory.makeAccessPolicyGrant(policy=policy)
-
artifact = self.factory.makeAccessArtifact()
artifact_grant = self.factory.makeAccessArtifactGrant(
artifact=artifact, grantee=policy_grant.grantee)
self.factory.makeAccessPolicyArtifact(
artifact=artifact_grant.abstract_artifact, policy=policy)
-
self.assertContentEqual(
[(policy_grant.grantee, {policy: SharingPermission.ALL},
[policy.type])],
- apgfs.findGranteePermissionsByPolicy(
+ self.apgfs.findGranteePermissionsByPolicy(
[policy, policy_with_no_grantees]))
def test_findGranteePermissionsByPolicy_filter_grantees(self):
# findGranteePermissionsByPolicy() returns anyone with a grant for any
# of the policies or the policies' artifacts so long as the grantee is
# in the specified list of grantees.
- apgfs = getUtility(IAccessPolicyGrantFlatSource)
-
- # People with grants on the policy show up.
policy = self.factory.makeAccessPolicy()
grantee_in_result = self.factory.makePerson()
grantee_not_in_result = self.factory.makePerson()
@@ -593,21 +631,263 @@
policy=policy, grantee=grantee_not_in_result)
self.assertContentEqual(
[(policy_grant.grantee, {policy: SharingPermission.ALL}, [])],
- apgfs.findGranteePermissionsByPolicy(
+ self.apgfs.findGranteePermissionsByPolicy(
[policy], [grantee_in_result]))
+ def _make_IndirectGrants(self):
+ policy_with_no_grantees = self.factory.makeAccessPolicy()
+ policy = self.factory.makeAccessPolicy()
+ indirect_person_grantee = self.factory.makePerson()
+ team_grantee = self.factory.makeTeam(members=[indirect_person_grantee])
+ self.factory.makeAccessPolicyGrant(policy=policy, grantee=team_grantee)
+ return (
+ policy, policy_with_no_grantees, team_grantee,
+ indirect_person_grantee)
+
+ def test_findIndirectGranteePermissionsByPolicy(self):
+ # findIndirectGranteePermissionsByPolicy() returns anyone with a grant
+ # for any of the policies or the policies' artifacts. The result
+ # includes members of teams with access and a list of teams via which
+ # they have gained access.
+ # This test checks that people with policy grants are returned.
+ (policy,
+ policy_with_no_grantees,
+ team_grantee, indirect_person_grantee) = self._make_IndirectGrants()
+ policy_info = {policy: SharingPermission.ALL}
+ expected_grantees = [
+ (member, policy_info, [team_grantee], [])
+ for member in team_grantee.activemembers]
+ expected_grantees.append(
+ (team_grantee, policy_info, None, []))
+ self.assertContentEqual(
+ expected_grantees,
+ self.apgfs.findIndirectGranteePermissionsByPolicy(
+ [policy, policy_with_no_grantees]))
+
+ def test_findIndirectGranteePermissionsIgnoreArtifactGrants(self):
+ # findIndirectGranteePermissionsByPolicy() returns anyone with a grant
+ # for any of the policies or the policies' artifacts. The result
+ # includes members of teams with access and a list of teams via which
+ # they have gained access.
+ # This test checks that people with grants on artifacts which are not
+ # linked to the access policy are ignored.
+ (policy,
+ policy_with_no_grantees,
+ team_grantee, indirect_person_grantee) = self._make_IndirectGrants()
+ artifact = self.factory.makeAccessArtifact()
+ self.factory.makeAccessArtifactGrant(
+ artifact=artifact, grantee=team_grantee)
+ self.factory.makeAccessArtifactGrant(artifact=artifact)
+ policy_info = {policy: SharingPermission.ALL}
+ expected_grantees = [
+ (member, policy_info, [team_grantee], [])
+ for member in team_grantee.activemembers]
+ expected_grantees.append(
+ (team_grantee, policy_info, None, []))
+ self.assertContentEqual(
+ expected_grantees,
+ self.apgfs.findIndirectGranteePermissionsByPolicy(
+ [policy, policy_with_no_grantees]))
+
+ def test_findIndirectGranteePermissionsIncludeArtifactGrants(self):
+ # findIndirectGranteePermissionsByPolicy() returns anyone with a grant
+ # for any of the policies or the policies' artifacts. The result
+ # includes members of teams with access and a list of teams via which
+ # they have gained access.
+ # This test checks that people with grants on artifacts which are
+ # linked to the access policy are included.
+ (policy,
+ policy_with_no_grantees,
+ team_grantee, indirect_person_grantee) = self._make_IndirectGrants()
+ artifact = self.factory.makeAccessArtifact()
+ artifact_grant = self.factory.makeAccessArtifactGrant(
+ artifact=artifact, grantee=team_grantee)
+ other_artifact_grant = self.factory.makeAccessArtifactGrant(
+ artifact=artifact)
+ another_policy = self.factory.makeAccessPolicy()
+ self.factory.makeAccessPolicyArtifact(
+ artifact=artifact_grant.abstract_artifact, policy=another_policy)
+ policy_info = {
+ policy: SharingPermission.ALL,
+ another_policy: SharingPermission.SOME}
+ expected_grantees = [
+ (member, policy_info, [team_grantee], [another_policy.type])
+ for member in team_grantee.activemembers]
+ expected_grantees.append(
+ (team_grantee, policy_info, None, [another_policy.type]))
+ expected_grantees.append(
+ (other_artifact_grant.grantee,
+ {another_policy: SharingPermission.SOME}, None,
+ [another_policy.type]))
+ self.assertContentEqual(
+ expected_grantees,
+ self.apgfs.findIndirectGranteePermissionsByPolicy([
+ policy, another_policy, policy_with_no_grantees]))
+
+ def test_findIndirectGranteePermissionsByPolicySlicing(self):
+ # findIndirectGranteePermissionsByPolicy() returns anyone with a grant
+ # for any of the policies or the policies' artifacts. The result
+ # includes members of teams with access and a list of teams via which
+ # they have gained access.
+ # This test checks that slicing works by person, not by
+ # (person, policy).
+ (policy,
+ policy_with_no_grantees,
+ team_grantee, indirect_person_grantee) = self._make_IndirectGrants()
+ another_policy = self.factory.makeAccessPolicy()
+ self.assertContentEqual(
+ [(indirect_person_grantee, {
+ policy: SharingPermission.ALL}, [team_grantee], [])],
+ self.apgfs.findIndirectGranteePermissionsByPolicy([
+ policy, another_policy, policy_with_no_grantees]).order_by(
+ Person.id)[:1])
+
+ def test_findIndirectGranteePermissionsByPolicy_filter_grantees(
+ self):
+ # findIndirectGranteePermissionsByPolicy() returns anyone with a grant
+ # for any of the policies or the policies' artifacts so long as the
+ # grantee is in the specified list of grantees.
+ policy = self.factory.makeAccessPolicy()
+ grantee_not_in_result = self.factory.makePerson()
+ indirect_person_grantee = self.factory.makePerson()
+ team_grantee = self.factory.makeTeam(members=[indirect_person_grantee])
+ self.factory.makeAccessPolicyGrant(policy=policy, grantee=team_grantee)
+ self.factory.makeAccessPolicyGrant(
+ policy=policy, grantee=grantee_not_in_result)
+ self.assertContentEqual(
+ [(indirect_person_grantee, {policy: SharingPermission.ALL},
+ [team_grantee], [])],
+ self.apgfs.findIndirectGranteePermissionsByPolicy(
+ [policy], [indirect_person_grantee]))
+
+ def test_findIndirectGranteePermissionsMultiTeam(self):
+ # Test that findIndirectGranteePermissionsByPolicy() works correctly
+ # when a user is granted access via membership of more than one team.
+ policy_with_no_grantees = self.factory.makeAccessPolicy()
+ policy = self.factory.makeAccessPolicy()
+ # Make an indirect grantee belonging to team1 and team2.
+ indirect_person_grantee = self.factory.makePerson()
+ team_grantee1 = self.factory.makeTeam(
+ members=[indirect_person_grantee])
+ # Make a team for indirect grantee which should not appear in the
+ # results.
+ self.factory.makeTeam(members=[indirect_person_grantee])
+ team_grantee2 = self.factory.makeTeam(
+ members=[indirect_person_grantee])
+ self.factory.makeAccessPolicyGrant(
+ policy=policy, grantee=team_grantee1)
+ self.factory.makeAccessPolicyGrant(
+ policy=policy, grantee=team_grantee2)
+ policy_info = {policy: SharingPermission.ALL}
+ # Indirect grantee has access.
+ expected_grantees = [
+ (indirect_person_grantee, policy_info,
+ sorted([team_grantee1, team_grantee2],
+ key=lambda x: x.displayname), [])]
+ # All other team members have access.
+ expected_grantees.extend([(member, policy_info, [team_grantee1], [])
+ for member in team_grantee1.activemembers
+ if member != indirect_person_grantee])
+ expected_grantees.extend([(member, policy_info, [team_grantee2], [])
+ for member in team_grantee2.activemembers
+ if member != indirect_person_grantee])
+ # The team itself has access also.
+ expected_grantees.append((team_grantee1, policy_info, None, []))
+ expected_grantees.append((team_grantee2, policy_info, None, []))
+ self.assertContentEqual(
+ expected_grantees,
+ self.apgfs.findIndirectGranteePermissionsByPolicy(
+ [policy, policy_with_no_grantees]))
+
+ def test_findIndirectGranteePermissionsDirect(self):
+ # Test that findIndirectGranteePermissionsByPolicy() works correctly
+ # when a user is granted access directly as well as via membership of a
+ # team.
+ policy_with_no_grantees = self.factory.makeAccessPolicy()
+ policy = self.factory.makeAccessPolicy()
+ # Make an direct grantee.
+ person_grantee = self.factory.makePerson()
+ self.factory.makeAccessPolicyGrant(
+ policy=policy, grantee=person_grantee)
+ # Make a team grantee with the person grantee as a member.
+ team_grantee = self.factory.makeTeam(owner=self.factory.makePerson(),
+ members=[person_grantee])
+ self.factory.makeAccessPolicyGrant(
+ policy=policy, grantee=team_grantee)
+ # Make a team for indirect grantee which should not appear in the
+ # results.
+ self.factory.makeTeam(members=[person_grantee])
+ policy_info = {policy: SharingPermission.ALL}
+ # Direct grantee has access.
+ expected_grantees = [(person_grantee, policy_info, None, [])]
+ # All other team members have indirect access.
+ expected_grantees.extend([(member, policy_info, [team_grantee], [])
+ for member in team_grantee.activemembers
+ if member != person_grantee])
+ # The team itself has access also.
+ expected_grantees.append((team_grantee, policy_info, None, []))
+ self.assertContentEqual(
+ expected_grantees,
+ self.apgfs.findIndirectGranteePermissionsByPolicy(
+ [policy, policy_with_no_grantees]))
+
+ def test_findIndirectGranteePermissionsMultiTeamPermissions(self):
+ # Test that findIndirectGranteePermissionsByPolicy() works correctly
+ # when a user is granted access via membership of more than one team
+ # where each team has a different level of access. If an indirect
+ # grantee has both ALL and SOME, then ALL is shown.
+ policy_with_no_grantees = self.factory.makeAccessPolicy()
+ policy = self.factory.makeAccessPolicy()
+ indirect_person_grantee = self.factory.makePerson()
+
+ # One team has ALL access.
+ team_grantee1 = self.factory.makeTeam(
+ members=[indirect_person_grantee])
+ self.factory.makeAccessPolicyGrant(
+ policy=policy, grantee=team_grantee1)
+
+ # Another team has SOME access.
+ indirect_person_grantee2 = self.factory.makePerson()
+ team_grantee2 = self.factory.makeTeam(
+ members=[indirect_person_grantee, indirect_person_grantee2])
+ artifact = self.factory.makeAccessArtifact()
+ artifact_grant = self.factory.makeAccessArtifactGrant(
+ artifact=artifact, grantee=team_grantee2)
+ self.factory.makeAccessPolicyArtifact(
+ artifact=artifact_grant.abstract_artifact, policy=policy)
+
+ policy_info = {policy: SharingPermission.ALL}
+ expected_grantees = [
+ (indirect_person_grantee, policy_info,
+ sorted([team_grantee1, team_grantee2],
+ key=lambda x: x.displayname), [policy.type])]
+ expected_grantees.extend([(member, policy_info, [team_grantee1], [])
+ for member in team_grantee1.activemembers
+ if member != indirect_person_grantee])
+ expected_grantees.append((team_grantee1, policy_info, None, []))
+ policy_info = {policy: SharingPermission.SOME}
+ expected_grantees.extend(
+ [(member, policy_info, [team_grantee2], [policy.type])
+ for member in team_grantee2.activemembers
+ if member != indirect_person_grantee])
+ expected_grantees.append(
+ (team_grantee2, policy_info, None, [policy.type]))
+ self.assertContentEqual(
+ expected_grantees,
+ self.apgfs.findIndirectGranteePermissionsByPolicy(
+ [policy, policy_with_no_grantees]))
+
def test_findArtifactsByGrantee(self):
# findArtifactsByGrantee() returns the artifacts for grantee for any of
# the policies.
- apgfs = getUtility(IAccessPolicyGrantFlatSource)
policy = self.factory.makeAccessPolicy()
grantee = self.factory.makePerson()
# Artifacts not linked to the policy do not show up.
artifact = self.factory.makeAccessArtifact()
self.factory.makeAccessArtifactGrant(artifact, grantee)
self.assertContentEqual(
- [], apgfs.findArtifactsByGrantee(grantee, [policy]))
+ [], self.apgfs.findArtifactsByGrantee(grantee, [policy]))
# Artifacts linked to the policy do show up.
self.factory.makeAccessPolicyArtifact(artifact=artifact, policy=policy)
self.assertContentEqual(
- [artifact], apgfs.findArtifactsByGrantee(grantee, [policy]))
+ [artifact], self.apgfs.findArtifactsByGrantee(grantee, [policy]))
=== added file 'lib/lp/registry/tests/test_sharingjob.py'
--- lib/lp/registry/tests/test_sharingjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_sharingjob.py 2012-05-17 22:13:25 +0000
@@ -0,0 +1,378 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for SharingJobs."""
+
+__metaclass__ = type
+
+import transaction
+
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+
+from lp.code.enums import (
+ BranchSubscriptionNotificationLevel,
+ CodeReviewNotificationLevel,
+ )
+from lp.registry.enums import InformationType
+from lp.registry.interfaces.accesspolicy import (
+ IAccessArtifactSource,
+ IAccessArtifactGrantSource,
+ )
+from lp.registry.interfaces.person import TeamSubscriptionPolicy
+from lp.registry.interfaces.sharingjob import (
+ IRemoveSubscriptionsJobSource,
+ ISharingJob,
+ ISharingJobSource,
+ )
+from lp.registry.model.sharingjob import (
+ RemoveSubscriptionsJob,
+ SharingJob,
+ SharingJobDerived,
+ SharingJobType,
+ )
+from lp.services.features.testing import FeatureFixture
+from lp.services.job.tests import block_on_job
+from lp.services.mail.sendmail import format_address_for_person
+from lp.testing import (
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import (
+ CeleryJobLayer,
+ DatabaseFunctionalLayer,
+ LaunchpadZopelessLayer,
+ )
+
+
+class SharingJobTestCase(TestCaseWithFactory):
+ """Test case for basic SharingJob class."""
+
+ layer = LaunchpadZopelessLayer
+
+ def test_init(self):
+ pillar = self.factory.makeProduct()
+ grantee = self.factory.makePerson()
+ metadata = ('some', 'arbitrary', 'metadata')
+ sharing_job = SharingJob(
+ SharingJobType.REMOVE_SUBSCRIPTIONS, pillar, grantee, metadata)
+ self.assertEqual(
+ SharingJobType.REMOVE_SUBSCRIPTIONS, sharing_job.job_type)
+ self.assertEqual(pillar, sharing_job.product)
+ self.assertEqual(grantee, sharing_job.grantee)
+ expected_json_data = '["some", "arbitrary", "metadata"]'
+ self.assertEqual(expected_json_data, sharing_job._json_data)
+
+ def test_metadata(self):
+ # The python structure stored as json is returned as python.
+ metadata = {
+ 'a_list': ('some', 'arbitrary', 'metadata'),
+ 'a_number': 1,
+ 'a_string': 'string',
+ }
+ pillar = self.factory.makeProduct()
+ grantee = self.factory.makePerson()
+ sharing_job = SharingJob(
+ SharingJobType.REMOVE_SUBSCRIPTIONS, pillar, grantee, metadata)
+ metadata['a_list'] = list(metadata['a_list'])
+ self.assertEqual(metadata, sharing_job.metadata)
+
+
+class SharingJobDerivedTestCase(TestCaseWithFactory):
+ """Test case for the SharingJobDerived class."""
+
+ layer = DatabaseFunctionalLayer
+
+ def _makeJob(self, prod_name=None, grantee_name=None):
+ pillar = self.factory.makeProduct(name=prod_name)
+ grantee = self.factory.makePerson(name=grantee_name)
+ requestor = self.factory.makePerson()
+ job = getUtility(IRemoveSubscriptionsJobSource).create(
+ pillar, grantee, requestor)
+ return job
+
+ def test_repr(self):
+ job = self._makeJob('prod', 'fred')
+ self.assertEqual(
+ '<REMOVE_SUBSCRIPTIONS job for Fred and Prod>', repr(job))
+
+ def test_create_success(self):
+ # Create an instance of SharingJobDerived that delegates to SharingJob.
+ self.assertIs(True, ISharingJobSource.providedBy(SharingJobDerived))
+ job = self._makeJob()
+ self.assertIsInstance(job, SharingJobDerived)
+ self.assertIs(True, ISharingJob.providedBy(job))
+ self.assertIs(True, ISharingJob.providedBy(job.context))
+
+ def test_create_raises_error(self):
+ # SharingJobDerived.create() raises an error because it
+ # needs to be subclassed to work properly.
+ pillar = self.factory.makeProduct()
+ grantee = self.factory.makePerson()
+ self.assertRaises(
+ AttributeError, SharingJobDerived.create, pillar, grantee, {})
+
+ def test_iterReady(self):
+ # iterReady finds job in the READY status that are of the same type.
+ job_1 = self._makeJob()
+ job_2 = self._makeJob()
+ job_2.start()
+ jobs = list(RemoveSubscriptionsJob.iterReady())
+ self.assertEqual(1, len(jobs))
+ self.assertEqual(job_1, jobs[0])
+
+ def test_log_name(self):
+ # The log_name is the name of the implementing class.
+ job = self._makeJob()
+ self.assertEqual('RemoveSubscriptionsJob', job.log_name)
+
+ def test_getOopsVars(self):
+ # The pillar and grantee name are added to the oops vars.
+ pillar = self.factory.makeDistribution()
+ grantee = self.factory.makePerson()
+ requestor = self.factory.makePerson()
+ job = getUtility(IRemoveSubscriptionsJobSource).create(
+ pillar, grantee, requestor)
+ oops_vars = job.getOopsVars()
+ self.assertIs(True, len(oops_vars) > 4)
+ self.assertIn(('distro', pillar.name), oops_vars)
+ self.assertIn(('grantee', grantee.name), oops_vars)
+
+ def test_getErrorRecipients(self):
+ # The pillar owner and job requestor are the error recipients.
+ pillar = self.factory.makeDistribution()
+ grantee = self.factory.makePerson()
+ requestor = self.factory.makePerson()
+ job = getUtility(IRemoveSubscriptionsJobSource).create(
+ pillar, grantee, requestor)
+ expected_emails = [
+ format_address_for_person(person)
+ for person in (pillar.owner, requestor)]
+ self.assertContentEqual(
+ expected_emails, job.getErrorRecipients())
+
+
+class RemoveSubscriptionsJobTestCase(TestCaseWithFactory):
+ """Test case for the RemoveSubscriptionsJob class."""
+
+ layer = CeleryJobLayer
+
+ def setUp(self):
+ self.useFixture(FeatureFixture({
+ 'jobs.celery.enabled_classes': 'RemoveSubscriptionsJob',
+ }))
+ super(RemoveSubscriptionsJobTestCase, self).setUp()
+
+ def test_create(self):
+ # Create an instance of RemoveSubscriptionsJob that stores
+ # the information type and artifact information.
+ self.assertIs(
+ True,
+ IRemoveSubscriptionsJobSource.providedBy(RemoveSubscriptionsJob))
+ self.assertEqual(
+ SharingJobType.REMOVE_SUBSCRIPTIONS,
+ RemoveSubscriptionsJob.class_job_type)
+ pillar = self.factory.makeProduct()
+ grantee = self.factory.makePerson()
+ requestor = self.factory.makePerson()
+ bug = self.factory.makeBug(product=pillar)
+ branch = self.factory.makeBranch(product=pillar)
+ info_type = InformationType.USERDATA
+ job = getUtility(IRemoveSubscriptionsJobSource).create(
+ pillar, grantee, requestor, [info_type], [bug], [branch])
+ naked_job = removeSecurityProxy(job)
+ self.assertIsInstance(job, RemoveSubscriptionsJob)
+ self.assertEqual(pillar, job.pillar)
+ self.assertEqual(grantee, job.grantee)
+ self.assertEqual(requestor.id, naked_job.requestor_id)
+ self.assertContentEqual([info_type], naked_job.information_types)
+ self.assertContentEqual([bug.id], naked_job.bug_ids)
+ self.assertContentEqual([branch.unique_name], naked_job.branch_names)
+
+ def test_create_no_pillar(self):
+ # Create an instance of RemoveSubscriptionsJob that stores
+ # the information type and artifact information but with no pillar.
+ grantee = self.factory.makePerson()
+ requestor = self.factory.makePerson()
+ job = getUtility(IRemoveSubscriptionsJobSource).create(
+ None, grantee, requestor)
+ naked_job = removeSecurityProxy(job)
+ self.assertIsInstance(job, RemoveSubscriptionsJob)
+ self.assertEqual(None, job.pillar)
+ self.assertEqual(grantee, job.grantee)
+ self.assertEqual(requestor.id, naked_job.requestor_id)
+ self.assertIn('all pillars', repr(job))
+ self.assertEqual(1, len(job.getErrorRecipients()))
+
+ def _make_subscribed_bug(self, grantee, product=None, distribution=None,
+ information_type=InformationType.USERDATA):
+ owner = self.factory.makePerson()
+ bug = self.factory.makeBug(
+ owner=owner, product=product, distribution=distribution,
+ information_type=information_type)
+ with person_logged_in(owner):
+ bug.subscribe(grantee, owner)
+ return bug, owner
+
+ def test_unsubscribe_bugs(self):
+ # The requested bug subscriptions are removed.
+ pillar = self.factory.makeDistribution()
+ grantee = self.factory.makePerson()
+ owner = self.factory.makePerson()
+ bug, ignored = self._make_subscribed_bug(grantee, distribution=pillar)
+ getUtility(IRemoveSubscriptionsJobSource).create(
+ pillar, grantee, owner, bugs=[bug])
+ with block_on_job(self):
+ transaction.commit()
+ self.assertNotIn(
+ grantee, removeSecurityProxy(bug).getDirectSubscribers())
+
+ def _make_subscribed_branch(self, pillar, grantee, private=False):
+ owner = self.factory.makePerson()
+ branch = self.factory.makeBranch(
+ owner=owner, product=pillar, private=private)
+ with person_logged_in(owner):
+ branch.subscribe(grantee,
+ BranchSubscriptionNotificationLevel.NOEMAIL, None,
+ CodeReviewNotificationLevel.NOEMAIL, owner)
+ return branch
+
+ def test_unsubscribe_branches(self):
+ # The requested branch subscriptions are removed.
+ owner = self.factory.makePerson()
+ pillar = self.factory.makeProduct(owner=owner)
+ grantee = self.factory.makePerson()
+ branch = self._make_subscribed_branch(pillar, grantee)
+ getUtility(IRemoveSubscriptionsJobSource).create(
+ pillar, grantee, owner, branches=[branch])
+ with block_on_job(self):
+ transaction.commit()
+ self.assertNotIn(
+ grantee, list(removeSecurityProxy(branch).subscribers))
+
+ def _assert_unsubscribe_pillar_artifacts_direct_bugs(self,
+ pillar=None):
+ # All direct pillar bug subscriptions are removed.
+ grantee = self.factory.makePerson()
+
+ # Make some bugs subscribed to by grantee.
+ bug1, ignored = self._make_subscribed_bug(
+ grantee, product=pillar,
+ information_type=InformationType.EMBARGOEDSECURITY)
+ bug2, ignored = self._make_subscribed_bug(
+ grantee, product=pillar,
+ information_type=InformationType.USERDATA)
+
+ # Subscribing grantee to bugs creates an access grant so we need to
+ # revoke those for our test.
+ accessartifact_source = getUtility(IAccessArtifactSource)
+ accessartifact_grant_source = getUtility(IAccessArtifactGrantSource)
+ accessartifact_grant_source.revokeByArtifact(
+ accessartifact_source.find([bug1, bug2]), [grantee])
+
+ # Now run the job.
+ requestor = self.factory.makePerson()
+ getUtility(IRemoveSubscriptionsJobSource).create(
+ pillar, grantee, requestor)
+ with block_on_job(self):
+ transaction.commit()
+
+ self.assertNotIn(
+ grantee, removeSecurityProxy(bug1).getDirectSubscribers())
+ self.assertNotIn(
+ grantee, removeSecurityProxy(bug2).getDirectSubscribers())
+
+ def test_unsubscribe_pillar_artifacts_direct_bugs(self):
+ pillar = self.factory.makeProduct()
+ self._assert_unsubscribe_pillar_artifacts_direct_bugs(pillar)
+
+ def test_unsubscribe_artifacts_direct_bugs_unspecified_pillar(self):
+ self._assert_unsubscribe_pillar_artifacts_direct_bugs()
+
+ def _assert_unsubscribe_pillar_artifacts_indirect_bugs(self,
+ pillar=None):
+ # Do not delete subscriptions to bugs a user has indirect access to
+ # because they belong to a team which has an artifact grant on the bug.
+
+ person_grantee = self.factory.makePerson(name='grantee')
+
+ # Make a bug the person_grantee is subscribed to.
+ bug1, ignored = self._make_subscribed_bug(
+ person_grantee, product=pillar,
+ information_type=InformationType.USERDATA)
+
+ # Make another bug and grant access to a team.
+ team_owner = self.factory.makePerson(name='teamowner')
+ team_grantee = self.factory.makeTeam(
+ owner=team_owner,
+ subscription_policy=TeamSubscriptionPolicy.RESTRICTED,
+ members=[person_grantee])
+ bug2, bug2_owner = self._make_subscribed_bug(
+ team_grantee, product=pillar,
+ information_type=InformationType.EMBARGOEDSECURITY)
+ # Add a subscription for the person_grantee.
+ with person_logged_in(bug2_owner):
+ bug2.subscribe(person_grantee, bug2_owner)
+
+ # Subscribing person_grantee to bugs creates an access grant so we
+ # need to revoke those for our test.
+ accessartifact_source = getUtility(IAccessArtifactSource)
+ accessartifact_grant_source = getUtility(IAccessArtifactGrantSource)
+ accessartifact_grant_source.revokeByArtifact(
+ accessartifact_source.find([bug1, bug2]), [person_grantee])
+
+ # Now run the job.
+ requestor = self.factory.makePerson()
+ getUtility(IRemoveSubscriptionsJobSource).create(
+ pillar, person_grantee, requestor)
+ with block_on_job(self):
+ transaction.commit()
+
+ # person_grantee is not longer subscribed to bug1.
+ self.assertNotIn(
+ person_grantee, removeSecurityProxy(bug1).getDirectSubscribers())
+ # person_grantee is still subscribed to bug2 because they have access
+ # via a team.
+ self.assertIn(
+ person_grantee, removeSecurityProxy(bug2).getDirectSubscribers())
+
+ def test_unsubscribe_pillar_artifacts_indirect_bugs(self):
+ pillar = self.factory.makeProduct()
+ self._assert_unsubscribe_pillar_artifacts_indirect_bugs(pillar)
+
+ def test_unsubscribe_artifacts_indirect_bugs_unspecified_pillar(self):
+ self._assert_unsubscribe_pillar_artifacts_indirect_bugs()
+
+ def test_unsubscribe_pillar_artifacts_specific_info_types(self):
+ # Only delete pillar artifacts of the specified info type.
+
+ owner = self.factory.makePerson(name='pillarowner')
+ pillar = self.factory.makeProduct(owner=owner)
+ person_grantee = self.factory.makePerson(name='grantee')
+
+ # Make bugs the person_grantee is subscribed to.
+ bug1, ignored = self._make_subscribed_bug(
+ person_grantee, product=pillar,
+ information_type=InformationType.USERDATA)
+
+ bug2, ignored = self._make_subscribed_bug(
+ person_grantee, product=pillar,
+ information_type=InformationType.EMBARGOEDSECURITY)
+
+ # Subscribing grantee to bugs creates an access grant so we
+ # need to revoke those for our test.
+ accessartifact_source = getUtility(IAccessArtifactSource)
+ accessartifact_grant_source = getUtility(IAccessArtifactGrantSource)
+ accessartifact_grant_source.revokeByArtifact(
+ accessartifact_source.find([bug1, bug2]), [person_grantee])
+
+ # Now run the job, removing access to userdata artifacts.
+ getUtility(IRemoveSubscriptionsJobSource).create(
+ pillar, person_grantee, owner, [InformationType.USERDATA])
+ with block_on_job(self):
+ transaction.commit()
+
+ self.assertNotIn(
+ person_grantee, removeSecurityProxy(bug1).getDirectSubscribers())
+ self.assertIn(
+ person_grantee, removeSecurityProxy(bug2).getDirectSubscribers())
=== modified file 'lib/lp/registry/tests/test_teammembership.py'
--- lib/lp/registry/tests/test_teammembership.py 2012-02-27 07:09:17 +0000
+++ lib/lp/registry/tests/test_teammembership.py 2012-05-17 22:13:25 +0000
@@ -23,6 +23,11 @@
from zope.security.proxy import removeSecurityProxy
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.registry.enums import InformationType
+from lp.registry.interfaces.accesspolicy import (
+ IAccessArtifactSource,
+ IAccessArtifactGrantSource,
+ )
from lp.registry.interfaces.person import (
IPersonSet,
TeamMembershipRenewalPolicy,
@@ -53,7 +58,9 @@
flush_database_updates,
sqlvalues,
)
+from lp.services.features.testing import FeatureFixture
from lp.services.log.logger import BufferLogger
+from lp.services.job.tests import block_on_job
from lp.testing import (
login,
login_celebrity,
@@ -65,6 +72,7 @@
)
from lp.testing.dbuser import dbuser
from lp.testing.layers import (
+ CeleryJobLayer,
DatabaseFunctionalLayer,
DatabaseLayer,
LaunchpadZopelessLayer,
@@ -984,6 +992,68 @@
self.assertEqual(TeamMembershipStatus.DEACTIVATED, tm.status)
+class TestTeamMembershipJobs(TestCaseWithFactory):
+ """Test jobs associated with managing team membership."""
+ layer = CeleryJobLayer
+
+ def setUp(self):
+ self.useFixture(FeatureFixture({
+ 'jobs.celery.enabled_classes': 'RemoveSubscriptionsJob',
+ }))
+ super(TestTeamMembershipJobs, self).setUp()
+
+ def _make_subscribed_bug(self, grantee, product=None, distribution=None,
+ information_type=InformationType.USERDATA):
+ owner = self.factory.makePerson()
+ bug = self.factory.makeBug(
+ owner=owner, product=product, distribution=distribution,
+ information_type=information_type)
+ with person_logged_in(owner):
+ bug.subscribe(grantee, owner)
+ return bug, owner
+
+ def test_retract_unsubscribes_former_member(self):
+ # When a team member is removed, any subscriptions to artifacts they
+ # can no longer see are removed also.
+ person_grantee = self.factory.makePerson()
+ product = self.factory.makeProduct()
+ # Make a bug the person_grantee is subscribed to.
+ bug1, ignored = self._make_subscribed_bug(
+ person_grantee, product=product,
+ information_type=InformationType.USERDATA)
+
+ # Make another bug and grant access to a team.
+ team_grantee = self.factory.makeTeam(
+ subscription_policy=TeamSubscriptionPolicy.RESTRICTED,
+ members=[person_grantee])
+ bug2, bug2_owner = self._make_subscribed_bug(
+ team_grantee, product=product,
+ information_type=InformationType.EMBARGOEDSECURITY)
+ # Add a subscription for the person_grantee.
+ with person_logged_in(bug2_owner):
+ bug2.subscribe(person_grantee, bug2_owner)
+
+ # Subscribing person_grantee to bugs creates an access grant so we
+ # need to revoke the one to bug2 for our test.
+ accessartifact_source = getUtility(IAccessArtifactSource)
+ accessartifact_grant_source = getUtility(IAccessArtifactGrantSource)
+ accessartifact_grant_source.revokeByArtifact(
+ accessartifact_source.find([bug2]), [person_grantee])
+
+ with person_logged_in(person_grantee):
+ person_grantee.retractTeamMembership(team_grantee, person_grantee)
+ with block_on_job(self):
+ transaction.commit()
+
+ # person_grantee is still subscribed to bug1.
+ self.assertIn(
+ person_grantee, removeSecurityProxy(bug1).getDirectSubscribers())
+ # person_grantee is not subscribed to bug2 because they no longer have
+ # access via a team.
+ self.assertNotIn(
+ person_grantee, removeSecurityProxy(bug2).getDirectSubscribers())
+
+
class TestTeamMembershipSendExpirationWarningEmail(TestCaseWithFactory):
"""Test the behaviour of sendExpirationWarningEmail()."""
layer = DatabaseFunctionalLayer
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf 2012-05-03 03:50:57 +0000
+++ lib/lp/services/config/schema-lazr.conf 2012-05-17 22:13:25 +0000
@@ -1558,6 +1558,14 @@
# datatype: string
virtual_host: none
+[sharing_jobs]
+# The database user which will be used by this process.
+# datatype: string
+dbuser: sharing-jobs
+
+# See [error_reports].
+error_dir: none
+
[txlongpoll]
# Should TxLongPoll be launched by default?
# datatype: boolean
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2012-05-08 01:31:10 +0000
+++ lib/lp/services/features/flags.py 2012-05-17 22:13:25 +0000
@@ -281,6 +281,13 @@
'',
'',
''),
+ ('disclosure.legacy_subscription_visibility.enabled',
+ 'boolean',
+ ('If true, the legacy behaviour of unsubscribing from a bug or branch'
+ 'revokes access.'),
+ '',
+ '',
+ ''),
('disclosure.enhanced_sharing.writable',
'boolean',
('If true, will allow the use of the new sharing view and apis used '
@@ -288,6 +295,13 @@
'',
'Sharing management',
''),
+ ('disclosure.access_mirror_triggers.removed',
+ 'boolean',
+ ('If true, the database triggers which cause subscribing to grant'
+ 'access to a bug or branch have been removed.'),
+ '',
+ '',
+ ''),
('garbo.workitem_migrator.enabled',
'boolean',
('If true, garbo will try to migrate work items from the whiteboard of '
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2012-05-04 14:52:44 +0000
+++ lib/lp/testing/factory.py 2012-05-17 22:13:25 +0000
@@ -1122,6 +1122,12 @@
if private:
removeSecurityProxy(branch).explicitly_private = True
removeSecurityProxy(branch).transitively_private = True
+ # XXX this is here till branch properly supports information_type
+ [artifact] = getUtility(IAccessArtifactSource).ensure([branch])
+ [policy] = getUtility(IAccessPolicySource).find(
+ [(branch.target.context, InformationType.USERDATA)])
+ getUtility(IAccessPolicyArtifactSource).create([(artifact, policy)])
+
if stacked_on is not None:
removeSecurityProxy(branch).stacked_on = stacked_on
if reviewer is not None:
=== modified file 'lib/lp/testing/fixture.py'
--- lib/lp/testing/fixture.py 2012-05-15 20:47:25 +0000
+++ lib/lp/testing/fixture.py 2012-05-17 22:13:25 +0000
@@ -7,6 +7,7 @@
__all__ = [
'CaptureOops',
'DemoMode',
+ 'DisableTriggerFixture',
'PGBouncerFixture',
'PGNotReadyError',
'Urllib2Fixture',
@@ -40,6 +41,7 @@
from zope.component import (
adapter,
getGlobalSiteManager,
+ getUtility,
provideHandler,
)
from zope.interface import Interface
@@ -56,6 +58,12 @@
from lp.services.messaging.rabbit import connect
from lp.services.timeline.requesttimeline import get_request_timeline
from lp.services.webapp.errorlog import ErrorReportEvent
+from lp.services.webapp.interfaces import (
+ DEFAULT_FLAVOR,
+ MAIN_STORE,
+ IStoreSelector,
+ )
+from lp.testing.dbuser import dbuser
class PGNotReadyError(Exception):
@@ -385,3 +393,33 @@
<a href="http://example.com">File a bug</a>.
''')
self.addCleanup(lambda: config.pop('demo-fixture'))
+
+
+class DisableTriggerFixture(Fixture):
+ """Let tests disable database triggers."""
+
+ def __init__(self, table_triggers=None):
+ self.table_triggers = table_triggers or {}
+
+ def setUp(self):
+ super(DisableTriggerFixture, self).setUp()
+ self._disable_triggers()
+ self.addCleanup(self._enable_triggers)
+
+ def _process_triggers(self, mode):
+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+ with dbuser('postgres'):
+ for table, trigger in self.table_triggers.items():
+ sql = ("ALTER TABLE %(table)s %(mode)s trigger "
+ "%(trigger)s") % {
+ 'table': table,
+ 'mode': mode,
+ 'trigger': trigger,
+ }
+ store.execute(sql)
+
+ def _disable_triggers(self):
+ self._process_triggers(mode='DISABLE')
+
+ def _enable_triggers(self):
+ self._process_triggers(mode='ENABLE')
=== modified file 'lib/lp/testing/tests/test_fixture.py'
--- lib/lp/testing/tests/test_fixture.py 2012-01-18 07:35:31 +0000
+++ lib/lp/testing/tests/test_fixture.py 2012-05-17 22:13:25 +0000
@@ -36,6 +36,7 @@
from lp.testing import TestCase
from lp.testing.fixture import (
CaptureOops,
+ DisableTriggerFixture,
PGBouncerFixture,
ZopeAdapterFixture,
ZopeUtilityFixture,
@@ -45,6 +46,7 @@
DatabaseLayer,
LaunchpadLayer,
LaunchpadZopelessLayer,
+ ZopelessDatabaseLayer,
)
@@ -263,3 +265,62 @@
capture = self.useFixture(CaptureOops())
capture.sync()
capture.sync()
+
+
+class TestDisableTriggerFixture(TestCase):
+ """Test the DisableTriggerFixture class."""
+
+ layer = ZopelessDatabaseLayer
+
+ def setUp(self):
+ super(TestDisableTriggerFixture, self).setUp()
+ con_str = dbconfig.rw_main_master + ' user=launchpad_main'
+ con = psycopg2.connect(con_str)
+ con.set_isolation_level(0)
+ self.cursor = con.cursor()
+ # Create a test table and trigger.
+ setup_sql = """
+ CREATE OR REPLACE FUNCTION trig() RETURNS trigger
+ LANGUAGE plpgsql
+ AS
+ 'BEGIN
+ update test_trigger set col_b = NEW.col_a
+ where col_a = NEW.col_a;
+ RETURN NULL;
+ END;';
+ DROP TABLE IF EXISTS test_trigger CASCADE;
+ CREATE TABLE test_trigger(col_a integer, col_b integer);
+ CREATE TRIGGER test_trigger_t
+ AFTER INSERT on test_trigger
+ FOR EACH ROW EXECUTE PROCEDURE trig();
+ """
+ self.cursor.execute(setup_sql)
+ self.addCleanup(self._cleanup)
+
+ def _cleanup(self):
+ self.cursor.execute('DROP TABLE test_trigger CASCADE;')
+ self.cursor.close()
+ self.cursor.connection.close()
+
+ def test_triggers_are_disabled(self):
+ # Test that the fixture correctly disables specified triggers.
+ with DisableTriggerFixture({'test_trigger': 'test_trigger_t'}):
+ self.cursor.execute(
+ 'INSERT INTO test_trigger(col_a) values (1)')
+ self.cursor.execute(
+ 'SELECT col_b FROM test_trigger WHERE col_a = 1')
+ [col_b] = self.cursor.fetchone()
+ self.assertEqual(None, col_b)
+
+ def test_triggers_are_enabled_after(self):
+ # Test that the fixture correctly enables the triggers again when it
+ # is done.
+ with DisableTriggerFixture({'test_trigger': 'test_trigger_t'}):
+ self.cursor.execute(
+ 'INSERT INTO test_trigger(col_a) values (1)')
+ self.cursor.execute(
+ 'INSERT INTO test_trigger(col_a) values (2)')
+ self.cursor.execute(
+ 'SELECT col_b FROM test_trigger WHERE col_a = 2')
+ [col_b] = self.cursor.fetchone()
+ self.assertEqual(2, col_b)