From 87b7cbd596fe022a5be5ebaa2e7fa3d5938cf9a5 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 11 Jan 2016 13:40:36 +0000 Subject: [PATCH] Implement permission policy modules: BlanketPermissionPolicy, AuthenticationOnlyPermissionPolicy, ModelPermissionPolicy, OwnershipPermissionPolicy These modules allow all permission logic ('can user X do Y', 'can user X do Y on instance Z', 'who are the users who can do Y on instance Z') to be moved into a single module that can be potentially swapped out to provide new permission rules without changing the rest of the code. --- wagtail/wagtailcore/permission_policies.py | 351 +++++ .../tests/test_permission_policies.py | 1374 +++++++++++++++++ 2 files changed, 1725 insertions(+) create mode 100644 wagtail/wagtailcore/permission_policies.py create mode 100644 wagtail/wagtailcore/tests/test_permission_policies.py diff --git a/wagtail/wagtailcore/permission_policies.py b/wagtail/wagtailcore/permission_policies.py new file mode 100644 index 000000000..0b4783930 --- /dev/null +++ b/wagtail/wagtailcore/permission_policies.py @@ -0,0 +1,351 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured +from django.db.models import Q +from django.utils.functional import cached_property + + +class BasePermissionPolicy(object): + """ + A 'permission policy' is an object that handles all decisions about the actions + users are allowed to perform on a given model. The mechanism by which it does this + is arbitrary, and may or may not involve the django.contrib.auth Permission model; + it could be as simple as "allow all users to do everything". + + In this way, admin apps can change their permission-handling logic just by swapping + to a different policy object, rather than having that logic spread across numerous + view functions. + + BasePermissionPolicy is an abstract class that all permission policies inherit from. + The only method that subclasses need to implement is users_with_any_permission; + all other methods can be derived from that (but in practice, subclasses will probably + want to override additional methods, either for efficiency or to implement more + fine-grained permission logic). + """ + + def __init__(self, model): + self.model = model + + # Basic user permission tests. Most policies are expected to override these, + # since the default implementation is to query the set of permitted users + # (which is pretty inefficient). + + def user_has_permission(self, user, action): + """ + Return whether the given user has permission to perform the given action + on some or all instances of this model + """ + return (user in self.users_with_permission(action)) + + def user_has_any_permission(self, user, actions): + """ + Return whether the given user has permission to perform any of the given actions + on some or all instances of this model + """ + return any(self.user_has_permission(user, action) for action in actions) + + # Operations for retrieving a list of users matching the permission criteria. + # All policies must implement, at minimum, users_with_any_permission. + + def users_with_any_permission(self, actions): + """ + Return a queryset of users who have permission to perform any of the given actions + on some or all instances of this model + """ + raise NotImplementedError + + def users_with_permission(self, action): + """ + Return a queryset of users who have permission to perform the given action on + some or all instances of this model + """ + return self.users_with_any_permission([action]) + + # Per-instance permission tests. In the simplest cases - corresponding to the + # basic Django permission model - permissions are enforced on a per-model basis + # and so these methods can simply defer to the per-model tests. Policies that + # require per-instance permission logic must override, at minimum: + # user_has_permission_for_instance + # instances_user_has_any_permission_for + # users_with_any_permission_for_instance + + def user_has_permission_for_instance(self, user, action, instance): + """ + Return whether the given user has permission to perform the given action on the + given model instance + """ + return self.user_has_permission(user, action) + + def user_has_any_permission_for_instance(self, user, actions, instance): + """ + Return whether the given user has permission to perform any of the given actions + on the given model instance + """ + return any( + self.user_has_permission_for_instance(user, action, instance) + for action in actions + ) + + def instances_user_has_any_permission_for(self, user, actions): + """ + Return a queryset of all instances of this model for which the given user has + permission to perform any of the given actions + """ + if self.user_has_any_permission(user, actions): + return self.model.objects.all() + else: + return self.model.objects.none() + + def instances_user_has_permission_for(self, user, action): + """ + Return a queryset of all instances of this model for which the given user has + permission to perform the given action + """ + return self.instances_user_has_any_permission_for(user, [action]) + + def users_with_any_permission_for_instance(self, actions, instance): + """ + Return a queryset of all users who have permission to perform any of the given + actions on the given model instance + """ + return self.users_with_any_permission(actions) + + def users_with_permission_for_instance(self, action, instance): + return self.users_with_any_permission_for_instance([action], instance) + + +class BlanketPermissionPolicy(BasePermissionPolicy): + """ + A permission policy that gives everyone (including anonymous users) + full permission over the given model + """ + def user_has_permission(self, user, action): + return True + + def user_has_any_permission(self, user, actions): + return True + + def users_with_any_permission(self, actions): + # Here we filter out inactive users from the results, even though inactive users + # - and for that matter anonymous users - still have permission according to the + # user_has_permission method. This is appropriate because, for most applications, + # setting is_active=False is equivalent to deleting the user account; you would + # not want these accounts to appear in, for example, a dropdown of users to + # assign a task to. The result here could never be completely logically correct + # (because it will not include anonymous users), so as the next best thing we + # return the "least surprise" result. + return get_user_model().objects.filter(is_active=True) + + def users_with_permission(self, action): + return get_user_model().objects.filter(is_active=True) + + +class AuthenticationOnlyPermissionPolicy(BasePermissionPolicy): + """ + A permission policy that gives all active authenticated users + full permission over the given model + """ + def user_has_permission(self, user, action): + return user.is_authenticated() and user.is_active + + def user_has_any_permission(self, user, actions): + return user.is_authenticated() and user.is_active + + def users_with_any_permission(self, actions): + return get_user_model().objects.filter(is_active=True) + + def users_with_permission(self, action): + return get_user_model().objects.filter(is_active=True) + + +class BaseDjangoAuthPermissionPolicy(BasePermissionPolicy): + """ + Extends BasePermissionPolicy with helper methods useful for policies that need to + perform lookups against the django.contrib.auth permission model + """ + def __init__(self, model): + super(BaseDjangoAuthPermissionPolicy, self).__init__(model) + self.app_label = self.model._meta.app_label + self.model_name = self.model._meta.model_name + + @cached_property + def _content_type(self): + return ContentType.objects.get_for_model(self.model) + + def _get_permission_name(self, action): + """ + Get the full app-label-qualified permission name (as required by + user.has_perm(...) ) for the given action on this model + """ + return '%s.%s_%s' % (self.app_label, action, self.model_name) + + def _get_users_with_any_permission_codenames_filter(self, permission_codenames): + """ + Given a list of permission codenames, return a filter expression which + will find all users which have any of those permissions - either + through group permissions, user permissions, or implicitly through + being a superuser. + """ + permissions = Permission.objects.filter( + content_type=self._content_type, + codename__in=permission_codenames + ) + return ( + Q(is_superuser=True) + | Q(user_permissions__in=permissions) + | Q(groups__permissions__in=permissions) + ) & Q(is_active=True) + + def _get_users_with_any_permission_codenames(self, permission_codenames): + """ + Given a list of permission codenames, return a queryset of users which + have any of those permissions - either through group permissions, user + permissions, or implicitly through being a superuser. + """ + filter_expr = self._get_users_with_any_permission_codenames_filter(permission_codenames) + return get_user_model().objects.filter(filter_expr).distinct() + + +class ModelPermissionPolicy(BaseDjangoAuthPermissionPolicy): + """ + A permission policy that enforces permissions at the model level, by consulting + the standard django.contrib.auth permission model directly + """ + def user_has_permission(self, user, action): + return user.has_perm(self._get_permission_name(action)) + + def users_with_any_permission(self, actions): + permission_codenames = [ + '%s_%s' % (action, self.model_name) + for action in actions + ] + return self._get_users_with_any_permission_codenames(permission_codenames) + + +class OwnershipPermissionPolicy(BaseDjangoAuthPermissionPolicy): + """ + A permission policy for objects that support a concept of 'ownership', where + the owner is typically the user who created the object. + + This policy piggybacks off 'add' and 'change' permissions defined through the + django.contrib.auth Permission model, as follows: + + * any user with 'add' permission can create instances, and ALSO edit instances + that they own + * any user with 'change' permission can edit instances regardless of ownership + * ability to edit also implies ability to delete + + Besides 'add', 'change' and 'delete', no other actions are recognised or permitted + (unless the user is an active superuser, in which case they can do everything). + """ + def __init__(self, model, owner_field_name='owner'): + super(OwnershipPermissionPolicy, self).__init__(model) + self.owner_field_name = owner_field_name + + # make sure owner_field_name is a field that exists on the model + try: + self.model._meta.get_field(self.owner_field_name) + except FieldDoesNotExist: + raise ImproperlyConfigured( + "%s has no field named '%s'. To use this model with OwnershipPermissionPolicy, " + "you must specify a valid field name as owner_field_name." + % (self.model, self.owner_field_name) + ) + + def user_has_permission(self, user, action): + if action == 'add': + return user.has_perm(self._get_permission_name('add')) + elif action == 'change' or action == 'delete': + return ( + # having 'add' permission means that there are *potentially* + # some instances they can edit (namely: ones they own), + # which is sufficient for returning True here + user.has_perm(self._get_permission_name('add')) + or user.has_perm(self._get_permission_name('change')) + ) + else: + # unrecognised actions are only allowed for active superusers + return user.is_active and user.is_superuser + + def users_with_any_permission(self, actions): + if 'change' in actions or 'delete' in actions: + # either 'add' or 'change' permission means that there are *potentially* + # some instances they can edit + permission_codenames = [ + 'add_%s' % self.model_name, + 'change_%s' % self.model_name + ] + elif 'add' in actions: + permission_codenames = [ + 'add_%s' % self.model_name, + ] + else: + # none of the actions passed in here are ones that we recognise, so only + # allow them for active superusers + return get_user_model().objects.filter(is_active=True, is_superuser=True) + + return self._get_users_with_any_permission_codenames(permission_codenames) + + def user_has_permission_for_instance(self, user, action, instance): + return self.user_has_any_permission_for_instance(user, [action], instance) + + def user_has_any_permission_for_instance(self, user, actions, instance): + if 'change' in actions or 'delete' in actions: + if user.has_perm(self._get_permission_name('change')): + return True + elif ( + user.has_perm(self._get_permission_name('add')) + and getattr(instance, self.owner_field_name) == user + ): + return True + else: + return False + else: + # 'change' and 'delete' are the only actions that are well-defined + # for specific instances. Other actions are only available to + # active superusers. + return user.is_active and user.is_superuser + + def instances_user_has_any_permission_for(self, user, actions): + if user.is_active and user.is_superuser: + # active superusers can perform any action (including unrecognised ones) + # on any instance + return self.model.objects.all() + elif 'change' in actions or 'delete' in actions: + if user.has_perm(self._get_permission_name('change')): + # user can edit all instances + return self.model.objects.all() + elif user.has_perm(self._get_permission_name('add')): + # user can edit their own instances + return self.model.objects.filter(**{self.owner_field_name: user}) + else: + # user has no permissions at all on this model + return self.model.objects.none() + else: + # action is either not recognised, or is the 'add' action which is + # not meaningful for existing instances. As such, non-superusers + # cannot perform it on any existing instances. + return self.model.objects.none() + + def users_with_any_permission_for_instance(self, actions, instance): + if 'change' in actions or 'delete' in actions: + # get filter expression for users with 'change' permission + filter_expr = self._get_users_with_any_permission_codenames_filter([ + 'change_%s' % self.model_name + ]) + + # add on the item's owner, if they still have 'add' permission + # (and the owner field isn't blank) + owner = getattr(instance, self.owner_field_name) + if owner is not None and owner.has_perm(self._get_permission_name('add')): + filter_expr = filter_expr | Q(pk=owner.pk) + + # return the filtered queryset + return get_user_model().objects.filter(filter_expr).distinct() + + else: + # action is either not recognised, or is the 'add' action which is + # not meaningful for existing instances. As such, the action is only + # available to superusers + return get_user_model().objects.filter(is_active=True, is_superuser=True) diff --git a/wagtail/wagtailcore/tests/test_permission_policies.py b/wagtail/wagtailcore/tests/test_permission_policies.py new file mode 100644 index 000000000..029528007 --- /dev/null +++ b/wagtail/wagtailcore/tests/test_permission_policies.py @@ -0,0 +1,1374 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group, Permission, AnonymousUser +from django.contrib.contenttypes.models import ContentType + +from wagtail.wagtailcore.permission_policies import ( + BlanketPermissionPolicy, AuthenticationOnlyPermissionPolicy, + ModelPermissionPolicy, OwnershipPermissionPolicy +) +from wagtail.wagtailimages.models import Image +from wagtail.wagtailimages.tests.utils import get_test_image_file + + +class PermissionPolicyTestCase(TestCase): + def setUp(self): + # Permissions + image_content_type = ContentType.objects.get_for_model(Image) + add_image_permission = Permission.objects.get( + content_type=image_content_type, codename='add_image' + ) + change_image_permission = Permission.objects.get( + content_type=image_content_type, codename='change_image' + ) + delete_image_permission = Permission.objects.get( + content_type=image_content_type, codename='delete_image' + ) + + # Groups + image_adders_group = Group.objects.create(name="Image adders") + image_adders_group.permissions.add(add_image_permission) + + image_changers_group = Group.objects.create(name="Image changers") + image_changers_group.permissions.add(change_image_permission) + + # Users + User = get_user_model() + + self.superuser = User.objects.create_superuser( + 'superuser', 'superuser@example.com', 'password' + ) + self.inactive_superuser = User.objects.create_superuser( + 'inactivesuperuser', 'inactivesuperuser@example.com', 'password', is_active=False + ) + + # a user with add_image permission through the 'Image adders' group + self.image_adder = User.objects.create_user( + 'imageadder', 'imageadder@example.com', 'password' + ) + self.image_adder.groups.add(image_adders_group) + + # a user with add_image permission through user_permissions + self.oneoff_image_adder = User.objects.create_user( + 'oneoffimageadder', 'oneoffimageadder@example.com', 'password' + ) + self.oneoff_image_adder.user_permissions.add(add_image_permission) + + # a user that has add_image permission, but is inactive + self.inactive_image_adder = User.objects.create_user( + 'inactiveimageadder', 'inactiveimageadder@example.com', 'password', is_active=False + ) + self.inactive_image_adder.groups.add(image_adders_group) + + # a user with change_image permission through the 'Image changers' group + self.image_changer = User.objects.create_user( + 'imagechanger', 'imagechanger@example.com', 'password' + ) + self.image_changer.groups.add(image_changers_group) + + # a user with change_image permission through user_permissions + self.oneoff_image_changer = User.objects.create_user( + 'oneoffimagechanger', 'oneoffimagechanger@example.com', 'password' + ) + self.oneoff_image_changer.user_permissions.add(change_image_permission) + + # a user that has change_image permission, but is inactive + self.inactive_image_changer = User.objects.create_user( + 'inactiveimagechanger', 'inactiveimagechanger@example.com', 'password', + is_active=False + ) + self.inactive_image_changer.groups.add(image_changers_group) + + # a user with delete_image permission through user_permissions + self.oneoff_image_deleter = User.objects.create_user( + 'oneoffimagedeleter', 'oneoffimagedeleter@example.com', 'password' + ) + self.oneoff_image_deleter.user_permissions.add(delete_image_permission) + + # a user with no permissions + self.useless_user = User.objects.create_user( + 'uselessuser', 'uselessuser@example.com', 'password' + ) + + self.anonymous_user = AnonymousUser() + + # Images + + # an image owned by 'imageadder' + self.adder_image = Image.objects.create( + title="imageadder's image", file=get_test_image_file(), + uploaded_by_user=self.image_adder + ) + + # an image owned by 'uselessuser' + self.useless_image = Image.objects.create( + title="uselessuser's image", file=get_test_image_file(), + uploaded_by_user=self.useless_user + ) + + # an image with no owner + self.anonymous_image = Image.objects.create( + title="anonymous image", file=get_test_image_file(), + ) + + def assertResultSetEqual(self, actual, expected): + self.assertEqual(set(actual), set(expected)) + + def assertUserPermissionMatrix(self, test_cases): + """ + Given a list of (user, can_add, can_change, can_delete, can_frobnicate) tuples + (where 'frobnicate' is an unrecognised action not defined on the model), + confirm that all tuples correctly represent permissions for that user as + returned by user_has_permission + """ + actions = ['add', 'change', 'delete', 'frobnicate'] + for test_case in test_cases: + user = test_case[0] + expected_results = zip(actions, test_case[1:]) + + for (action, expected_result) in expected_results: + if expected_result: + self.assertTrue( + self.policy.user_has_permission(user, action), + "User %s should be able to %s, but can't" % (user, action) + ) + else: + self.assertFalse( + self.policy.user_has_permission(user, action), + "User %s should not be able to %s, but can" % (user, action) + ) + + def assertUserInstancePermissionMatrix(self, instance, test_cases): + """ + Given a list of (user, can_change, can_delete, can_frobnicate) tuples + (where 'frobnicate' is an unrecognised action not defined on the model), + confirm that all tuples correctly represent permissions for that user on + the given instance, as returned by user_has_permission_for_instance + """ + actions = ['change', 'delete', 'frobnicate'] + for test_case in test_cases: + user = test_case[0] + expected_results = zip(actions, test_case[1:]) + + for (action, expected_result) in expected_results: + if expected_result: + self.assertTrue( + self.policy.user_has_permission_for_instance(user, action, instance), + "User %s should be able to %s instance %s, but can't" % ( + user, action, instance + ) + ) + else: + self.assertFalse( + self.policy.user_has_permission_for_instance(user, action, instance), + "User %s should not be able to %s instance %s, but can" % ( + user, action, instance + ) + ) + + +class TestBlanketPermissionPolicy(PermissionPolicyTestCase): + def setUp(self): + super(TestBlanketPermissionPolicy, self).setUp() + self.policy = BlanketPermissionPolicy(Image) + + self.active_users = [ + self.superuser, + self.image_adder, + self.oneoff_image_adder, + self.image_changer, + self.oneoff_image_changer, + self.oneoff_image_deleter, + self.useless_user, + ] + self.all_users = self.active_users + [ + self.inactive_superuser, + self.inactive_image_adder, + self.inactive_image_changer, + self.anonymous_user, + ] + + def test_user_has_permission(self): + # All users have permission to do everything + self.assertUserPermissionMatrix([ + (user, True, True, True, True) + for user in self.all_users + ]) + + def test_user_has_any_permission(self): + for user in self.all_users: + self.assertTrue( + self.policy.user_has_any_permission(user, ['add', 'change']) + ) + + def test_users_with_permission(self): + # all active users have permission + users_with_add_permission = self.policy.users_with_permission('add') + + self.assertResultSetEqual(users_with_add_permission, self.active_users) + + def test_users_with_any_permission(self): + # all active users have permission + users_with_add_or_change_permission = self.policy.users_with_any_permission( + ['add', 'change'] + ) + + self.assertResultSetEqual(users_with_add_or_change_permission, self.active_users) + + def test_user_has_permission_for_instance(self): + # All users have permission to do everything on any given instance + self.assertUserInstancePermissionMatrix(self.adder_image, [ + (user, True, True, True) + for user in self.all_users + ]) + + def test_user_has_any_permission_for_instance(self): + for user in self.all_users: + self.assertTrue( + self.policy.user_has_any_permission_for_instance( + user, ['change', 'delete'], self.adder_image + ) + ) + + def test_instances_user_has_permission_for(self): + all_images = [ + self.adder_image, self.useless_image, self.anonymous_image + ] + + # all users can edit all instances + for user in self.all_users: + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for(user, 'change'), + all_images + ) + + def test_instances_user_has_any_permission_for(self): + all_images = [ + self.adder_image, self.useless_image, self.anonymous_image + ] + + for user in self.all_users: + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for(user, ['change', 'delete']), + all_images + ) + + def test_users_with_permission_for_instance(self): + # all active users have permission + users_with_change_permission = self.policy.users_with_permission_for_instance( + 'change', self.useless_image + ) + + self.assertResultSetEqual(users_with_change_permission, self.active_users) + + def test_users_with_any_permission_for_instance(self): + # all active users have permission + users_with_change_or_del_permission = self.policy.users_with_any_permission_for_instance( + ['change', 'delete'], self.useless_image + ) + + self.assertResultSetEqual(users_with_change_or_del_permission, self.active_users) + + +class TestAuthenticationOnlyPermissionPolicy(PermissionPolicyTestCase): + def setUp(self): + super(TestAuthenticationOnlyPermissionPolicy, self).setUp() + self.policy = AuthenticationOnlyPermissionPolicy(Image) + + def test_user_has_permission(self): + # All active authenticated users have permission to do everything; + # inactive and anonymous users have permission to do nothing + self.assertUserPermissionMatrix([ + (self.superuser, True, True, True, True), + (self.inactive_superuser, False, False, False, False), + (self.image_adder, True, True, True, True), + (self.oneoff_image_adder, True, True, True, True), + (self.inactive_image_adder, False, False, False, False), + (self.image_changer, True, True, True, True), + (self.oneoff_image_changer, True, True, True, True), + (self.inactive_image_changer, False, False, False, False), + (self.oneoff_image_deleter, True, True, True, True), + (self.useless_user, True, True, True, True), + (self.anonymous_user, False, False, False, False), + ]) + + def test_user_has_any_permission(self): + self.assertTrue( + self.policy.user_has_any_permission(self.superuser, ['add', 'change']) + ) + + self.assertFalse( + self.policy.user_has_any_permission(self.inactive_superuser, ['add', 'change']) + ) + + self.assertTrue( + self.policy.user_has_any_permission(self.useless_user, ['add', 'change']) + ) + + self.assertFalse( + self.policy.user_has_any_permission(self.anonymous_user, ['add', 'change']) + ) + + def test_users_with_permission(self): + # all active users have permission + users_with_add_permission = self.policy.users_with_permission('add') + + self.assertResultSetEqual(users_with_add_permission, [ + self.superuser, + self.image_adder, + self.oneoff_image_adder, + self.image_changer, + self.oneoff_image_changer, + self.oneoff_image_deleter, + self.useless_user, + ]) + + def test_users_with_any_permission(self): + # all active users have permission + users_with_add_or_change_permission = self.policy.users_with_any_permission( + ['add', 'change'] + ) + + self.assertResultSetEqual(users_with_add_or_change_permission, [ + self.superuser, + self.image_adder, + self.oneoff_image_adder, + self.image_changer, + self.oneoff_image_changer, + self.oneoff_image_deleter, + self.useless_user, + ]) + + def test_user_has_permission_for_instance(self): + # Permissions for this policy are applied at the model level, + # so rules for a specific instance will match rules for the + # model as a whole + self.assertUserInstancePermissionMatrix(self.adder_image, [ + (self.superuser, True, True, True), + (self.inactive_superuser, False, False, False), + (self.image_adder, True, True, True), + (self.oneoff_image_adder, True, True, True), + (self.inactive_image_adder, False, False, False), + (self.image_changer, True, True, True), + (self.oneoff_image_changer, True, True, True), + (self.inactive_image_changer, False, False, False), + (self.oneoff_image_deleter, True, True, True), + (self.useless_user, True, True, True), + (self.anonymous_user, False, False, False), + ]) + + def test_user_has_any_permission_for_instance(self): + # superuser has permission + self.assertTrue( + self.policy.user_has_any_permission_for_instance( + self.superuser, ['change', 'delete'], self.adder_image + ) + ) + + # inactive user has no permission + self.assertFalse( + self.policy.user_has_any_permission_for_instance( + self.inactive_superuser, ['change', 'delete'], self.adder_image + ) + ) + + # ordinary user has permission + self.assertTrue( + self.policy.user_has_any_permission_for_instance( + self.useless_user, ['change', 'delete'], self.adder_image + ) + ) + + # anonymous user has no permission + self.assertFalse( + self.policy.user_has_any_permission_for_instance( + self.anonymous_user, ['change', 'delete'], self.adder_image + ) + ) + + def test_instances_user_has_permission_for(self): + all_images = [ + self.adder_image, self.useless_image, self.anonymous_image + ] + no_images = [] + + # the set of images editable by superuser includes all images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.superuser, 'change' + ), + all_images + ) + + # the set of images editable by inactive superuser includes no images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.inactive_superuser, 'change' + ), + no_images + ) + + # the set of images editable by ordinary user includes all images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.useless_user, 'change' + ), + all_images + ) + + # the set of images editable by anonymous user includes no images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.anonymous_user, 'change' + ), + no_images + ) + + def test_instances_user_has_any_permission_for(self): + all_images = [ + self.adder_image, self.useless_image, self.anonymous_image + ] + no_images = [] + + # the set of images editable by superuser includes all images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.superuser, ['change', 'delete'] + ), + all_images + ) + + # the set of images editable by inactive superuser includes no images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.inactive_superuser, ['change', 'delete'] + ), + no_images + ) + + # the set of images editable by ordinary user includes all images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.useless_user, ['change', 'delete'] + ), + all_images + ) + + # the set of images editable by anonymous user includes no images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.anonymous_user, ['change', 'delete'] + ), + no_images + ) + + def test_users_with_permission_for_instance(self): + # all active users have permission + users_with_change_permission = self.policy.users_with_permission_for_instance( + 'change', self.useless_image + ) + + self.assertResultSetEqual(users_with_change_permission, [ + self.superuser, + self.image_adder, + self.oneoff_image_adder, + self.image_changer, + self.oneoff_image_changer, + self.oneoff_image_deleter, + self.useless_user, + ]) + + def test_users_with_any_permission_for_instance(self): + # all active users have permission + users_with_change_or_del_permission = self.policy.users_with_any_permission_for_instance( + ['change', 'delete'], self.useless_image + ) + + self.assertResultSetEqual(users_with_change_or_del_permission, [ + self.superuser, + self.image_adder, + self.oneoff_image_adder, + self.image_changer, + self.oneoff_image_changer, + self.oneoff_image_deleter, + self.useless_user, + ]) + + +class TestModelPermissionPolicy(PermissionPolicyTestCase): + def setUp(self): + super(TestModelPermissionPolicy, self).setUp() + self.policy = ModelPermissionPolicy(Image) + + def test_user_has_permission(self): + self.assertUserPermissionMatrix([ + # Superuser has permission to do everything + (self.superuser, True, True, True, True), + + # Inactive superuser can do nothing + (self.inactive_superuser, False, False, False, False), + + # User with 'add' permission via group can only add + (self.image_adder, True, False, False, False), + + # User with 'add' permission via user can only add + (self.oneoff_image_adder, True, False, False, False), + + # Inactive user with 'add' permission can do nothing + (self.inactive_image_adder, False, False, False, False), + + # User with 'change' permission via group can only change + (self.image_changer, False, True, False, False), + + # User with 'change' permission via user can only change + (self.oneoff_image_changer, False, True, False, False), + + # Inactive user with 'add' permission can do nothing + (self.inactive_image_changer, False, False, False, False), + + # User with 'delete' permission can only delete + (self.oneoff_image_deleter, False, False, True, False), + + # User with no permissions can do nothing + (self.useless_user, False, False, False, False), + + # Anonymous user can do nothing + (self.anonymous_user, False, False, False, False), + ]) + + def test_user_has_any_permission(self): + # Superuser can do everything + self.assertTrue( + self.policy.user_has_any_permission(self.superuser, ['add', 'change']) + ) + + # Inactive superuser can do nothing + self.assertFalse( + self.policy.user_has_any_permission(self.inactive_superuser, ['add', 'change']) + ) + + # Only one of the permissions in the list needs to pass + # in order for user_has_any_permission to return true + self.assertTrue( + self.policy.user_has_any_permission(self.image_adder, ['add', 'change']) + ) + self.assertTrue( + self.policy.user_has_any_permission(self.oneoff_image_adder, ['add', 'change']) + ) + self.assertTrue( + self.policy.user_has_any_permission(self.image_changer, ['add', 'change']) + ) + + # User with some permission, but not the ones in the list, + # should return false + self.assertFalse( + self.policy.user_has_any_permission(self.image_changer, ['add', 'delete']) + ) + + # Inactive user with the appropriate permissions can do nothing + self.assertFalse( + self.policy.user_has_any_permission(self.inactive_image_adder, ['add', 'delete']) + ) + + # User with no permissions can do nothing + self.assertFalse( + self.policy.user_has_any_permission(self.useless_user, ['add', 'change']) + ) + + # Anonymous user can do nothing + self.assertFalse( + self.policy.user_has_any_permission(self.anonymous_user, ['add', 'change']) + ) + + def test_users_with_permission(self): + users_with_add_permission = self.policy.users_with_permission('add') + + self.assertResultSetEqual(users_with_add_permission, [ + self.superuser, + self.image_adder, + self.oneoff_image_adder, + ]) + + users_with_change_permission = self.policy.users_with_permission('change') + + self.assertResultSetEqual(users_with_change_permission, [ + self.superuser, + self.image_changer, + self.oneoff_image_changer, + ]) + + def test_users_with_any_permission(self): + users_with_add_or_change_permission = self.policy.users_with_any_permission( + ['add', 'change'] + ) + + self.assertResultSetEqual(users_with_add_or_change_permission, [ + self.superuser, + self.image_adder, + self.oneoff_image_adder, + self.image_changer, + self.oneoff_image_changer, + ]) + + users_with_change_or_delete_permission = self.policy.users_with_any_permission( + ['change', 'delete'] + ) + + self.assertResultSetEqual(users_with_change_or_delete_permission, [ + self.superuser, + self.image_changer, + self.oneoff_image_changer, + self.oneoff_image_deleter, + ]) + + def test_user_has_permission_for_instance(self): + # Permissions for this policy are applied at the model level, + # so rules for a specific instance will match rules for the + # model as a whole + self.assertUserInstancePermissionMatrix(self.adder_image, [ + (self.superuser, True, True, True), + (self.inactive_superuser, False, False, False), + (self.image_adder, False, False, False), + (self.oneoff_image_adder, False, False, False), + (self.inactive_image_adder, False, False, False), + (self.image_changer, True, False, False), + (self.oneoff_image_changer, True, False, False), + (self.inactive_image_changer, False, False, False), + (self.oneoff_image_deleter, False, True, False), + (self.useless_user, False, False, False), + (self.anonymous_user, False, False, False), + ]) + + def test_user_has_any_permission_for_instance(self): + # Superuser can do everything + self.assertTrue( + self.policy.user_has_any_permission_for_instance( + self.superuser, ['change', 'delete'], self.adder_image + ) + ) + + # Inactive superuser can do nothing + self.assertFalse( + self.policy.user_has_any_permission_for_instance( + self.inactive_superuser, ['change', 'delete'], self.adder_image + ) + ) + + # Only one of the permissions in the list needs to pass + # in order for user_has_any_permission to return true + self.assertTrue( + self.policy.user_has_any_permission_for_instance( + self.image_changer, ['change', 'delete'], self.adder_image + ) + ) + self.assertTrue( + self.policy.user_has_any_permission_for_instance( + self.oneoff_image_changer, ['change', 'delete'], self.adder_image + ) + ) + + # User with some permission, but not the ones in the list, + # should return false + self.assertFalse( + self.policy.user_has_any_permission_for_instance( + self.image_adder, ['change', 'delete'], self.adder_image + ) + ) + + # Inactive user with the appropriate permissions can do nothing + self.assertFalse( + self.policy.user_has_any_permission_for_instance( + self.inactive_image_changer, ['change', 'delete'], self.adder_image + ) + ) + + # User with no permissions can do nothing + self.assertFalse( + self.policy.user_has_any_permission_for_instance( + self.useless_user, ['change', 'delete'], self.adder_image + ) + ) + + # Anonymous user can do nothing + self.assertFalse( + self.policy.user_has_any_permission_for_instance( + self.anonymous_user, ['change', 'delete'], self.adder_image + ) + ) + + def test_instances_user_has_permission_for(self): + all_images = [ + self.adder_image, self.useless_image, self.anonymous_image + ] + no_images = [] + + # the set of images editable by superuser includes all images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.superuser, 'change' + ), + all_images + ) + + # the set of images editable by inactive superuser includes no images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.inactive_superuser, 'change' + ), + no_images + ) + + # given the relevant model permission at the group level, a user can edit all images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.image_changer, 'change' + ), + all_images + ) + + # given the relevant model permission at the user level, a user can edit all images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.oneoff_image_changer, 'change' + ), + all_images + ) + + # a user with no permission can edit no images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.useless_user, 'change' + ), + no_images + ) + + # an inactive user with the relevant permission can edit no images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.inactive_image_changer, 'change' + ), + no_images + ) + + # a user with permission, but not the matching one, can edit no images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.image_changer, 'delete' + ), + no_images + ) + + # the set of images editable by anonymous user includes no images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.anonymous_user, 'change' + ), + no_images + ) + + def test_instances_user_has_any_permission_for(self): + all_images = [ + self.adder_image, self.useless_image, self.anonymous_image + ] + no_images = [] + + # the set of images editable by superuser includes all images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.superuser, ['change', 'delete'] + ), + all_images + ) + + # the set of images editable by inactive superuser includes no images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.inactive_superuser, ['change', 'delete'] + ), + no_images + ) + + # given the relevant model permission at the group level, a user can edit all images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.image_changer, ['change', 'delete'] + ), + all_images + ) + + # given the relevant model permission at the user level, a user can edit all images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.oneoff_image_changer, ['change', 'delete'] + ), + all_images + ) + + # a user with no permission can edit no images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.useless_user, ['change', 'delete'] + ), + no_images + ) + + # an inactive user with the relevant permission can edit no images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.inactive_image_changer, ['change', 'delete'] + ), + no_images + ) + + # a user with permission, but not the matching one, can edit no images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.image_adder, ['change', 'delete'] + ), + no_images + ) + + # the set of images editable by anonymous user includes no images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.anonymous_user, ['change', 'delete'] + ), + no_images + ) + + def test_users_with_permission_for_instance(self): + users_with_change_permission = self.policy.users_with_permission_for_instance( + 'change', self.useless_image + ) + + self.assertResultSetEqual(users_with_change_permission, [ + self.superuser, + self.image_changer, + self.oneoff_image_changer, + ]) + + users_with_delete_permission = self.policy.users_with_permission_for_instance( + 'delete', self.useless_image + ) + + self.assertResultSetEqual(users_with_delete_permission, [ + self.superuser, + self.oneoff_image_deleter, + ]) + + def test_users_with_any_permission_for_instance(self): + users_with_change_or_del_permission = self.policy.users_with_any_permission_for_instance( + ['change', 'delete'], self.useless_image + ) + + self.assertResultSetEqual(users_with_change_or_del_permission, [ + self.superuser, + self.image_changer, + self.oneoff_image_changer, + self.oneoff_image_deleter, + ]) + + +class TestOwnershipPermissionPolicy(PermissionPolicyTestCase): + def setUp(self): + super(TestOwnershipPermissionPolicy, self).setUp() + self.policy = OwnershipPermissionPolicy(Image, owner_field_name='uploaded_by_user') + + def test_user_has_permission(self): + self.assertUserPermissionMatrix([ + # Superuser has permission to do everything + (self.superuser, True, True, True, True), + + # Inactive superuser can do nothing + (self.inactive_superuser, False, False, False, False), + + # User with 'add' permission via group can add, + # and by extension, change and delete their own instances + (self.image_adder, True, True, True, False), + + # User with 'add' permission via user can add, + # and by extension, change and delete their own instances + (self.oneoff_image_adder, True, True, True, False), + + # Inactive user with 'add' permission can do nothing + (self.inactive_image_adder, False, False, False, False), + + # User with 'change' permission via group can change and delete but not add + (self.image_changer, False, True, True, False), + + # User with 'change' permission via user can change and delete but not add + (self.oneoff_image_changer, False, True, True, False), + + # Inactive user with 'change' permission can do nothing + (self.inactive_image_changer, False, False, False, False), + + # 'delete' permission is ignored for this policy + (self.oneoff_image_deleter, False, False, False, False), + + # User with no permission can do nothing + (self.useless_user, False, False, False, False), + + # Anonymous user can do nothing + (self.anonymous_user, False, False, False, False), + ]) + + def test_user_has_any_permission(self): + # Superuser can do everything + self.assertTrue( + self.policy.user_has_any_permission(self.superuser, ['add', 'change']) + ) + + # Inactive superuser can do nothing + self.assertFalse( + self.policy.user_has_any_permission(self.inactive_superuser, ['add', 'change']) + ) + + # Only one of the permissions in the list needs to pass + # in order for user_has_any_permission to return true + self.assertTrue( + self.policy.user_has_any_permission(self.image_changer, ['add', 'change']) + ) + self.assertTrue( + self.policy.user_has_any_permission(self.oneoff_image_changer, ['add', 'change']) + ) + + # User with some permission, but not the ones in the list, + # should return false + self.assertFalse( + self.policy.user_has_any_permission(self.oneoff_image_deleter, ['add', 'change']) + ) + + # Inactive user with the appropriate permissions can do nothing + self.assertFalse( + self.policy.user_has_any_permission(self.inactive_image_changer, ['add', 'delete']) + ) + + # User with no permissions can do nothing + self.assertFalse( + self.policy.user_has_any_permission(self.useless_user, ['add', 'change']) + ) + + # Anonymous user can do nothing + self.assertFalse( + self.policy.user_has_any_permission(self.anonymous_user, ['add', 'change']) + ) + + def test_users_with_permission(self): + users_with_add_permission = self.policy.users_with_permission('add') + + self.assertResultSetEqual(users_with_add_permission, [ + self.superuser, + self.image_adder, + self.oneoff_image_adder, + ]) + + # users with add permission have change permission too (i.e. for their own images) + users_with_change_permission = self.policy.users_with_permission('change') + + self.assertResultSetEqual(users_with_change_permission, [ + self.superuser, + self.image_adder, + self.oneoff_image_adder, + self.image_changer, + self.oneoff_image_changer, + ]) + + # conditions for deletion are the same as for change; 'delete' permission + # records in django.contrib.auth are ignored + users_with_delete_permission = self.policy.users_with_permission('delete') + + self.assertResultSetEqual(users_with_delete_permission, [ + self.superuser, + self.image_adder, + self.oneoff_image_adder, + self.image_changer, + self.oneoff_image_changer, + ]) + + # non-standard permissions are only available to superusers + users_with_frobnicate_permission = self.policy.users_with_permission('frobnicate') + + self.assertResultSetEqual(users_with_frobnicate_permission, [ + self.superuser, + ]) + + def test_users_with_any_permission(self): + users_with_add_or_change_permission = self.policy.users_with_any_permission( + ['add', 'change'] + ) + + self.assertResultSetEqual(users_with_add_or_change_permission, [ + self.superuser, + self.image_adder, + self.oneoff_image_adder, + self.image_changer, + self.oneoff_image_changer, + ]) + + users_with_add_or_frobnicate_permission = self.policy.users_with_any_permission( + ['add', 'frobnicate'] + ) + + self.assertResultSetEqual(users_with_add_or_frobnicate_permission, [ + self.superuser, + self.image_adder, + self.oneoff_image_adder, + ]) + + def test_user_has_permission_for_instance(self): + # Test permissions for an image owned by image_adder + self.assertUserInstancePermissionMatrix(self.adder_image, [ + # superuser can do everything + (self.superuser, True, True, True), + + # inactive superuser can do nothing + (self.inactive_superuser, False, False, False), + + # image_adder can change and delete their own images, + # but not perform custom actions + (self.image_adder, True, True, False), + + # user with add permission cannot edit images owned by others + (self.oneoff_image_adder, False, False, False), + + # inactive user with 'add' permission can do nothing + (self.inactive_image_adder, False, False, False), + + # user with change permission can change and delete all images + (self.image_changer, True, True, False), + + # likewise for change permission specified at the user level + (self.oneoff_image_changer, True, True, False), + + # inactive user with 'change' permission can do nothing + (self.inactive_image_changer, False, False, False), + + # delete permissions are ignored + (self.oneoff_image_deleter, False, False, False), + + # user with no permissions can do nothing + (self.useless_user, False, False, False), + + # anonymous user can do nothing + (self.anonymous_user, False, False, False), + ]) + + # Test permissions for an image owned by useless_user + self.assertUserInstancePermissionMatrix(self.useless_image, [ + # superuser can do everything + (self.superuser, True, True, True), + + # image_adder cannot edit images owned by others + (self.image_adder, False, False, False), + (self.oneoff_image_adder, False, False, False), + + # user with change permission can change and delete all images + (self.image_changer, True, True, False), + (self.oneoff_image_changer, True, True, False), + + # inactive users can do nothing + (self.inactive_superuser, False, False, False), + (self.inactive_image_adder, False, False, False), + (self.inactive_image_changer, False, False, False), + + # delete permissions are ignored + (self.oneoff_image_deleter, False, False, False), + + # user with no permissions can do nothing, even on images + # they own + (self.useless_user, False, False, False), + + # anonymous user can do nothing + (self.anonymous_user, False, False, False), + ]) + + # Instances with a null owner should always follow the same rules + # as 'an instance owned by someone else' + self.assertUserInstancePermissionMatrix(self.anonymous_image, [ + (self.superuser, True, True, True), + (self.image_adder, False, False, False), + (self.oneoff_image_adder, False, False, False), + (self.image_changer, True, True, False), + (self.oneoff_image_changer, True, True, False), + (self.inactive_superuser, False, False, False), + (self.inactive_image_adder, False, False, False), + (self.inactive_image_changer, False, False, False), + (self.oneoff_image_deleter, False, False, False), + (self.useless_user, False, False, False), + (self.anonymous_user, False, False, False), + ]) + + def test_user_has_any_permission_for_instance(self): + # Superuser can do everything + self.assertTrue( + self.policy.user_has_any_permission_for_instance( + self.superuser, ['change', 'delete'], self.adder_image + ) + ) + + # Inactive superuser can do nothing + self.assertFalse( + self.policy.user_has_any_permission_for_instance( + self.inactive_superuser, ['change', 'delete'], self.adder_image + ) + ) + + # Only one of the permissions in the list needs to pass + # in order for user_has_any_permission to return true + self.assertTrue( + self.policy.user_has_any_permission_for_instance( + self.image_changer, ['change', 'frobnicate'], self.adder_image + ) + ) + self.assertTrue( + self.policy.user_has_any_permission_for_instance( + self.oneoff_image_changer, ['change', 'frobnicate'], self.adder_image + ) + ) + + # User with some permission, but not the ones in the list, + # should return false + self.assertFalse( + self.policy.user_has_any_permission_for_instance( + self.oneoff_image_deleter, ['change', 'delete'], self.adder_image + ) + ) + + # Inactive user with the appropriate permissions can do nothing + self.assertFalse( + self.policy.user_has_any_permission_for_instance( + self.inactive_image_changer, ['change', 'delete'], self.adder_image + ) + ) + + # User with no permissions can do nothing + self.assertFalse( + self.policy.user_has_any_permission_for_instance( + self.useless_user, ['change', 'delete'], self.adder_image + ) + ) + + # Anonymous user can do nothing + self.assertFalse( + self.policy.user_has_any_permission_for_instance( + self.anonymous_user, ['change', 'delete'], self.adder_image + ) + ) + + def test_instances_user_has_permission_for(self): + all_images = [ + self.adder_image, self.useless_image, self.anonymous_image + ] + no_images = [] + + # the set of images editable by superuser includes all images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.superuser, 'change' + ), + all_images + ) + + # the set of images editable by inactive superuser includes no images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.inactive_superuser, 'change' + ), + no_images + ) + + # a user with 'add' permission can change their own images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.image_adder, 'change' + ), + [self.adder_image] + ) + # a user with 'add' permission can also delete their own images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.image_adder, 'delete' + ), + [self.adder_image] + ) + + # a user with 'change' permission can change all images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.image_changer, 'change' + ), + all_images + ) + + # ditto for 'change' permission assigned at the user level + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.oneoff_image_changer, 'change' + ), + all_images + ) + + # an inactive user with the relevant permission can edit no images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.inactive_image_changer, 'change' + ), + no_images + ) + + # a user with no permission can edit no images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.useless_user, 'change' + ), + no_images + ) + + # the set of images editable by anonymous user includes no images + self.assertResultSetEqual( + self.policy.instances_user_has_permission_for( + self.anonymous_user, 'change' + ), + no_images + ) + + def test_instances_user_has_any_permission_for(self): + all_images = [ + self.adder_image, self.useless_image, self.anonymous_image + ] + no_images = [] + + # the set of images editable by superuser includes all images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.superuser, ['change', 'delete'] + ), + all_images + ) + + # the set of images editable by inactive superuser includes no images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.inactive_superuser, ['change', 'delete'] + ), + no_images + ) + + # a user with 'add' permission can change/delete their own images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.image_adder, ['delete', 'frobnicate'] + ), + [self.adder_image] + ) + + # a user with 'edit' permission can change/delete all images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.oneoff_image_changer, ['delete', 'frobnicate'] + ), + all_images + ) + + # a user with no permission can edit no images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.useless_user, ['change', 'delete'] + ), + no_images + ) + + # an inactive user with the relevant permission can edit no images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.inactive_image_changer, ['change', 'delete'] + ), + no_images + ) + + # a user with permission, but not the matching one, can edit no images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.oneoff_image_deleter, ['change', 'delete'] + ), + no_images + ) + + # the set of images editable by anonymous user includes no images + self.assertResultSetEqual( + self.policy.instances_user_has_any_permission_for( + self.anonymous_user, ['change', 'delete'] + ), + no_images + ) + + def test_users_with_permission_for_instance(self): + # adder_image can be edited by its owner (who has add permission) and + # all users with 'change' permission + users_with_change_permission = self.policy.users_with_permission_for_instance( + 'change', self.adder_image + ) + + self.assertResultSetEqual(users_with_change_permission, [ + self.superuser, + self.image_adder, + self.image_changer, + self.oneoff_image_changer, + ]) + + # the same set of users can also delete + users_with_delete_permission = self.policy.users_with_permission_for_instance( + 'delete', self.adder_image + ) + + self.assertResultSetEqual(users_with_delete_permission, [ + self.superuser, + self.image_adder, + self.image_changer, + self.oneoff_image_changer, + ]) + + # custom actions are available to superusers only + users_with_delete_permission = self.policy.users_with_permission_for_instance( + 'frobnicate', self.adder_image + ) + + self.assertResultSetEqual(users_with_delete_permission, [ + self.superuser, + ]) + + # useless_user can NOT edit their own image, because they do not have + # 'add' permission + users_with_change_permission = self.policy.users_with_permission_for_instance( + 'change', self.useless_image + ) + + self.assertResultSetEqual(users_with_change_permission, [ + self.superuser, + self.image_changer, + self.oneoff_image_changer, + ]) + + # an image with no owner is treated as if it's owned by 'somebody else' - + # i.e. users with 'change' permission can edit it + users_with_change_permission = self.policy.users_with_permission_for_instance( + 'change', self.anonymous_image + ) + + self.assertResultSetEqual(users_with_change_permission, [ + self.superuser, + self.image_changer, + self.oneoff_image_changer, + ]) + + def test_users_with_any_permission_for_instance(self): + users_with_change_or_frob_permission = self.policy.users_with_any_permission_for_instance( + ['change', 'frobnicate'], self.adder_image + ) + + self.assertResultSetEqual(users_with_change_or_frob_permission, [ + self.superuser, + self.image_adder, + self.image_changer, + self.oneoff_image_changer, + ])