Skip to content

Commit cd09129

Browse files
authored
fix: form input/Meta field name/missing mapping (sentry bugs) (baserow#5361)
1 parent 8336371 commit cd09129

13 files changed

Lines changed: 205 additions & 12 deletions

File tree

backend/src/baserow/api/utils.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -389,9 +389,6 @@ class Meta(extends_meta):
389389

390390
attrs = {"Meta": Meta}
391391

392-
if field_overrides:
393-
attrs.update(field_overrides)
394-
395392
def validate(self, value):
396393
if required_fields:
397394
for field_name in required_fields:
@@ -404,7 +401,7 @@ def validate(self, value):
404401

405402
attrs["validate"] = validate
406403
mixins = base_mixins or []
407-
return type(
404+
serializer_class = type(
408405
str(model_.__name__ + "Serializer"),
409406
(
410407
*mixins,
@@ -413,6 +410,18 @@ def validate(self, value):
413410
attrs,
414411
)
415412

413+
if field_overrides:
414+
# User-facing field names are valid serializer field names, but some of
415+
# them, such as "Meta" or "validate", collide with DRF class internals.
416+
# Register declared fields directly so those names can be serialized
417+
# without replacing the serializer configuration or methods.
418+
serializer_class._declared_fields = {
419+
**serializer_class._declared_fields,
420+
**field_overrides,
421+
}
422+
423+
return serializer_class
424+
416425

417426
class MappingSerializer:
418427
"""

backend/src/baserow/contrib/automation/application_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ def import_serialized(
216216
progress.increment(
217217
state=IMPORT_SERIALIZED_IMPORTING, by=integration_progress
218218
)
219+
id_mapping.setdefault("integrations", {}) # Just in case
219220
else:
220221
self.import_integrations_serialized(
221222
automation,

backend/src/baserow/contrib/automation/nodes/registries.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
from baserow.contrib.automation.automation_dispatch_context import (
66
AutomationDispatchContext,
77
)
8-
from baserow.contrib.automation.nodes.exceptions import AutomationNodeNotReplaceable
8+
from baserow.contrib.automation.nodes.exceptions import (
9+
AutomationNodeMisconfiguredService,
10+
AutomationNodeNotReplaceable,
11+
)
912
from baserow.contrib.automation.nodes.models import AutomationNode
1013
from baserow.contrib.automation.nodes.types import AutomationNodeDict, NodePositionType
1114
from baserow.contrib.automation.workflows.models import AutomationWorkflow
@@ -194,6 +197,12 @@ def deserialize_property(
194197
integration_id, integration_id
195198
)
196199
integration = Integration.objects.get(id=integration_id)
200+
workflow = kwargs.get("workflow")
201+
if (
202+
workflow is not None
203+
and integration.application_id != workflow.automation_id
204+
):
205+
integration = None
197206

198207
return ServiceHandler().import_service(
199208
integration,
@@ -229,9 +238,32 @@ def import_serialized(
229238
parent,
230239
serialized_values,
231240
id_mapping,
241+
workflow=parent,
232242
**kwargs,
233243
)
234244

245+
def _validate_service_integration_belongs_to_workflow(
246+
self,
247+
workflow: Optional[AutomationWorkflow],
248+
service_values: Dict[str, Any],
249+
) -> None:
250+
if not workflow or "integration_id" not in service_values:
251+
return
252+
253+
integration_id = service_values["integration_id"]
254+
if integration_id is None:
255+
return
256+
257+
integration = Integration.objects.filter(id=integration_id).first()
258+
if integration is None:
259+
return
260+
261+
if integration.application_id != workflow.automation_id:
262+
raise AutomationNodeMisconfiguredService(
263+
f"The integration with ID {integration_id} is not related to the "
264+
f"automation {workflow.automation_id}."
265+
)
266+
235267
def prepare_values(
236268
self,
237269
values: Dict[str, Any],
@@ -263,6 +295,11 @@ def prepare_values(
263295

264296
# If we received any service values, prepare them.
265297
service_values = values.pop("service", None) or {}
298+
workflow = instance.workflow if instance else values.get("workflow", None)
299+
self._validate_service_integration_belongs_to_workflow(
300+
workflow,
301+
service_values,
302+
)
266303
prepared_service_values = service_type.prepare_values(
267304
service_values, user, service if instance else None
268305
)

backend/src/baserow/contrib/automation/nodes/service.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,10 @@ def create_node(
174174

175175
node_type.before_create(workflow, reference_node, position, output)
176176

177-
prepared_values = node_type.prepare_values(kwargs, user)
177+
prepared_values = node_type.prepare_values(
178+
{**kwargs, "workflow": workflow},
179+
user,
180+
)
178181

179182
# Preselect first integration if exactly one exists
180183
if node_type.get_service_type().integration_type:
@@ -190,7 +193,6 @@ def create_node(
190193

191194
new_node = self.handler.create_node(
192195
node_type,
193-
workflow=workflow,
194196
**prepared_values,
195197
)
196198

backend/src/baserow/contrib/database/fields/field_types.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353

5454
from dateutil import parser
5555
from dateutil.parser import ParserError
56+
from drf_spectacular.utils import extend_schema_serializer
5657
from loguru import logger
5758
from rest_framework import serializers
5859

@@ -598,8 +599,7 @@ class NumberFieldType(FieldType):
598599
"The number_type option has been removed and can no longer be provided. "
599600
"Instead set number_decimal_places to 0 for an integer or 1-5 for a "
600601
"decimal."
601-
),
602-
"_spectacular_annotation": {"exclude_fields": ["number_type"]},
602+
)
603603
}
604604
_can_group_by = True
605605
_db_column_fields = ["number_decimal_places"]
@@ -616,6 +616,12 @@ def serialize_allowed_fields(self, field: Field) -> Dict[str, Any]:
616616
serialized[field_name] = value
617617
return serialized
618618

619+
def get_serializer_class(self, *args, **kwargs) -> serializers.ModelSerializer:
620+
serializer_class = super().get_serializer_class(*args, **kwargs)
621+
return extend_schema_serializer(exclude_fields=["number_type"])(
622+
serializer_class
623+
)
624+
619625
def serialize_to_input_value(self, field: Field, value: any) -> any:
620626
if field.specific.number_decimal_places == 0:
621627
return int(value)

backend/tests/baserow/api/test_api_utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,26 @@ def test_get_serializer_class(data_fixture):
391391
}
392392

393393

394+
@pytest.mark.django_db
395+
def test_get_serializer_class_with_fields_named_like_serializer_internals(data_fixture):
396+
workspace = data_fixture.create_workspace(name="Workspace 1")
397+
398+
workspace_serializer = get_serializer_class(
399+
Workspace,
400+
["Meta", "validate"],
401+
{
402+
"Meta": CharField(source="name"),
403+
"validate": CharField(source="name"),
404+
},
405+
)(workspace)
406+
407+
assert workspace_serializer.data == {
408+
"Meta": "Workspace 1",
409+
"validate": "Workspace 1",
410+
}
411+
assert workspace_serializer.__class__.Meta.model == Workspace
412+
413+
394414
@override_settings(DEBUG=False)
395415
def test_api_error_if_url_trailing_slash_is_missing(api_client):
396416
invalid_url = "/api/invalid-url"

backend/tests/baserow/contrib/automation/nodes/test_node_handler.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,23 @@ def test_import_node_only(data_fixture):
204204
"automation_workflow_nodes": {node.id: new_node.id},
205205
"services": {node.service_id: new_node.service_id},
206206
}
207+
208+
209+
@pytest.mark.django_db
210+
def test_import_node_only_ignores_integration_from_another_application(data_fixture):
211+
workflow = data_fixture.create_automation_workflow()
212+
trigger = workflow.get_trigger()
213+
other_integration = data_fixture.create_local_baserow_integration()
214+
215+
exported_node = AutomationNodeHandler().export_node(trigger)
216+
exported_node["service"]["integration_id"] = other_integration.id
217+
id_mapping = {
218+
"integrations": {},
219+
"automation_workflow_nodes": MirrorDict(),
220+
}
221+
222+
imported_node = AutomationNodeHandler().import_node_only(
223+
workflow, exported_node, id_mapping
224+
)
225+
226+
assert imported_node.service.specific.integration_id is None

backend/tests/baserow/contrib/automation/nodes/test_node_service.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from baserow.contrib.automation.nodes.exceptions import (
66
AutomationNodeDoesNotExist,
7+
AutomationNodeMisconfiguredService,
78
AutomationNodeNotMovable,
89
AutomationNodeReferenceNodeInvalid,
910
)
@@ -144,6 +145,30 @@ def test_create_node_permission_error(data_fixture: Fixtures):
144145
)
145146

146147

148+
@pytest.mark.django_db
149+
def test_create_node_rejects_integration_from_another_application(data_fixture):
150+
user = data_fixture.create_user()
151+
workflow = data_fixture.create_automation_workflow(user)
152+
other_integration = data_fixture.create_local_baserow_integration(user=user)
153+
154+
with pytest.raises(AutomationNodeMisconfiguredService) as exc:
155+
AutomationNodeService().create_node(
156+
user,
157+
automation_node_type_registry.get("local_baserow_create_row"),
158+
workflow,
159+
reference_node_id=workflow.get_trigger().id,
160+
position="south",
161+
output="",
162+
service={"integration_id": other_integration.id},
163+
)
164+
165+
assert (
166+
str(exc.value)
167+
== f"The integration with ID {other_integration.id} is not related to the "
168+
f"automation {workflow.automation_id}."
169+
)
170+
171+
147172
@pytest.mark.django_db
148173
def test_get_node(data_fixture: Fixtures):
149174
user = data_fixture.create_user()
@@ -779,6 +804,26 @@ def test_update_node_updates_workflow_dirty_cache(data_fixture):
779804
assert global_cache.get(cache_key, default=False) is True
780805

781806

807+
@pytest.mark.django_db
808+
def test_update_node_rejects_integration_from_another_application(data_fixture):
809+
user = data_fixture.create_user()
810+
node = data_fixture.create_local_baserow_create_row_action_node(user=user)
811+
other_integration = data_fixture.create_local_baserow_integration(user=user)
812+
813+
with pytest.raises(AutomationNodeMisconfiguredService) as exc:
814+
AutomationNodeService().update_node(
815+
user,
816+
node.id,
817+
service={"integration_id": other_integration.id},
818+
)
819+
820+
assert (
821+
str(exc.value)
822+
== f"The integration with ID {other_integration.id} is not related to the "
823+
f"automation {node.workflow.automation_id}."
824+
)
825+
826+
782827
@pytest.mark.django_db
783828
def test_create_node_updates_workflow_dirty_cache(data_fixture):
784829
"""

backend/tests/baserow/contrib/automation/test_automation_application_types.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,28 @@ def test_automation_application_import(data_fixture):
228228
)
229229

230230

231+
@pytest.mark.django_db
232+
def test_automation_application_import_initializes_integrations_mapping(data_fixture):
233+
workspace = data_fixture.create_workspace()
234+
id_mapping = {}
235+
236+
AutomationApplicationType().import_serialized(
237+
workspace,
238+
{
239+
"id": 1,
240+
"name": "Sample automation",
241+
"order": 1,
242+
"type": "automation",
243+
"integrations": [],
244+
"workflows": [],
245+
},
246+
ImportExportConfig(include_permission_data=True),
247+
id_mapping,
248+
)
249+
250+
assert id_mapping["integrations"] == {}
251+
252+
231253
@pytest.mark.django_db
232254
def test_fetch_workflows_to_serialize_without_user(data_fixture):
233255
workflow = data_fixture.create_automation_workflow(name="test")

backend/tests/baserow/contrib/database/api/rows/test_row_serializers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,28 @@ def test_get_row_serializer_with_user_field_names(
578578
assert test_result == expected_result
579579

580580

581+
@pytest.mark.django_db
582+
def test_get_row_serializer_with_user_field_names_named_meta(data_fixture):
583+
table = data_fixture.create_database_table(name="Cars")
584+
text_field = data_fixture.create_text_field(table=table, order=0, name="Meta")
585+
586+
model = table.get_model()
587+
row = model.objects.create(**{f"field_{text_field.id}": "Test value"})
588+
589+
serialized_row = get_row_serializer_class(
590+
model,
591+
RowSerializer,
592+
is_response=True,
593+
user_field_names=True,
594+
)(row).data
595+
596+
assert serialized_row == {
597+
"id": 1,
598+
"order": "1.00000000000000000000",
599+
"Meta": "Test value",
600+
}
601+
602+
581603
@pytest.mark.django_db
582604
def test_remap_serialized_row_to_user_field_names(data_fixture):
583605
user = data_fixture.create_user()

0 commit comments

Comments
 (0)