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_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.http import Http404
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):
template_name = "admin_doc/model_index.html"
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})
@ -228,6 +246,8 @@ class ModelDetailView(BaseAdminDocsView):
)
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 = 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.
.. _admindocs-model-reference:
Model reference
===============
The **models** section of the ``admindocs`` page describes each model in the
system along with all the fields, properties, and methods available on it.
Relationships to other models appear as hyperlinks. Descriptions are pulled
from ``help_text`` attributes on fields or from docstrings on model methods.
The **models** section of the ``admindocs`` page describes each model that the
user has access to along with all the fields, properties, and methods available
on it. Relationships to other models appear as hyperlinks. Descriptions are
pulled from ``help_text`` attributes on fields or from docstrings on model
methods.
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."""
...
.. versionchanged:: 5.2
Access was restricted to only allow users with model view or change
permissions.
View reference
==============

View File

@ -47,7 +47,9 @@ Minor features
* Links to components in docstrings now supports custom link text, using the
format ``:role:`link text <link>```. See :ref:`documentation helpers
<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`
~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -5,6 +5,8 @@ from django.conf import settings
from django.contrib import admin
from django.contrib.admindocs import utils, views
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.db import models
from django.db.models import fields
@ -482,6 +484,110 @@ class TestModelDetailView(TestDataMixin, AdminDocsTestCase):
)
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):
description = "A custom field type"