diff --git a/cms/dynamic_content/blocks.py b/cms/dynamic_content/blocks.py index e57afd8ba..43f1c7d0a 100644 --- a/cms/dynamic_content/blocks.py +++ b/cms/dynamic_content/blocks.py @@ -1,5 +1,6 @@ from django.core.exceptions import ValidationError from django.db import models +from common.auth.permissions import check_page_permissions from wagtail import blocks from wagtail.blocks import ( CharBlock, @@ -72,7 +73,6 @@ class PageLinkChooserBlock(PageChooserBlock): def get_api_representation(cls, value, context=None) -> str | None: if value: return value.full_url - return None @@ -206,6 +206,42 @@ class PageLink(StructBlock): ) page = PageLinkChooserBlock(target_model=["topic.TopicPage"]) + def get_api_representation(self, value, context=None): + data = super().get_api_representation(value, context) + page = value.get("page") + + if not page: + data["is_authorised"] = False + data["title"] = "" + data["sub_title"] = "" + data["page"] = "" + return data + + page = page.specific + request = context.get("request") if context else None + user = getattr(request, "user", None) + + if not page.is_public: + user_permissions = getattr(user, "permission_sets", None) + + full_user_permissions = ( + user_permissions.get("permission_sets") if user_permissions else None + ) + if not check_page_permissions( + permission_sets=full_user_permissions, + theme_id=getattr(page, "theme", None), + sub_theme_id=getattr(page, "sub_theme", None), + topic_id=getattr(page, "topic", None), + ): + data["is_authorised"] = False + data["title"] = "" + data["sub_title"] = "" + data["page"] = "" + return data + + data["is_authorised"] = True + return data + class InternalPageLinks(StreamBlock): page_link = PageLink() diff --git a/tests/unit/cms/dynamic_content/test_blocks.py b/tests/unit/cms/dynamic_content/test_blocks.py index fe4fb9d79..5bbadaaa5 100644 --- a/tests/unit/cms/dynamic_content/test_blocks.py +++ b/tests/unit/cms/dynamic_content/test_blocks.py @@ -6,7 +6,7 @@ import pytest from wagtail.blocks import StructBlock, StructValue -from cms.dynamic_content.blocks import SourceLinkBlock +from cms.dynamic_content.blocks import PageLink, SourceLinkBlock class TestSourceLinkBlockClean: @@ -133,3 +133,169 @@ def test_does_not_raise_when_only_external_url_set(self): ) SourceLinkBlock._validate_only_one_of_page_or_external_url(value=value) + + +class TestPageLinkBlock: + """Tests for PageLink.get_api_representation().""" + + def test_no_page_returns_unauthorised(self): + """ + Given a value with no page set + When get_api_representation() is called + Then the response is unauthorised and fields are blanked. + """ + block = PageLink() + value = { + "title": "Test title", + "sub_title": "Test subtitle", + "page": None, + } + + result = block.get_api_representation(value=value, context={}) + + assert result["is_authorised"] is False + assert result["title"] == "" + assert result["sub_title"] == "" + assert result["page"] == "" + + def test_public_page_is_always_authorised(self): + """ + Given a public page + When get_api_representation() is called + Then the response is authorised and fields are preserved. + """ + block = PageLink() + + mock_page = mock.MagicMock() + mock_page.specific = mock_page + mock_page.is_public = True + + value = { + "title": "Test title", + "sub_title": "Test subtitle", + "page": mock_page, + } + + result = block.get_api_representation(value=value, context={}) + + assert result["is_authorised"] is True + assert result["title"] == "Test title" + assert result["sub_title"] == "Test subtitle" + + @mock.patch("cms.dynamic_content.blocks.check_page_permissions") + def test_non_public_page_permission_denied(self, mock_check_page_permissions): + """ + Given a non-public page and permissions are denied + When get_api_representation() is called + Then the response is unauthorised and fields are blanked. + """ + mock_check_page_permissions.return_value = False + + block = PageLink() + + mock_page = mock.MagicMock() + mock_page.specific = mock_page + mock_page.is_public = False + mock_page.theme = 1 + mock_page.sub_theme = 2 + mock_page.topic = 3 + + mock_user = mock.MagicMock() + mock_user.permission_sets = mock.MagicMock() + mock_user.permission_sets = {"permission_sets": []} + + mock_request = mock.MagicMock() + mock_request.user = mock_user + + value = { + "title": "Test title", + "sub_title": "Test subtitle", + "page": mock_page, + } + + context = {"request": mock_request} + + result = block.get_api_representation(value=value, context=context) + + assert result["is_authorised"] is False + assert result["title"] == "" + assert result["sub_title"] == "" + assert result["page"] == "" + + mock_check_page_permissions.assert_called_once() + + @mock.patch("cms.dynamic_content.blocks.check_page_permissions") + def test_non_public_page_permission_granted(self, mock_check_page_permissions): + """ + Given a non-public page and permissions are granted + When get_api_representation() is called + Then the response is authorised and fields are preserved. + """ + mock_check_page_permissions.return_value = True + + block = PageLink() + + mock_page = mock.MagicMock() + mock_page.specific = mock_page + mock_page.is_public = False + mock_page.theme = 1 + mock_page.sub_theme = 2 + mock_page.topic = 3 + mock_page.full_url = "https://test-page-url" + + mock_user = mock.MagicMock() + mock_user.permission_sets = mock.MagicMock() + mock_user.permission_sets = {"permission_sets": []} + + mock_request = mock.MagicMock() + mock_request.user = mock_user + + value = { + "title": "Test title", + "sub_title": "Test subtitle", + "page": mock_page, + } + + context = {"request": mock_request} + + result = block.get_api_representation(value=value, context=context) + + assert result["is_authorised"] is True + assert result["title"] == "Test title" + assert result["sub_title"] == "Test subtitle" + assert result["page"] == "https://test-page-url" + + mock_check_page_permissions.assert_called_once() + + @mock.patch("cms.dynamic_content.blocks.check_page_permissions") + def test_non_public_page_missing_request(self, mock_check_page_permissions): + """ + Given a non-public page and no request in context + When get_api_representation() is called + Then the response is unauthorised and fields are blanked. + """ + mock_check_page_permissions.return_value = False + + block = PageLink() + + mock_page = mock.MagicMock() + mock_page.specific = mock_page + mock_page.is_public = False + mock_page.theme = 1 + mock_page.sub_theme = 2 + mock_page.topic = 3 + + value = { + "title": "Test title", + "sub_title": "Test subtitle", + "page": mock_page, + } + + result = block.get_api_representation(value=value, context={}) + + assert result["is_authorised"] is False + assert result["title"] == "" + assert result["sub_title"] == "" + assert result["page"] == "" + + mock_check_page_permissions.assert_called_once() diff --git a/tests/unit/common/auth/test_permissions.py b/tests/unit/common/auth/test_permissions.py index cf077e2b0..4caa97093 100644 --- a/tests/unit/common/auth/test_permissions.py +++ b/tests/unit/common/auth/test_permissions.py @@ -1090,6 +1090,7 @@ def test_check_page_permissions_invalid_access( "user_permissions, theme_id, sub_theme_id, topic_id", [ ([{}], "10", "20", "30"), + ("invalid", "10", "20", "30"), (None, "10", "20", "30"), ([{"sub_theme": {"id": "-1"}, "topic": {"id": "-1"}}], "10", "20", "30"), ( @@ -1125,3 +1126,38 @@ def test_check_page_permissions_with_missing_values( sub_theme_id=sub_theme_id, topic_id=topic_id, ) + + @pytest.mark.parametrize( + "user_permissions", + [ + ["invalid"], + [123], + [None], + [{"theme": {"id": "10"}}, "invalid"], + ], + ) + def test_check_page_permissions_non_dict_permission_entry(self, user_permissions): + assert not check_page_permissions( + permission_sets=user_permissions, + theme_id="10", + sub_theme_id="20", + topic_id="30", + ) + + @pytest.mark.parametrize( + "theme_id, sub_theme_id, topic_id", + [ + (None, "20", "30"), + ("10", None, "30"), + ("10", "20", None), + ], + ) + def test_check_page_permissions_invalid_resource_ids( + self, theme_id, sub_theme_id, topic_id + ): + assert not check_page_permissions( + permission_sets=[{"theme": {"id": "-1"}}], + theme_id=theme_id, + sub_theme_id=sub_theme_id, + topic_id=topic_id, + )