From 5cc582b638b9f2fd5b34a2db0b23cdf3e72e520a Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 5 Nov 2025 10:37:01 +0100 Subject: [PATCH] Validate fields used in custom form config --- src/servala/core/forms.py | 72 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/servala/core/forms.py b/src/servala/core/forms.py index 9742233..36f8be5 100644 --- a/src/servala/core/forms.py +++ b/src/servala/core/forms.py @@ -178,8 +178,80 @@ class ServiceDefinitionAdminForm(forms.ModelForm): {"form_config": _("Schema error: {}").format(e.message)} ) + self._validate_field_mappings(form_config, cleaned_data) + return cleaned_data + def _validate_field_mappings(self, form_config, cleaned_data): + if not self.instance.pk: + return + crd = self.instance.offering_control_planes.all().first() + if not crd: + return + + schema = None + try: + schema = crd.resource_schema + except Exception as e: + pass + + if not schema or not (spec_schema := schema.get("properties", {}).get("spec")): + return + + valid_paths = self._extract_field_paths(spec_schema, "spec") | {"name"} + included_mappings = set() + errors = [] + for fieldset in form_config.get("fieldsets", []): + for field in fieldset.get("fields", []): + mapping = field.get("controlplane_field_mapping") + included_mappings.add(mapping) + if mapping and mapping not in valid_paths: + field_name = field.get("name", mapping) + errors.append( + _( + "Field '{}' has invalid mapping '{}'. Valid paths are: {}" + ).format( + field_name, + mapping, + ", ".join(sorted(valid_paths)[:10]) + + ("..." if len(valid_paths) > 10 else ""), + ) + ) + + if "name" not in included_mappings: + raise forms.ValidationError( + { + "form_config": _( + "You must include a `name` field in the custom form config." + ) + } + ) + + if errors: + raise forms.ValidationError({"form_config": errors}) + + def _extract_field_paths(self, schema, prefix=""): + paths = set() + + if not isinstance(schema, dict): + return paths + + if "type" in schema and schema["type"] != "object": + if prefix: + paths.add(prefix) + + if schema.get("properties"): + for prop_name, prop_schema in schema["properties"].items(): + new_prefix = f"{prefix}.{prop_name}" if prefix else prop_name + paths.add(new_prefix) + paths.update(self._extract_field_paths(prop_schema, new_prefix)) + + if schema.get("type") == "array" and "items" in schema: + if prefix: + paths.add(prefix) + + return paths + def save(self, *args, **kwargs): self.instance.api_definition = self.cleaned_data["api_definition"] return super().save(*args, **kwargs)