fix(api): scope the secret reassignment to Azure certificate_content only

This commit is contained in:
Lydia Vilchez committed 2026-09-01 21:49:16 +02:00
1 parent 721a80921b
commit e01ce68f10
2 files changed
+139 -8

No files matched your search

+106
View File
@@ -3591,6 +3591,112 @@ current-context: test-context
provider_secret.refresh_from_db()
assert provider_secret.secret["certificate_content"] == clean_b64
def test_provider_secrets_create_m365_preserves_whitespace_in_client_secret(
self,
authenticated_client,
m365_provider,
):
# DRF `CharField.trim_whitespace` (the default) would silently strip
# surrounding whitespace off opaque credentials if the outer write
# path blindly persisted the validated dict. Legitimate M365 client
# secrets can contain leading/trailing whitespace, so the raw
# submitted value must survive the round-trip unchanged.
whitespace_secret = " M365-Secret-With-Padding "
data = {
"data": {
"type": "provider-secrets",
"attributes": {
"name": "M365 Secret",
"secret_type": ProviderSecret.TypeChoices.STATIC,
"secret": {
"client_id": "87654321-4321-4321-4321-210987654321",
"tenant_id": "12345678-1234-1234-1234-123456789012",
"client_secret": whitespace_secret,
},
},
"relationships": {
"provider": {
"data": {"type": "providers", "id": str(m365_provider.id)}
}
},
}
}
response = authenticated_client.post(
reverse("providersecret-list"),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_201_CREATED, response.content
provider_secret = ProviderSecret.objects.get(id=response.json()["data"]["id"])
assert provider_secret.secret["client_secret"] == whitespace_secret
def test_provider_secrets_update_image_preserves_whitespace_in_registry_password(
self,
authenticated_client,
image_provider,
):
# Container-registry passwords can be opaque high-entropy strings
# generated by tooling that includes trailing whitespace as part of
# the credential. The outer write path must keep the submitted
# value verbatim so the registry still accepts it after a PATCH.
create_response = authenticated_client.post(
reverse("providersecret-list"),
data=json.dumps(
{
"data": {
"type": "provider-secrets",
"attributes": {
"name": "Registry Secret",
"secret_type": ProviderSecret.TypeChoices.STATIC,
"secret": {
"registry_username": "prowler",
"registry_password": "initial-password",
},
},
"relationships": {
"provider": {
"data": {
"type": "providers",
"id": str(image_provider.id),
}
}
},
}
}
),
content_type="application/vnd.api+json",
)
assert create_response.status_code == status.HTTP_201_CREATED
whitespace_password = " registry-password-with-padding "
provider_secret = ProviderSecret.objects.get(
id=create_response.json()["data"]["id"]
)
response = authenticated_client.patch(
reverse("providersecret-detail", kwargs={"pk": provider_secret.id}),
data=json.dumps(
{
"data": {
"type": "provider-secrets",
"id": str(provider_secret.id),
"attributes": {
"secret": {
"registry_username": "prowler",
"registry_password": whitespace_password,
}
},
}
}
),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_200_OK, response.content
provider_secret.refresh_from_db()
assert provider_secret.secret["registry_password"] == whitespace_password
@pytest.mark.parametrize(
"attributes, error_code, error_pointer",
(
+33 -8
View File
@@ -2163,11 +2163,24 @@ class ProviderSecretCreateSerializer(
validated_secret = self.validate_secret_based_on_provider(
provider.provider, secret_type, secret
)
# Persist the per-provider validator's normalized output (e.g.
# `AzureProviderSecret` strips whitespace inside `certificate_content`).
# Previously this reassignment only happened for OCI, so every
# per-provider validator's normalization was discarded on write.
validated_attrs["secret"] = validated_secret
# OCI persists the full validated dict on purpose (its serializer
# already performs the sanitization it wants). For Azure, only the
# `certificate_content` field must be replaced with the normalized
# (whitespace-stripped) value; the rest of the dict is left as the
# caller submitted it so opaque credentials elsewhere in the payload
# keep whatever whitespace they carried. Every other provider stays
# on the outer JSONField's raw dict — DRF's `CharField.trim_whitespace`
# would otherwise silently mutate opaque tokens/passwords.
if provider.provider == Provider.ProviderChoices.ORACLECLOUD.value:
validated_attrs["secret"] = validated_secret
elif (
provider.provider == Provider.ProviderChoices.AZURE.value
and validated_secret.get("certificate_content")
):
validated_attrs["secret"] = {
**secret,
"certificate_content": validated_secret["certificate_content"],
}
return validated_attrs
@@ -2202,9 +2215,21 @@ class ProviderSecretUpdateSerializer(BaseWriteProviderSecretSerializer):
validated_secret = self.validate_secret_based_on_provider(
provider.provider, secret_type, secret
)
# Persist the per-provider validator's normalized output — same
# rationale as `ProviderSecretCreateSerializer.validate`.
validated_attrs["secret"] = validated_secret
# Same targeted persistence as `ProviderSecretCreateSerializer.validate`:
# OCI keeps its full validated dict, Azure replaces only
# `certificate_content`, and every other provider stays on the raw
# submitted dict so opaque credentials do not get their whitespace
# silently trimmed.
if provider.provider == Provider.ProviderChoices.ORACLECLOUD.value:
validated_attrs["secret"] = validated_secret
elif (
provider.provider == Provider.ProviderChoices.AZURE.value
and validated_secret.get("certificate_content")
):
validated_attrs["secret"] = {
**secret,
"certificate_content": validated_secret["certificate_content"],
}
return validated_attrs