Skip to content

Commit 33a66ba

Browse files
authored
Check permissions for collection element during dispatch (baserow#5356)
1 parent e54ea1f commit 33a66ba

6 files changed

Lines changed: 258 additions & 19 deletions

File tree

backend/src/baserow/contrib/builder/api/data_providers/serializers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
from rest_framework import serializers
33
from rest_framework.exceptions import ValidationError
44

5+
from baserow.contrib.builder.data_sources.exceptions import (
6+
DataSourceRefinementForbidden,
7+
)
8+
from baserow.contrib.builder.elements.exceptions import ElementDoesNotExist
59
from baserow.contrib.builder.elements.models import Element
10+
from baserow.contrib.builder.elements.service import ElementService
11+
from baserow.core.exceptions import PermissionException
612

713
IANA_TIMEZONES = [(tz, tz) for tz in pytz.all_timezones]
814

@@ -27,6 +33,15 @@ def validate(self, data):
2733
page = self.context.get("page")
2834
element = data.get("element")
2935
if element:
36+
user = self.context.get("user")
37+
if user is not None:
38+
try:
39+
data["element"] = ElementService().get_element(user, element.id)
40+
except (ElementDoesNotExist, PermissionException):
41+
raise DataSourceRefinementForbidden(
42+
"The data source is not available for the dispatched element."
43+
) from None
44+
3045
if (
3146
element.page_id != page.id
3247
and element.page.builder.shared_page.id != page.id

backend/src/baserow/contrib/builder/data_sources/builder_dispatch_context.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,14 @@ def request_data(self) -> Dict:
104104

105105
serializer = DynamicMetadataSerializer(
106106
data=getattr(self.request, "data", {}).get("metadata", {}),
107-
context={"page": self.page},
107+
context={
108+
"page": self.page,
109+
"user": getattr(
110+
self.request,
111+
"user_source_user",
112+
getattr(self.request, "user", None),
113+
),
114+
},
108115
)
109116
serializer.is_valid(raise_exception=True)
110117

backend/src/baserow/contrib/builder/elements/permission_manager.py

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
from django.contrib.auth.models import AbstractUser
33
from django.db.models import Q, QuerySet
44

5-
from baserow.contrib.builder.elements.operations import ListElementsPageOperationType
5+
from baserow.contrib.builder.elements.operations import (
6+
ListElementsPageOperationType,
7+
ReadElementOperationType,
8+
)
69
from baserow.contrib.builder.pages.models import Page
710
from baserow.contrib.builder.workflow_actions.operations import (
811
DispatchBuilderWorkflowActionOperationType,
@@ -70,6 +73,53 @@ def auth_user_can_view_element(self, user, element):
7073
# Return False by default for safety
7174
return False
7275

76+
def actor_can_view_page(self, actor, page):
77+
"""
78+
Return True if the actor is allowed to view the page.
79+
"""
80+
81+
if isinstance(actor, User):
82+
return True
83+
84+
if page.visibility != Page.VISIBILITY_TYPES.LOGGED_IN:
85+
return True
86+
87+
if not getattr(actor, "is_authenticated", False):
88+
return False
89+
90+
if page.role_type == Page.ROLE_TYPES.ALLOW_ALL:
91+
return True
92+
elif page.role_type == Page.ROLE_TYPES.ALLOW_ALL_EXCEPT:
93+
return actor.role not in page.roles
94+
elif page.role_type == Page.ROLE_TYPES.DISALLOW_ALL_EXCEPT:
95+
return actor.role in page.roles
96+
97+
return False
98+
99+
def actor_can_view_element(self, actor, element):
100+
"""
101+
Return True if the actor is allowed to view the element, taking element
102+
and page visibility and role settings into account.
103+
"""
104+
105+
if not self.actor_can_view_page(actor, element.page):
106+
return False
107+
108+
current_element = element
109+
while current_element is not None:
110+
if getattr(actor, "is_authenticated", False):
111+
if current_element.visibility == Element.VISIBILITY_TYPES.NOT_LOGGED:
112+
return False
113+
114+
if not self.auth_user_can_view_element(actor, current_element):
115+
return False
116+
elif current_element.visibility == Element.VISIBILITY_TYPES.LOGGED_IN:
117+
return False
118+
119+
current_element = current_element.parent_element
120+
121+
return True
122+
73123
def check_multiple_permissions(
74124
self,
75125
checks,
@@ -85,22 +135,10 @@ def check_multiple_permissions(
85135

86136
for check in checks:
87137
if check.operation_name == DispatchBuilderWorkflowActionOperationType.type:
88-
if getattr(check.actor, "is_authenticated", False):
89-
if (
90-
check.context.element.visibility
91-
== Element.VISIBILITY_TYPES.NOT_LOGGED
92-
):
93-
result[check] = False
94-
elif not self.auth_user_can_view_element(
95-
check.actor, check.context.element
96-
):
97-
result[check] = False
98-
else:
99-
if (
100-
check.context.element.visibility
101-
== Element.VISIBILITY_TYPES.LOGGED_IN
102-
):
103-
result[check] = False
138+
if not self.actor_can_view_element(check.actor, check.context.element):
139+
result[check] = False
140+
elif check.operation_name == ReadElementOperationType.type:
141+
result[check] = self.actor_can_view_element(check.actor, check.context)
104142

105143
return result
106144

backend/tests/baserow/contrib/builder/api/domains/test_domain_public_views.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from unittest.mock import ANY, MagicMock, patch
23

34
from django.test.utils import override_settings
@@ -1066,6 +1067,96 @@ def test_public_dispatch_data_source_view_returns_some_fields(
10661067
}
10671068

10681069

1070+
@pytest.mark.django_db
1071+
def test_public_dispatch_data_source_cannot_use_hidden_element_refinements(
1072+
api_client, data_fixture
1073+
):
1074+
user = data_fixture.create_user()
1075+
builder_from = data_fixture.create_builder_application(user=user)
1076+
builder = data_fixture.create_builder_application(workspace=None)
1077+
data_fixture.create_builder_custom_domain(
1078+
builder=builder_from, published_to=builder
1079+
)
1080+
public_page = data_fixture.create_builder_page(builder=builder)
1081+
hidden_page = data_fixture.create_builder_page(
1082+
builder=builder, visibility=Page.VISIBILITY_TYPES.LOGGED_IN
1083+
)
1084+
integration = data_fixture.create_local_baserow_integration(
1085+
application=builder, authorized_user=user, name="test"
1086+
)
1087+
table, _, _ = data_fixture.build_table(
1088+
user=user,
1089+
columns=[
1090+
("Name", "text"),
1091+
("SSN", "text"),
1092+
],
1093+
rows=[
1094+
["Peter", "111"],
1095+
["Afonso", "222"],
1096+
],
1097+
)
1098+
public_field = table.field_set.get(name="Name")
1099+
private_field = table.field_set.get(name="SSN")
1100+
data_source = data_fixture.create_builder_local_baserow_list_rows_data_source(
1101+
user=user,
1102+
page=builder.shared_page,
1103+
integration=integration,
1104+
table=table,
1105+
)
1106+
1107+
visible_element = data_fixture.create_builder_table_element(
1108+
page=public_page,
1109+
data_source=data_source,
1110+
fields=[
1111+
{
1112+
"name": "Name",
1113+
"type": "text",
1114+
"config": {"value": f"get('current_record.{public_field.db_column}')"},
1115+
},
1116+
],
1117+
)
1118+
visible_element.property_options.create(
1119+
schema_property=private_field.db_column, filterable=False
1120+
)
1121+
1122+
hidden_element = data_fixture.create_builder_table_element(
1123+
page=hidden_page,
1124+
data_source=data_source,
1125+
visibility=Element.VISIBILITY_TYPES.LOGGED_IN,
1126+
)
1127+
hidden_element.property_options.create(
1128+
schema_property=private_field.db_column, filterable=True
1129+
)
1130+
1131+
advanced_filters = {
1132+
"filter_type": "AND",
1133+
"filters": [
1134+
{
1135+
"field": private_field.id,
1136+
"type": "equal",
1137+
"value": "222",
1138+
}
1139+
],
1140+
}
1141+
url = reverse(
1142+
"api:builder:domains:public_dispatch",
1143+
kwargs={"data_source_id": data_source.id},
1144+
)
1145+
1146+
response = api_client.post(
1147+
f"{url}?filters={json.dumps(advanced_filters)}",
1148+
{"metadata": {"data_source": {"element": hidden_element.id}}},
1149+
format="json",
1150+
)
1151+
1152+
assert response.status_code == HTTP_400_BAD_REQUEST
1153+
assert response.json() == {
1154+
"error": "ERROR_DATA_SOURCE_REFINEMENT_FORBIDDEN",
1155+
"detail": "Data source filter, search and/or sort fields error: "
1156+
"The data source is not available for the dispatched element.",
1157+
}
1158+
1159+
10691160
@pytest.mark.django_db
10701161
def test_public_dispatch_data_sources_get_row_no_elements(
10711162
api_client, data_fixture, user_source_user_fixture

backend/tests/baserow/contrib/builder/elements/test_element_permission_manager.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import pytest
44

55
from baserow.contrib.builder.elements.models import Element
6-
from baserow.contrib.builder.elements.operations import ListElementsPageOperationType
6+
from baserow.contrib.builder.elements.operations import (
7+
ListElementsPageOperationType,
8+
ReadElementOperationType,
9+
)
710
from baserow.contrib.builder.elements.permission_manager import (
811
ElementVisibilityPermissionManager,
912
)
@@ -255,6 +258,82 @@ def test_element_visibility_permission_manager_filter_queryset(
255258
)
256259

257260

261+
@pytest.mark.django_db
262+
def test_element_visibility_permission_manager_read_element_permission(
263+
data_fixture,
264+
stub_user_source_registry,
265+
):
266+
user = data_fixture.create_user(username="Auth user")
267+
builder = data_fixture.create_builder_application(user=user)
268+
builder_to = data_fixture.create_builder_application(workspace=None)
269+
data_fixture.create_builder_custom_domain(builder=builder, published_to=builder_to)
270+
public_page = data_fixture.create_builder_page(builder=builder_to)
271+
logged_in_page = data_fixture.create_builder_page(
272+
builder=builder_to, visibility=Page.VISIBILITY_TYPES.LOGGED_IN
273+
)
274+
public_user_source = data_fixture.create_user_source_with_first_type(
275+
application=builder_to
276+
)
277+
public_user_source_user = UserSourceUser(
278+
public_user_source, None, 1, "US public", "e@ma.il"
279+
)
280+
281+
element_all = data_fixture.create_builder_button_element(
282+
page=public_page, visibility=Element.VISIBILITY_TYPES.ALL
283+
)
284+
element_logged_in = data_fixture.create_builder_button_element(
285+
page=public_page, visibility=Element.VISIBILITY_TYPES.LOGGED_IN
286+
)
287+
element_not_logged = data_fixture.create_builder_button_element(
288+
page=public_page, visibility=Element.VISIBILITY_TYPES.NOT_LOGGED
289+
)
290+
element_on_logged_in_page = data_fixture.create_builder_button_element(
291+
page=logged_in_page, visibility=Element.VISIBILITY_TYPES.ALL
292+
)
293+
294+
checks = [
295+
PermissionCheck(
296+
public_user_source_user, ReadElementOperationType.type, element_all
297+
),
298+
PermissionCheck(
299+
public_user_source_user, ReadElementOperationType.type, element_logged_in
300+
),
301+
PermissionCheck(
302+
public_user_source_user, ReadElementOperationType.type, element_not_logged
303+
),
304+
PermissionCheck(
305+
public_user_source_user,
306+
ReadElementOperationType.type,
307+
element_on_logged_in_page,
308+
),
309+
PermissionCheck(AnonymousUser(), ReadElementOperationType.type, element_all),
310+
PermissionCheck(
311+
AnonymousUser(), ReadElementOperationType.type, element_logged_in
312+
),
313+
PermissionCheck(
314+
AnonymousUser(), ReadElementOperationType.type, element_not_logged
315+
),
316+
PermissionCheck(
317+
AnonymousUser(), ReadElementOperationType.type, element_on_logged_in_page
318+
),
319+
]
320+
321+
result = ElementVisibilityPermissionManager().check_multiple_permissions(
322+
checks, builder.workspace
323+
)
324+
325+
assert [result.get(check, None) for check in checks] == [
326+
True,
327+
True,
328+
False,
329+
True,
330+
True,
331+
False,
332+
True,
333+
False,
334+
]
335+
336+
258337
@pytest.fixture(autouse=True)
259338
def ab_builder_user_page(data_fixture):
260339
"""A fixture to help test Element permissions."""
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "bug",
3+
"message": "Check permission on the element provided during a collection element data source dispatch",
4+
"issue_origin": "github",
5+
"issue_number": null,
6+
"domain": "builder",
7+
"bullet_points": [],
8+
"created_at": "2026-05-12"
9+
}

0 commit comments

Comments
 (0)