diff options
| author | Carlton Gibson <carlton.gibson@noumenal.es> | 2018-06-18 21:07:29 +0200 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2018-06-18 15:37:05 -0400 |
| commit | 306f1f8ea3e2b54e194a59ac0ecb686460f180e8 (patch) | |
| tree | d3291701cf8c292248d6c074f656575de333de14 /django | |
| parent | d2ca28db54a5871d851cdd9184f4cf0d31aff946 (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.py | 5 | ||||
| -rw-r--r-- | django/contrib/admin/checks.py | 27 | ||||
| -rw-r--r-- | django/contrib/admin/options.py | 40 |
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)) |
