From 957343fc9097a0826af9f091ac59572c90d0767c Mon Sep 17 00:00:00 2001 From: Gerrod Ubben Date: Wed, 4 Mar 2026 16:15:13 -0500 Subject: [PATCH 1/2] Fix traceback on null pulp_labels values The pulp_labels_validator crashed with "expected string or bytes-like object" when a label value was null because re.search() doesn't accept None. Skip the regex check for None values since they are already accepted by the set_label endpoint and HStoreField. closes #6593 Assisted-by: Claude (Cursor) Made-with: Cursor --- CHANGES/6593.bugfix | 1 + pulpcore/app/serializers/fields.py | 2 +- .../tests/unit/serializers/test_fields.py | 31 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 CHANGES/6593.bugfix diff --git a/CHANGES/6593.bugfix b/CHANGES/6593.bugfix new file mode 100644 index 00000000000..afc313b9f81 --- /dev/null +++ b/CHANGES/6593.bugfix @@ -0,0 +1 @@ +Fixed a traceback when setting `pulp_labels` with null values on create or update. diff --git a/pulpcore/app/serializers/fields.py b/pulpcore/app/serializers/fields.py index dbb22612398..762e9fe88e4 100644 --- a/pulpcore/app/serializers/fields.py +++ b/pulpcore/app/serializers/fields.py @@ -429,7 +429,7 @@ def pulp_labels_validator(value): for k, v in value.items(): if not re.match(r"^[\w ]+$", k): raise serializers.ValidationError(_("Key '{}' contains non-alphanumerics.").format(k)) - if re.search(r"[,()]", v): + if v is not None and re.search(r"[,()]", v): raise serializers.ValidationError( _("Key '{}' contains value with comma or parenthesis.").format(k) ) diff --git a/pulpcore/tests/unit/serializers/test_fields.py b/pulpcore/tests/unit/serializers/test_fields.py index 506d4c1361d..8e68def11fa 100644 --- a/pulpcore/tests/unit/serializers/test_fields.py +++ b/pulpcore/tests/unit/serializers/test_fields.py @@ -2,6 +2,37 @@ from rest_framework import serializers from pulpcore.app.serializers import fields +from pulpcore.app.serializers.fields import pulp_labels_validator + + +@pytest.mark.parametrize( + "labels", + [ + {"key": "value"}, + {"key": ""}, + {"key": None}, + {"key1": "value", "key2": None, "key3": ""}, + ], +) +def test_pulp_labels_validator_valid(labels): + """Valid label values including None should pass validation.""" + result = pulp_labels_validator(labels) + assert result == labels + + +@pytest.mark.parametrize( + "labels", + [ + {"key": "val,ue"}, + {"key": "val(ue"}, + {"key": "val)ue"}, + {"bad!key": "value"}, + ], +) +def test_pulp_labels_validator_invalid(labels): + """Invalid label keys or values should raise ValidationError.""" + with pytest.raises(serializers.ValidationError): + pulp_labels_validator(labels) @pytest.mark.parametrize( From 56a2d8006e1ac63827b5e9f9b31cb58a72370827 Mon Sep 17 00:00:00 2001 From: Gerrod Ubben Date: Wed, 4 Mar 2026 16:37:09 -0500 Subject: [PATCH 2/2] Unify pulp_labels key validation to allow hyphens, spaces, and dots The three places validating label keys (pulp_labels_validator, SetLabelSerializer/UnsetLabelSerializer, and LabelFilter) each allowed different characters, making it possible to create labels that couldn't be modified or filtered. Unify them all to accept alphanumerics, underscores, spaces, hyphens, and dots. closes #6456 Assisted-by: Claude-Opus-4.6 (Cursor) --- CHANGES/6456.bugfix | 1 + pulpcore/app/serializers/base.py | 10 ++++++-- pulpcore/app/serializers/fields.py | 10 ++++++-- pulpcore/app/viewsets/custom_filters.py | 3 ++- pulpcore/constants.py | 4 +++ .../tests/unit/serializers/test_fields.py | 25 ++++++++++++------- 6 files changed, 39 insertions(+), 14 deletions(-) create mode 100644 CHANGES/6456.bugfix diff --git a/CHANGES/6456.bugfix b/CHANGES/6456.bugfix new file mode 100644 index 00000000000..1c2ee912803 --- /dev/null +++ b/CHANGES/6456.bugfix @@ -0,0 +1 @@ +Unified `pulp_labels` key validation across create/update, `set_label`/`unset_label`, and label filters to consistently allow alphanumerics, underscores, spaces, hyphens, and dots. diff --git a/pulpcore/app/serializers/base.py b/pulpcore/app/serializers/base.py index e693cfc1e22..0b51442baa6 100644 --- a/pulpcore/app/serializers/base.py +++ b/pulpcore/app/serializers/base.py @@ -564,16 +564,22 @@ class SetLabelSerializer(serializers.Serializer): Serializer for synchronously setting a label. """ - key = serializers.SlugField(required=True) + key = serializers.CharField(required=True) value = serializers.CharField(required=True, allow_null=True, allow_blank=True) + def validate(self, data): + from pulpcore.app.serializers.fields import pulp_labels_validator + + pulp_labels_validator({data["key"]: data["value"]}) + return super().validate(data) + class UnsetLabelSerializer(serializers.Serializer): """ Serializer for synchronously UNsetting a label. """ - key = serializers.SlugField(required=True) + key = serializers.CharField(required=True) value = serializers.CharField(read_only=True) def validate_key(self, value): diff --git a/pulpcore/app/serializers/fields.py b/pulpcore/app/serializers/fields.py index 762e9fe88e4..505fc82d2c7 100644 --- a/pulpcore/app/serializers/fields.py +++ b/pulpcore/app/serializers/fields.py @@ -11,6 +11,7 @@ from rest_framework.fields import empty from pulpcore.app import models +from pulpcore.constants import LABEL_KEY_REGEX from pulpcore.app.serializers import DetailIdentityField, IdentityField, RelatedField from pulpcore.app.util import reverse @@ -427,8 +428,13 @@ def pulp_labels_validator(value): value = json.loads(value) for k, v in value.items(): - if not re.match(r"^[\w ]+$", k): - raise serializers.ValidationError(_("Key '{}' contains non-alphanumerics.").format(k)) + if not re.match(LABEL_KEY_REGEX, k): + raise serializers.ValidationError( + _( + "Key '{}' contains invalid characters. Only alphanumerics, underscores," + " spaces, hyphens, and dots are allowed." + ).format(k) + ) if v is not None and re.search(r"[,()]", v): raise serializers.ValidationError( _("Key '{}' contains value with comma or parenthesis.").format(k) diff --git a/pulpcore/app/viewsets/custom_filters.py b/pulpcore/app/viewsets/custom_filters.py index dfe140e092f..16b7e9f504b 100644 --- a/pulpcore/app/viewsets/custom_filters.py +++ b/pulpcore/app/viewsets/custom_filters.py @@ -9,6 +9,7 @@ from gettext import gettext as _ from django.conf import settings +from pulpcore.constants import LABEL_KEY_CHARS from django.db.models import ObjectDoesNotExist from django_filters import BaseInFilter, CharFilter, Filter from drf_spectacular.types import OpenApiTypes @@ -313,7 +314,7 @@ def filter(self, qs, value): return qs for term in value.split(","): - match = re.match(r"(!?[\w\s]+)(=|!=|~)?(.*)?", term) + match = re.match(rf"(!?{LABEL_KEY_CHARS}+)(=|!=|~)?(.*)?", term) if not match: raise DRFValidationError(_("Invalid search term: '{}'.").format(term)) key, op, val = match.groups() diff --git a/pulpcore/constants.py b/pulpcore/constants.py index 614e131ea92..af980f98b93 100644 --- a/pulpcore/constants.py +++ b/pulpcore/constants.py @@ -129,6 +129,10 @@ ORPHAN_PROTECTION_TIME_LOWER_BOUND = 0 ORPHAN_PROTECTION_TIME_UPPER_BOUND = 4294967295 # (2^32)-1 +# Valid characters for pulp_labels keys: alphanumerics, underscores, spaces, hyphens, and dots. +LABEL_KEY_CHARS = r"[\w .\-]" +LABEL_KEY_REGEX = rf"^{LABEL_KEY_CHARS}+$" + # VULNERABILITY REPORT CONSTANTS # OSV API URL OSV_QUERY_URL = "https://api.osv.dev/v1/query" diff --git a/pulpcore/tests/unit/serializers/test_fields.py b/pulpcore/tests/unit/serializers/test_fields.py index 8e68def11fa..d003278a9f1 100644 --- a/pulpcore/tests/unit/serializers/test_fields.py +++ b/pulpcore/tests/unit/serializers/test_fields.py @@ -8,14 +8,19 @@ @pytest.mark.parametrize( "labels", [ - {"key": "value"}, - {"key": ""}, - {"key": None}, - {"key1": "value", "key2": None, "key3": ""}, + pytest.param({"key": "value"}, id="normal"), + pytest.param({"key": ""}, id="empty-value"), + pytest.param({"key": None}, id="none-value"), + pytest.param({"key1": "value", "key2": None, "key3": ""}, id="multiple-keys"), + pytest.param({"my-key": "value"}, id="dash-key"), + pytest.param({"my.key": "value"}, id="dotted-key"), + pytest.param({"my key": "value"}, id="spaced-key"), + pytest.param({"my-dotted.key": "value"}, id="dotted-dash-key"), + pytest.param({"spaced key-with.mixed_chars": "value"}, id="all-key"), ], ) def test_pulp_labels_validator_valid(labels): - """Valid label values including None should pass validation.""" + """Valid label keys and values should pass validation.""" result = pulp_labels_validator(labels) assert result == labels @@ -23,10 +28,12 @@ def test_pulp_labels_validator_valid(labels): @pytest.mark.parametrize( "labels", [ - {"key": "val,ue"}, - {"key": "val(ue"}, - {"key": "val)ue"}, - {"bad!key": "value"}, + pytest.param({"key": "val,ue"}, id="comma-value"), + pytest.param({"key": "val(ue"}, id="open-parenthesis-value"), + pytest.param({"key": "val)ue"}, id="close-parenthesis-value"), + pytest.param({"bad!key": "value"}, id="exclamation-key"), + pytest.param({"bad:key": "value"}, id="colon-key"), + pytest.param({"bad@key": "value"}, id="at-sign-key"), ], ) def test_pulp_labels_validator_invalid(labels):