0
0
mirror of https://github.com/django/django.git synced 2024-11-22 03:18:31 +01:00

Fixed #17905 -- Restricted access to model pages in admindocs.

Only users with view or change model permissions can access.
Thank you to Sarah Boyce for the review.
This commit is contained in:
sai-ganesh-03 2024-11-07 16:01:14 +05:30 committed by Sarah Boyce
parent ef8ae06c2a
commit c12bc980e5
4 changed files with 143 additions and 7 deletions

View File

@ -13,7 +13,12 @@ from django.contrib.admindocs.utils import (
replace_named_groups, replace_named_groups,
replace_unnamed_groups, replace_unnamed_groups,
) )
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist from django.contrib.auth import get_permission_codename
from django.core.exceptions import (
ImproperlyConfigured,
PermissionDenied,
ViewDoesNotExist,
)
from django.db import models from django.db import models
from django.http import Http404 from django.http import Http404
from django.template.engine import Engine from django.template.engine import Engine
@ -202,11 +207,24 @@ class ViewDetailView(BaseAdminDocsView):
) )
def user_has_model_view_permission(user, opts):
"""Based off ModelAdmin.has_view_permission."""
codename_view = get_permission_codename("view", opts)
codename_change = get_permission_codename("change", opts)
return user.has_perm("%s.%s" % (opts.app_label, codename_view)) or user.has_perm(
"%s.%s" % (opts.app_label, codename_change)
)
class ModelIndexView(BaseAdminDocsView): class ModelIndexView(BaseAdminDocsView):
template_name = "admin_doc/model_index.html" template_name = "admin_doc/model_index.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
m_list = [m._meta for m in apps.get_models()] m_list = [
m._meta
for m in apps.get_models()
if user_has_model_view_permission(self.request.user, m._meta)
]
return super().get_context_data(**{**kwargs, "models": m_list}) return super().get_context_data(**{**kwargs, "models": m_list})
@ -228,6 +246,8 @@ class ModelDetailView(BaseAdminDocsView):
) )
opts = model._meta opts = model._meta
if not user_has_model_view_permission(self.request.user, opts):
raise PermissionDenied
title, body, metadata = utils.parse_docstring(model.__doc__) title, body, metadata = utils.parse_docstring(model.__doc__)
title = title and utils.parse_rst(title, "model", _("model:") + model_name) title = title and utils.parse_rst(title, "model", _("model:") + model_name)

View File

@ -56,13 +56,16 @@ Each of these support custom link text with the format
Support for custom link text was added. Support for custom link text was added.
.. _admindocs-model-reference:
Model reference Model reference
=============== ===============
The **models** section of the ``admindocs`` page describes each model in the The **models** section of the ``admindocs`` page describes each model that the
system along with all the fields, properties, and methods available on it. user has access to along with all the fields, properties, and methods available
Relationships to other models appear as hyperlinks. Descriptions are pulled on it. Relationships to other models appear as hyperlinks. Descriptions are
from ``help_text`` attributes on fields or from docstrings on model methods. pulled from ``help_text`` attributes on fields or from docstrings on model
methods.
A model with useful documentation might look like this:: A model with useful documentation might look like this::
@ -86,6 +89,11 @@ A model with useful documentation might look like this::
"""Makes the blog entry live on the site.""" """Makes the blog entry live on the site."""
... ...
.. versionchanged:: 5.2
Access was restricted to only allow users with model view or change
permissions.
View reference View reference
============== ==============

View File

