Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions cms/dashboard/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from cms.auth_content.constants import WILDCARD_ID_VALUE


def check_permissions(user_permissions, theme_id, sub_theme_id, topic_id) -> bool:
if not isinstance(user_permissions, list):
return False

for permission in user_permissions:
permission_theme_id = permission.get("theme", {}).get("id")
permission_sub_theme_id = permission.get("sub_theme", {}).get("id")
permission_topic_id = permission.get("topic", {}).get("id")

if permission_theme_id == WILDCARD_ID_VALUE:
return True

if (
permission_theme_id == theme_id
and permission_sub_theme_id == WILDCARD_ID_VALUE
):
return True

if (
permission_theme_id == theme_id
and permission_sub_theme_id == sub_theme_id
and (permission_topic_id in {WILDCARD_ID_VALUE, topic_id})
):
return True

return False
30 changes: 1 addition & 29 deletions cms/dashboard/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from caching.private_api.decorators import cache_response
from cms.auth_content.auth_utils import is_auth_enabled
from cms.auth_content.constants import WILDCARD_ID_VALUE
from cms.dashboard.permissions import check_permissions
from cms.dashboard.serializers import CMSDraftPagesSerializer, ListablePageSerializer
from cms.metrics_documentation.models.child import MetricsDocumentationChildEntry
from cms.topic.models import TopicPage
Expand All @@ -19,34 +19,6 @@
AUTH_ENABLED = is_auth_enabled()


def check_permissions(user_permissions, theme_id, sub_theme_id, topic_id) -> bool:
if not isinstance(user_permissions, list):
return False

for permission in user_permissions:
permission_theme_id = permission.get("theme", {}).get("id")
permission_sub_theme_id = permission.get("sub_theme", {}).get("id")
permission_topic_id = permission.get("topic", {}).get("id")

if permission_theme_id == WILDCARD_ID_VALUE:
return True

if (
permission_theme_id == theme_id
and permission_sub_theme_id == WILDCARD_ID_VALUE
):
return True

if (
permission_theme_id == theme_id
and permission_sub_theme_id == sub_theme_id
and (permission_topic_id in {WILDCARD_ID_VALUE, topic_id})
):
return True

return False


@extend_schema(tags=["cms"])
class CMSPagesAPIViewSet(PagesAPIViewSet):
# This is the /pages (or proxy/pages env dependent endpoint)
Expand Down
42 changes: 41 additions & 1 deletion cms/dynamic_content/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.db import models
from wagtail import blocks
from wagtail.blocks import (
BooleanBlock,
CharBlock,
ChoiceBlock,
PageChooserBlock,
Expand All @@ -13,6 +14,8 @@
)
from wagtail.snippets.blocks import SnippetChooserBlock


from cms.dashboard.permissions import check_permissions
from cms.dynamic_content import help_texts
from cms.dynamic_content.components import (
HeadlineNumberComponent,
Expand Down Expand Up @@ -72,7 +75,6 @@ class PageLinkChooserBlock(PageChooserBlock):
def get_api_representation(cls, value, context=None) -> str | None:
if value:
return value.full_url

return None


Expand Down Expand Up @@ -206,6 +208,44 @@ 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.permission_sets.get("permission_sets")
if user_permissions and hasattr(user_permissions, "permission_sets")
else None
)
if not check_permissions(
full_user_permissions,
getattr(page, "theme", None),
getattr(page, "sub_theme", None),
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()
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/cms/dashboard/test_viewsets.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import pytest

from cms.dashboard.permissions import check_permissions
from cms.dashboard.serializers import CMSDraftPagesSerializer, ListablePageSerializer
from cms.dashboard.viewsets import (
CMSDraftPagesViewSet,
CMSPagesAPIViewSet,
check_permissions,
)


Expand Down
172 changes: 171 additions & 1 deletion tests/unit/cms/dynamic_content/test_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -133,3 +133,173 @@ 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_permissions")
def test_non_public_page_permission_denied(self, mock_check_permissions):
"""
Given a non-public page and permissions denied
When get_api_representation() is called
Then the response is unauthorised and fields are blanked.
"""
mock_check_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 = {
"permission_sets": []
}

mock_request = mock.MagicMock()
mock_request.user = mock_user

value = {
"title": "Test title",
"sub_title": "Test subtitle",
"page": mock_page,
}

result = block.get_api_representation(
value=value, context={"request": mock_request}
)

assert result["is_authorised"] is False
assert result["title"] == ""
assert result["sub_title"] == ""
assert result["page"] == ""

mock_check_permissions.assert_called_once()

@mock.patch("cms.dynamic_content.blocks.check_permissions")
def test_non_public_page_permission_granted(self, mock_check_permissions):
"""
Given a non-public page and permissions granted
When get_api_representation() is called
Then the response is authorised and fields are preserved.
"""
mock_check_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 = "http://test-page-url"

mock_user = mock.MagicMock()
mock_user.permission_sets = mock.MagicMock()
mock_user.permission_sets.permission_sets = {
"permission_sets": []
}

mock_request = mock.MagicMock()
mock_request.user = mock_user

value = {
"title": "Test title",
"sub_title": "Test subtitle",
"page": mock_page,
}

result = block.get_api_representation(
value=value, context={"request": mock_request}
)

assert result["is_authorised"] is True
assert result["title"] == "Test title"
assert result["sub_title"] == "Test subtitle"
assert result["page"] == "http://test-page-url"

mock_check_permissions.assert_called_once()

@mock.patch("cms.dynamic_content.blocks.check_permissions")
def test_non_public_page_missing_request(self, mock_check_permissions):
"""
Given a non-public page without request context
When get_api_representation() is called
Then the response is unauthorised and fields are blanked.
"""
mock_check_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_permissions.assert_called_once()
Loading