diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index f05b5cb..ba806c4 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -270,6 +270,41 @@ class BaseModelAdmin(object):
             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 @@ class ModelAdmin(BaseModelAdmin):
         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,24 @@ class ModelAdmin(BaseModelAdmin):
                     break
         super(ModelAdmin, self).__init__()
 
+    def get_inline_instances(self, request=None, action=None):
+        inline_instances = []
+        for inline_class in self.inlines:
+            inline = inline_class(self.model, self.admin_site)
+            if request:
+                if not ((action == 'add' and inline.has_add_permission(request)) or
+                    (inline.has_add_permission(request) or inline.has_change_permission(request) or inline.has_delete_permission(request))):
+                    continue
+                if action == 'change' and not inline.has_add_permission(request):
+                    inline.max_num = 0
+            inline_instances.append(inline)
+
+        return inline_instances
+
+    @property
+    def inline_instances(self):
+        return self.get_inline_instances()
+
     def get_urls(self):
         from django.conf.urls import patterns, url
 
@@ -369,42 +418,6 @@ class ModelAdmin(BaseModelAdmin):
             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 +513,8 @@ class ModelAdmin(BaseModelAdmin):
             fields=self.list_editable, **defaults)
 
     def get_formsets(self, request, obj=None):
-        for inline in self.inline_instances:
+        action = ('change' if obj else 'add')
+        for inline in self.get_inline_instances(request, action):
             yield inline.get_formset(request, obj)
 
     def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True):
@@ -914,6 +928,7 @@ class ModelAdmin(BaseModelAdmin):
 
         ModelForm = self.get_form(request)
         formsets = []
+        inline_instances = self.get_inline_instances(request, 'add')
         if request.method == 'POST':
             form = ModelForm(request.POST, request.FILES)
             if form.is_valid():
@@ -923,7 +938,7 @@ class ModelAdmin(BaseModelAdmin):
                 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 +966,7 @@ class ModelAdmin(BaseModelAdmin):
                     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 +982,7 @@ class ModelAdmin(BaseModelAdmin):
         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 +1026,7 @@ class ModelAdmin(BaseModelAdmin):
 
         ModelForm = self.get_form(request, obj)
         formsets = []
+        inline_instances = self.get_inline_instances(request, 'change')
         if request.method == 'POST':
             form = ModelForm(request.POST, request.FILES, instance=obj)
             if form.is_valid():
@@ -1021,8 +1036,7 @@ class ModelAdmin(BaseModelAdmin):
                 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 +1057,7 @@ class ModelAdmin(BaseModelAdmin):
         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 +1073,7 @@ class ModelAdmin(BaseModelAdmin):
         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 +1391,9 @@ class InlineModelAdmin(BaseModelAdmin):
         # if exclude is an empty list we use None, since that's the actual
         # default
         exclude = exclude or None
+        can_delete = self.can_delete
+        if request:  # some tests pass None as request
+            can_delete = can_delete and self.has_delete_permission(request, obj)
         defaults = {
             "form": self.form,
             "formset": self.formset,
@@ -1386,7 +1403,7 @@ class InlineModelAdmin(BaseModelAdmin):
             "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 +1415,47 @@ class InlineModelAdmin(BaseModelAdmin):
         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 request and not self.has_change_permission(request):
+            queryset = queryset.none()
+        return queryset
+
+    def get_permission_opts(self):
+        opts = self.opts
+        if opts.auto_created:
+            # The model was auto-created as intermediary for a
+            # ManyToMany-relationship, find out the destination model
+            for field in opts.fields:
+                if isinstance(field, models.ForeignKey) and field.rel.to != opts.auto_created:
+                    opts = field.rel.to._meta
+                    break
+        return opts
+
+    def has_add_permission(self, request):
+        if self.opts.auto_created:
+            # We're checking the rights to an auto-created intermediate model. As per
+            # the discussion on ticket #8060, 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)
+        opts = self.get_permission_opts()
+        return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
+
+    def has_change_permission(self, request, obj=None):
+        opts = self.get_permission_opts()
+        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. As per
+            # the discussion on ticket #8060, 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)
+        opts = self.get_permission_opts()
+        return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
+
 class StackedInline(InlineModelAdmin):
     template = 'admin/edit_inline/stacked.html'
 
diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py
index 69dd527..e558aee 100644
--- a/django/contrib/contenttypes/generic.py
+++ b/django/contrib/contenttypes/generic.py
@@ -411,6 +411,9 @@ class GenericInlineModelAdmin(InlineModelAdmin):
             # GenericInlineModelAdmin doesn't define its own.
             exclude.extend(self.form._meta.exclude)
         exclude = exclude or None
+        can_delete = self.can_delete
+        if request:  # some tests pass None as request
+            can_delete = can_delete and self.has_delete_permission(request, obj)
         defaults = {
             "ct_field": self.ct_field,
             "fk_field": self.ct_fk_field,
@@ -418,7 +421,7 @@ class GenericInlineModelAdmin(InlineModelAdmin):
             "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,
diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py
index 955d620..ebfe787 100644
--- a/tests/regressiontests/admin_inlines/tests.py
+++ b/tests/regressiontests/admin_inlines/tests.py
@@ -1,11 +1,12 @@
 from django.contrib.admin.helpers import InlineAdminForm
+from django.contrib.auth.models import User, Permission
 from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 
 # local test models
 from models import (Holder, Inner, Holder2, Inner2, Holder3,
     Inner3, Person, OutfitItem, Fashionista, Teacher, Parent, Child,
-    CapoFamiglia, Consigliere, SottoCapo)
+    Author, Book, TitleCollection, Title)
 from admin import InnerInline
 
 
@@ -141,7 +142,6 @@ class TestInline(TestCase):
                 '<input id="id_-2-0-name" type="text" class="vTextField" '
                 'name="-2-0-name" maxlength="100" />')
 