@ -48,6 +48,8 @@ Minor features
format ``:role:`link text <link>```. See :ref:`documentation helpers format ``:role:`link text <link>```. See :ref:`documentation helpers
<admindocs-helpers>` for more details. <admindocs-helpers>` for more details.
* The :ref:`model pages <admindocs-model-reference>` are now restricted to only
allow access to users with the corresponding model view or change permissions.
:mod:`django.contrib.auth` :mod:`django.contrib.auth`
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -5,6 +5,8 @@ from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.contrib.admindocs import utils, views from django.contrib.admindocs import utils, views
from django.contrib.admindocs.views import get_return_data_type, simplify_regex from django.contrib.admindocs.views import get_return_data_type, simplify_regex
from django.contrib.auth.models import Permission, User
from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.db import models from django.db import models
from django.db.models import fields from django.db.models import fields
@ -482,6 +484,110 @@ class TestModelDetailView(TestDataMixin, AdminDocsTestCase):
) )
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_model_permission_denied(self):
person_url = reverse(
"django-admindocs-models-detail", args=["admin_docs", "person"]
)
company_url = reverse(
"django-admindocs-models-detail", args=["admin_docs", "company"]
)
staff_user = User.objects.create_user(
username="staff", password="secret", is_staff=True
)
self.client.force_login(staff_user)
response_for_person = self.client.get(person_url)
response_for_company = self.client.get(company_url)
# No access without permissions.
self.assertEqual(response_for_person.status_code, 403)
self.assertEqual(response_for_company.status_code, 403)
company_content_type = ContentType.objects.get_for_model(Company)
person_content_type = ContentType.objects.get_for_model(Person)
view_company = Permission.objects.get(
codename="view_company", content_type=company_content_type
)
change_person = Permission.objects.get(
codename="change_person", content_type=person_content_type
)
staff_user.user_permissions.add(view_company, change_person)
response_for_person = self.client.get(person_url)
response_for_company = self.client.get(company_url)
# View or change permission grants access.
self.assertEqual(response_for_person.status_code, 200)
self.assertEqual(response_for_company.status_code, 200)
@unittest.skipUnless(utils.docutils_is_available, "no docutils installed.")
class TestModelIndexView(TestDataMixin, AdminDocsTestCase):
def test_model_index_superuser(self):
self.client.force_login(self.superuser)
index_url = reverse("django-admindocs-models-index")
response = self.client.get(index_url)
self.assertContains(
response,
'<a href="/admindocs/models/admin_docs.family/">Family</a>',
html=True,
)
self.assertContains(
response,
'<a href="/admindocs/models/admin_docs.person/">Person</a>',
html=True,
)
self.assertContains(
response,
'<a href="/admindocs/models/admin_docs.company/">Company</a>',
html=True,
)
def test_model_index_with_model_permission(self):
staff_user = User.objects.create_user(
username="staff", password="secret", is_staff=True
)
self.client.force_login(staff_user)
index_url = reverse("django-admindocs-models-index")
response = self.client.get(index_url)
# Models are not listed without permissions.
self.assertNotContains(
response,
'<a href="/admindocs/models/admin_docs.family/">Family</a>',
html=True,
)
self.assertNotContains(
response,
'<a href="/admindocs/models/admin_docs.person/">Person</a>',
html=True,
)
self.assertNotContains(
response,
'<a href="/admindocs/models/admin_docs.company/">Company</a>',
html=True,
)
company_content_type = ContentType.objects.get_for_model(Company)
person_content_type = ContentType.objects.get_for_model(Person)
view_company = Permission.objects.get(
codename="view_company", content_type=company_content_type
)
change_person = Permission.objects.get(
codename="change_person", content_type=person_content_type
)
staff_user.user_permissions.add(view_company, change_person)
response = self.client.get(index_url)
# View or change permission grants access.
self.assertNotContains(
response,
'<a href="/admindocs/models/admin_docs.family/">Family</a>',
html=True,
)
self.assertContains(
response,
'<a href="/admindocs/models/admin_docs.person/">Person</a>',
html=True,
)
self.assertContains(
response,
'<a href="/admindocs/models/admin_docs.company/">Company</a>',
html=True,
)
class CustomField(models.Field): class CustomField(models.Field):
description = "A custom field type" description = "A custom field type"