summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorCarlton Gibson <carlton.gibson@noumenal.es>2018-06-18 21:07:29 +0200
committerTim Graham <timograham@gmail.com>2018-06-18 15:37:05 -0400
commit306f1f8ea3e2b54e194a59ac0ecb686460f180e8 (patch)
treed3291701cf8c292248d6c074f656575de333de14 /django
parentd2ca28db54a5871d851cdd9184f4cf0d31aff946 (diff)
[2.1.x] Fixed #29419 -- Allowed permissioning of admin actions.
Backport of 958c7b301ead79974db8edd5b9c6588a10a28ae7 from master
Diffstat (limited to 'django')
-rw-r--r--django/contrib/admin/actions.py5
-rw-r--r--django/contrib/admin/checks.py27
-rw-r--r--django/contrib/admin/options.py40
3 files changed, 57 insertions, 15 deletions
diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py
index f64b89205e..1e1c3bd384 100644
--- a/django/contrib/admin/actions.py
+++ b/django/contrib/admin/actions.py
@@ -23,10 +23,6 @@ def delete_selected(modeladmin, request, queryset):
opts = modeladmin.model._meta
app_label = opts.app_label
- # Check that the user has delete permission for the actual model
- if not modeladmin.has_delete_permission(request):
- raise PermissionDenied
-
# Populate deletable_objects, a data structure of all related objects that
# will also be deleted.
deletable_objects, model_count, perms_needed, protected = modeladmin.get_deleted_objects(queryset, request)
@@ -79,4 +75,5 @@ def delete_selected(modeladmin, request, queryset):
], context)
+delete_selected.allowed_permissions = ('delete',)
delete_selected.short_description = gettext_lazy("Delete selected %(verbose_name_plural)s")
diff --git a/django/contrib/admin/checks.py b/django/contrib/admin/checks.py
index cf5b312369..8dd5a72256 100644
--- a/django/contrib/admin/checks.py
+++ b/django/contrib/admin/checks.py
@@ -579,6 +579,7 @@ class ModelAdminChecks(BaseModelAdminChecks):
*self._check_list_editable(admin_obj),
*self._check_search_fields(admin_obj),
*self._check_date_hierarchy(admin_obj),
+ *self._check_action_permission_methods(admin_obj),
]
def _check_save_as(self, obj):
@@ -891,6 +892,32 @@ class ModelAdminChecks(BaseModelAdminChecks):
else:
return []
+ def _check_action_permission_methods(self, obj):
+ """
+ Actions with an allowed_permission attribute require the ModelAdmin to
+ implement a has_<perm>_permission() method for each permission.
+ """
+ actions = obj._get_base_actions()
+ errors = []
+ for func, name, _ in actions:
+ if not hasattr(func, 'allowed_permissions'):
+ continue
+ for permission in func.allowed_permissions:
+ method_name = 'has_%s_permission' % permission
+ if not hasattr(obj, method_name):
+ errors.append(
+ checks.Error(
+ '%s must define a %s() method for the %s action.' % (
+ obj.__class__.__name__,
+ method_name,
+ func.__name__,
+ ),
+ obj=obj.__class__,
+ id='admin.E129',
+ )
+ )
+ return errors
+
class InlineModelAdminChecks(BaseModelAdminChecks):
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 62c881eb74..0f4dd93cb3 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -852,16 +852,8 @@ class ModelAdmin(BaseModelAdmin):
return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, str(obj.pk))
action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle">')
- def get_actions(self, request):
- """
- Return a dictionary mapping the names of all actions for this
- ModelAdmin to a tuple of (callable, name, description) for each action.
- """
- # If self.actions is explicitly set to None that means that we don't
- # want *any* actions enabled on this page.
- if self.actions is None or IS_POPUP_VAR in request.GET:
- return OrderedDict()
-
+ def _get_base_actions(self):
+ """Return the list of actions, prior to any request-based filtering."""
actions = []
# Gather actions from the admin site first
@@ -876,8 +868,34 @@ class ModelAdmin(BaseModelAdmin):
actions.extend(self.get_action(action) for action in class_actions)
# get_action might have returned None, so filter any of those out.
- actions = filter(None, actions)
+ return filter(None, actions)
+
+ def _filter_actions_by_permissions(self, request, actions):
+ """Filter out any actions that the user doesn't have access to."""
+ filtered_actions = []
+ for action in actions:
+ callable = action[0]
+ if not hasattr(callable, 'allowed_permissions'):
+ filtered_actions.append(action)
+ continue
+ permission_checks = (
+ getattr(self, 'has_%s_permission' % permission)
+ for permission in callable.allowed_permissions
+ )
+ if any(has_permission(request) for has_permission in permission_checks):
+ filtered_actions.append(action)
+ return filtered_actions
+ def get_actions(self, request):
+ """
+ Return a dictionary mapping the names of all actions for this
+ ModelAdmin to a tuple of (callable, name, description) for each action.
+ """
+ # If self.actions is set to None that means actions are disabled on
+ # this page.
+ if self.actions is None or IS_POPUP_VAR in request.GET:
+ return OrderedDict()
+ actions = self._filter_actions_by_permissions(request, self._get_base_actions())
# Convert the actions into an OrderedDict keyed by name.
return OrderedDict(
(name, (func, name, desc))