Permalink
Browse files

Fixed #8060 - Added permissions-checking for admin inlines. Thanks p.…

…patruno for report and Stephan Jaensch for work on the patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16934 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent e2f9c11 commit b1b1da1eac93297503c04b8394fb98e38f552f5f @carljm carljm committed Oct 7, 2011
@@ -270,6 +270,41 @@ def lookup_allowed(self, lookup, value):
clean_lookup = LOOKUP_SEP.join(parts)
return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy
+ def has_add_permission(self, request):
+ """
+ Returns True if the given request has permission to add an object.
+ Can be overriden by the user in subclasses.
+ """
+ opts = self.opts
+ return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
+
+ def has_change_permission(self, request, obj=None):
+ """
+ Returns True if the given request has permission to change the given
+ Django model instance, the default implementation doesn't examine the
+ `obj` parameter.
+
+ Can be overriden by the user in subclasses. In such case it should
+ return True if the given request has permission to change the `obj`
+ model instance. If `obj` is None, this should return True if the given
+ request has permission to change *any* object of the given type.
+ """
+ opts = self.opts
+ return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
+
+ def has_delete_permission(self, request, obj=None):
+ """
+ Returns True if the given request has permission to change the given
+ Django model instance, the default implementation doesn't examine the
+ `obj` parameter.
+
+ Can be overriden by the user in subclasses. In such case it should
+ return True if the given request has permission to delete the `obj`
+ model instance. If `obj` is None, this should return True if the given
+ request has permission to delete *any* object of the given type.
+ """
+ opts = self.opts
+ return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
class ModelAdmin(BaseModelAdmin):
"Encapsulates all admin options and functionality for a given model."
@@ -307,10 +342,6 @@ def __init__(self, model, admin_site):
self.model = model
self.opts = model._meta
self.admin_site = admin_site
- self.inline_instances = []
- for inline_class in self.inlines:
- inline_instance = inline_class(self.model, self.admin_site)
- self.inline_instances.append(inline_instance)
if 'action_checkbox' not in self.list_display and self.actions is not None:
self.list_display = ['action_checkbox'] + list(self.list_display)
if not self.list_display_links:
@@ -320,6 +351,21 @@ def __init__(self, model, admin_site):
break
super(ModelAdmin, self).__init__()
+ def get_inline_instances(self, request):
+ inline_instances = []
+ for inline_class in self.inlines:
+ inline = inline_class(self.model, self.admin_site)
+ if request:
+ if not (inline.has_add_permission(request) or
+ inline.has_change_permission(request) or
+ inline.has_delete_permission(request)):
+ continue
+ if not inline.has_add_permission(request):
+ inline.max_num = 0
+ inline_instances.append(inline)
+
+ return inline_instances
+
def get_urls(self):
from django.conf.urls import patterns, url
@@ -369,42 +415,6 @@ def media(self):
js.extend(['getElementsBySelector.js', 'dom-drag.js' , 'admin/ordering.js'])
return forms.Media(js=[static('admin/js/%s' % url) for url in js])
- def has_add_permission(self, request):
- """
- Returns True if the given request has permission to add an object.
- Can be overriden by the user in subclasses.
- """
- opts = self.opts
- return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
-
- def has_change_permission(self, request, obj=None):
- """
- Returns True if the given request has permission to change the given
- Django model instance, the default implementation doesn't examine the
- `obj` parameter.
-
- Can be overriden by the user in subclasses. In such case it should
- return True if the given request has permission to change the `obj`
- model instance. If `obj` is None, this should return True if the given
- request has permission to change *any* object of the given type.
- """
- opts = self.opts
- return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
-
- def has_delete_permission(self, request, obj=None):
- """
- Returns True if the given request has permission to change the given
- Django model instance, the default implementation doesn't examine the
- `obj` parameter.
-
- Can be overriden by the user in subclasses. In such case it should
- return True if the given request has permission to delete the `obj`
- model instance. If `obj` is None, this should return True if the given
- request has permission to delete *any* object of the given type.
- """
- opts = self.opts
- return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
-
def get_model_perms(self, request):
"""
Returns a dict of all perms for this model. This dict has the keys
@@ -500,7 +510,7 @@ def get_changelist_formset(self, request, **kwargs):
fields=self.list_editable, **defaults)
def get_formsets(self, request, obj=None):
- for inline in self.inline_instances:
+ for inline in self.get_inline_instances(request):
yield inline.get_formset(request, obj)
def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True):
@@ -914,6 +924,7 @@ def add_view(self, request, form_url='', extra_context=None):
ModelForm = self.get_form(request)
formsets = []
+ inline_instances = self.get_inline_instances(request)
if request.method == 'POST':
form = ModelForm(request.POST, request.FILES)
if form.is_valid():
@@ -923,7 +934,7 @@ def add_view(self, request, form_url='', extra_context=None):
form_validated = False
new_object = self.model()
prefixes = {}
- for FormSet, inline in zip(self.get_formsets(request), self.inline_instances):
+ for FormSet, inline in zip(self.get_formsets(request), inline_instances):
prefix = FormSet.get_default_prefix()
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1 or not prefix:
@@ -951,8 +962,7 @@ def add_view(self, request, form_url='', extra_context=None):
initial[k] = initial[k].split(",")
form = ModelForm(initial=initial)
prefixes = {}
- for FormSet, inline in zip(self.get_formsets(request),
- self.inline_instances):
+ for FormSet, inline in zip(self.get_formsets(request), inline_instances):
prefix = FormSet.get_default_prefix()
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1 or not prefix:
@@ -968,7 +978,7 @@ def add_view(self, request, form_url='', extra_context=None):
media = self.media + adminForm.media
inline_admin_formsets = []
- for inline, formset in zip(self.inline_instances, formsets):
+ for inline, formset in zip(inline_instances, formsets):
fieldsets = list(inline.get_fieldsets(request))
readonly = list(inline.get_readonly_fields(request))
prepopulated = dict(inline.get_prepopulated_fields(request))
@@ -1012,6 +1022,7 @@ def change_view(self, request, object_id, extra_context=None):
ModelForm = self.get_form(request, obj)
formsets = []
+ inline_instances = self.get_inline_instances(request)
if request.method == 'POST':
form = ModelForm(request.POST, request.FILES, instance=obj)
if form.is_valid():
@@ -1021,8 +1032,7 @@ def change_view(self, request, object_id, extra_context=None):
form_validated = False
new_object = obj
prefixes = {}
- for FormSet, inline in zip(self.get_formsets(request, new_object),
- self.inline_instances):
+ for FormSet, inline in zip(self.get_formsets(request, new_object), inline_instances):
prefix = FormSet.get_default_prefix()
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1 or not prefix:
@@ -1043,7 +1053,7 @@ def change_view(self, request, object_id, extra_context=None):
else:
form = ModelForm(instance=obj)
prefixes = {}
- for FormSet, inline in zip(self.get_formsets(request, obj), self.inline_instances):
+ for FormSet, inline in zip(self.get_formsets(request, obj), inline_instances):
prefix = FormSet.get_default_prefix()
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1 or not prefix:
@@ -1059,7 +1069,7 @@ def change_view(self, request, object_id, extra_context=None):
media = self.media + adminForm.media
inline_admin_formsets = []
- for inline, formset in zip(self.inline_instances, formsets):
+ for inline, formset in zip(inline_instances, formsets):
fieldsets = list(inline.get_fieldsets(request, obj))
readonly = list(inline.get_readonly_fields(request, obj))
prepopulated = dict(inline.get_prepopulated_fields(request, obj))
@@ -1377,6 +1387,7 @@ def get_formset(self, request, obj=None, **kwargs):
# if exclude is an empty list we use None, since that's the actual
# default
exclude = exclude or None
+ can_delete = self.can_delete and self.has_delete_permission(request, obj)
defaults = {
"form": self.form,
"formset": self.formset,
@@ -1386,7 +1397,7 @@ def get_formset(self, request, obj=None, **kwargs):
"formfield_callback": partial(self.formfield_for_dbfield, request=request),
"extra": self.extra,
"max_num": self.max_num,
- "can_delete": self.can_delete,
+ "can_delete": can_delete,
}
defaults.update(kwargs)
return inlineformset_factory(self.parent_model, self.model, **defaults)
@@ -1398,6 +1409,44 @@ def get_fieldsets(self, request, obj=None):
fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
return [(None, {'fields': fields})]
+ def queryset(self, request):
+ queryset = super(InlineModelAdmin, self).queryset(request)
+ if not self.has_change_permission(request):
+ queryset = queryset.none()
+ return queryset
+
+ def has_add_permission(self, request):
+ if self.opts.auto_created:
+ # We're checking the rights to an auto-created intermediate model,
+ # which doesn't have its own individual permissions. The user needs
+ # to have the change permission for the related model in order to
+ # be able to do anything with the intermediate model.
+ return self.has_change_permission(request)
+ return request.user.has_perm(
+ self.opts.app_label + '.' + self.opts.get_add_permission())
+
+ def has_change_permission(self, request, obj=None):
+ opts = self.opts
+ if opts.auto_created:
+ # The model was auto-created as intermediary for a
+ # ManyToMany-relationship, find the target model
+ for field in opts.fields:
+ if field.rel and field.rel.to != self.parent_model:
+ opts = field.rel.to._meta
+ break
+ return request.user.has_perm(
+ opts.app_label + '.' + opts.get_change_permission())
+
+ def has_delete_permission(self, request, obj=None):
+ if self.opts.auto_created:
+ # We're checking the rights to an auto-created intermediate model,
+ # which doesn't have its own individual permissions. The user needs
+ # to have the change permission for the related model in order to
+ # be able to do anything with the intermediate model.
+ return self.has_change_permission(request, obj)
+ return request.user.has_perm(
+ self.opts.app_label + '.' + self.opts.get_delete_permission())
+
class StackedInline(InlineModelAdmin):
template = 'admin/edit_inline/stacked.html'
@@ -424,14 +424,15 @@ def get_formset(self, request, obj=None, **kwargs):
# GenericInlineModelAdmin doesn't define its own.
exclude.extend(self.form._meta.exclude)
exclude = exclude or None
+ can_delete = self.can_delete and self.has_delete_permission(request, obj)
defaults = {
"ct_field": self.ct_field,
"fk_field": self.ct_fk_field,
"form": self.form,
"formfield_callback": partial(self.formfield_for_dbfield, request=request),
"formset": self.formset,
"extra": self.extra,
- "can_delete": self.can_delete,
+ "can_delete": can_delete,
"can_order": False,
"fields": fields,
"max_num": self.max_num,
@@ -1391,11 +1391,17 @@ adds some of its own (the shared features are actually defined in the
- :attr:`~ModelAdmin.ordering`
- :meth:`~ModelAdmin.queryset`
+.. versionadded:: 1.4
+
+- :meth:`~ModelAdmin.has_add_permission`
+- :meth:`~ModelAdmin.has_change_permission`
+- :meth:`~ModelAdmin.has_delete_permission`
+
The ``InlineModelAdmin`` class adds:
.. attribute:: InlineModelAdmin.model
- The model in which the inline is using. This is required.
+ The model which the inline is using. This is required.
.. attribute:: InlineModelAdmin.fk_name
@@ -128,6 +128,15 @@ A new :meth:`~django.contrib.admin.ModelAdmin.save_related` hook was added to
:mod:`~django.contrib.admin.ModelAdmin` to ease the customization of how
related objects are saved in the admin.
+Admin inlines respect user permissions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Admin inlines will now only allow those actions for which the user has
+permission. For ``ManyToMany`` relationships with an auto-created intermediate
+model (which does not have its own permissions), the change permission for the
+related model determines if the user has the permission to add, change or
+delete relationships.
+
Tools for cryptographic signing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Oops, something went wrong.

0 comments on commit b1b1da1

Please sign in to comment.