-
 class TestInlineMedia(TestCase):
     urls = "regressiontests.admin_inlines.urls"
     fixtures = ['admin-views-users.xml']
@@ -196,3 +196,178 @@ class TestInlineAdminForm(TestCase):
         iaf = InlineAdminForm(None, None, {}, {}, joe)
         parent_ct = ContentType.objects.get_for_model(Parent)
         self.assertEqual(iaf.original.content_type, parent_ct)
+
+class TestInlinePermissions(TestCase):
+    """
+    Make sure the admin respects permissions for objects that are edited
+    inline. Ref #8060.
+    """
+    urls = "regressiontests.admin_inlines.urls"
+    fixtures = ['admin-views-users.xml']
+
+    def setUp(self):
+        self.user = User.objects.get(username='super')
+        self.user.is_superuser = False
+        self.user.save()
+
+        self.author_ct = ContentType.objects.get_for_model(Author)
+        self.holder_ct = ContentType.objects.get_for_model(Holder)
+        self.book_ct = ContentType.objects.get_for_model(Book)
+        self.inner_ct = ContentType.objects.get_for_model(Inner)
+
+        author = Author.objects.create(pk=1, name=u'The Author')
+        author.books.create(name=u'The inline Book')
+        holder = Holder(dummy=13)
+        holder.save()
+        Inner(dummy=42, holder=holder).save()
+        self.change_url = '/admin/admin_inlines/holder/%i/' % holder.id
+
+        result = self.client.login(username='super', password='secret')
+        self.assertEqual(result, True)
+
+    def tearDown(self):
+        self.client.logout()
+
+    def test_inline_add_m2m_noperm(self):
+        user = self.user
+        permission = Permission.objects.get(codename='add_author', content_type=self.author_ct)
+        user.user_permissions.add(permission)
+        # Make sure the inline is removed
+        response = self.client.get('/admin/admin_inlines/author/add/')
+        # This would be a TabularInline
+        self.assertNotContains(response, '<h2>Author-book relationships</h2>')
+        self.assertNotContains(response, 'Add another Author-Book Relationship')
+        self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
+
+    def test_inline_add_fk_noperm(self):
+        user = self.user
+        permission = Permission.objects.get(codename='add_holder', content_type=self.holder_ct)
+        user.user_permissions.add(permission)
+        response = self.client.get('/admin/admin_inlines/holder/add/')
+        # This would be a StackedInline
+        self.assertNotContains(response, '<h2>Inners</h2>')
+        self.assertNotContains(response, 'Add another Inner')
+        self.assertNotContains(response, 'id="id_inner_set-TOTAL_FORMS"')
+
+    def test_inline_change_m2m_noperm(self):
+        user = self.user
+        permission = Permission.objects.get(codename='change_author', content_type=self.author_ct)
+        user.user_permissions.add(permission)
+        response = self.client.get('/admin/admin_inlines/author/1/')
+        self.assertNotContains(response, '<h2>Author-book relationships</h2>')
+        self.assertNotContains(response, 'Add another Author-Book Relationship')
+        self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
+
+    def test_inline_change_fk_noperm(self):
+        user = self.user
+        permission = Permission.objects.get(codename='change_holder', content_type=self.holder_ct)
+        user.user_permissions.add(permission)
+        response = self.client.get(self.change_url)
+        self.assertNotContains(response, '<h2>Inners</h2>')
+        self.assertNotContains(response, 'Add another Inner')
+        self.assertNotContains(response, 'id="id_inner_set-TOTAL_FORMS"')
+
+    def test_inline_add_m2m_add_perm(self):
+        user = self.user
+        permission = Permission.objects.get(codename='add_author', content_type=self.author_ct)
+        user.user_permissions.add(permission)
+        permission = Permission.objects.get(codename='add_book', content_type=self.book_ct)
+        user.user_permissions.add(permission)
+        response = self.client.get('/admin/admin_inlines/author/add/')
+        self.assertNotContains(response, '<h2>Author-book relationships</h2>')
+        self.assertNotContains(response, 'Add another Author-Book Relationship')
+        self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
+
+    def test_inline_add_fk_add_perm(self):
+        user = self.user
+        permission = Permission.objects.get(codename='add_holder', content_type=self.holder_ct)
+        user.user_permissions.add(permission)
+        permission = Permission.objects.get(codename='add_inner', content_type=self.inner_ct)
+        user.user_permissions.add(permission)
+        response = self.client.get('/admin/admin_inlines/holder/add/')
+        self.assertContains(response, '<h2>Inners</h2>')
+        self.assertContains(response, 'Add another Inner')
+        self.assertContains(response, 'value="3" id="id_inner_set-TOTAL_FORMS"')
+
+    def test_inline_change_m2m_add_perm(self):
+        # We need the change permission on the related model to make changes to the
+        # intermediate model.
+        user = self.user
+        permission = Permission.objects.get(codename='change_author', content_type=self.author_ct)
+        user.user_permissions.add(permission)
+        permission = Permission.objects.get(codename='add_book', content_type=self.book_ct)
+        user.user_permissions.add(permission)
+        response = self.client.get('/admin/admin_inlines/author/1/')
+        self.assertNotContains(response, '<h2>Author-book relationships</h2>')
+        self.assertNotContains(response, 'Add another Author-Book Relationship')
+        self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
+        self.assertNotContains(response, 'id="id_Author_books-0-DELETE"')
+
+    def test_inline_change_m2m_change_perm(self):
+        # Editing the preexisting m2m relation as well as adding additional
+        # ones should be possible.
+        user = self.user
+        permission = Permission.objects.get(codename='change_author', content_type=self.author_ct)
+        user.user_permissions.add(permission)
+        permission = Permission.objects.get(codename='change_book', content_type=self.book_ct)
+        user.user_permissions.add(permission)
+        response = self.client.get('/admin/admin_inlines/author/1/')
+        self.assertContains(response, '<h2>Author-book relationships</h2>')
+        self.assertContains(response, 'Add another Author-Book Relationship')
+        self.assertContains(response, 'value="4" id="id_Author_books-TOTAL_FORMS"')
+        self.assertContains(response, '<input type="hidden" name="Author_books-0-id" value="1"')
+        self.assertContains(response, 'id="id_Author_books-0-DELETE"')
+
+    def test_inline_change_fk_add_perm(self):
+        user = self.user
+        permission = Permission.objects.get(codename='change_holder', content_type=self.holder_ct)
+        user.user_permissions.add(permission)
+        permission = Permission.objects.get(codename='add_inner', content_type=self.inner_ct)
+        user.user_permissions.add(permission)
+        response = self.client.get(self.change_url)
+        self.assertContains(response, '<h2>Inners</h2>')
+        self.assertContains(response, 'Add another Inner')
+        self.assertContains(response, 'value="3" id="id_inner_set-TOTAL_FORMS"')
+        self.assertNotContains(response, '<input type="hidden" name="inner_set-0-id" value="1"')
+
+    def test_inline_change_fk_change_perm(self):
+        user = self.user
+        permission = Permission.objects.get(codename='change_holder', content_type=self.holder_ct)
+        user.user_permissions.add(permission)
+        permission = Permission.objects.get(codename='change_inner', content_type=self.inner_ct)
+        user.user_permissions.add(permission)
+        response = self.client.get(self.change_url)
+        self.assertContains(response, '<h2>Inners</h2>')
+        self.assertContains(response, 'value="1" id="id_inner_set-TOTAL_FORMS"')
+        self.assertContains(response, '<input type="hidden" name="inner_set-0-id" value="1"')
+
+    def test_inline_change_fk_add_change_perm(self):
+        user = self.user
+        permission = Permission.objects.get(codename='change_holder', content_type=self.holder_ct)
+        user.user_permissions.add(permission)
+        permission = Permission.objects.get(codename='add_inner', content_type=self.inner_ct)
+        user.user_permissions.add(permission)
+        permission = Permission.objects.get(codename='change_inner', content_type=self.inner_ct)
+        user.user_permissions.add(permission)
+        response = self.client.get(self.change_url)
+        self.assertContains(response, '<h2>Inners</h2>')
+        self.assertContains(response, 'value="4" id="id_inner_set-TOTAL_FORMS"')
+        self.assertContains(response, '<input type="hidden" name="inner_set-0-id" value="1"')
+
+    def test_inline_change_fk_del_perm(self):
+        # The Author ForeignKey in the Book model does not allow NULL values,
+        # so we use different models this time.
+        user = self.user
+        collection = TitleCollection.objects.create(pk=1)
+        title = Title.objects.create(collection=collection, title1='foo', title2='foo')
+        collection_ct = ContentType.objects.get_for_model(TitleCollection)
+        title_ct = ContentType.objects.get_for_model(Title)
+        permission = Permission.objects.get(codename='change_titlecollection', content_type=collection_ct)
+        user.user_permissions.add(permission)
+        permission = Permission.objects.get(codename='change_title', content_type=title_ct)
+        user.user_permissions.add(permission)
+        permission = Permission.objects.get(codename='delete_title', content_type=title_ct)
+        user.user_permissions.add(permission)
+        response = self.client.get('/admin/admin_inlines/titlecollection/1/')
+        self.assertContains(response, '<h2>Titles</h2>')
+        self.assertContains(response, 'id="id_title_set-0-DELETE"')
