feat(api): validate provider against SDK-available providers

- Drive provider validity from the SDK instead of a static enum
- Tolerate providers without a uid validator at model clean()
- Accept any SDK-exposed provider in the provider serializer field
This commit is contained in:
StylusFrost
2026-05-31 22:28:00 +02:00
parent 64fdea2954
commit f14778438e
4 changed files with 69 additions and 3 deletions
+13 -1
View File
@@ -58,6 +58,7 @@ from api.rls import (
Tenant,
)
from prowler.lib.check.models import Severity
from prowler.providers.common.provider import Provider as SDKProvider
fernet = Fernet(settings.SECRETS_ENCRYPTION_KEY.encode())
@@ -501,13 +502,24 @@ class Provider(RowLevelSecurityProtectedModel):
def clean(self):
super().clean()
if self.provider not in SDKProvider.get_available_providers():
raise ModelValidationError(
detail=f"{self.provider} is not a supported provider.",
code="invalid",
pointer="/data/attributes/provider",
)
if self.provider == self.ProviderChoices.OKTA and self.uid:
# Mirror the SDK, which lowercases the org domain before connecting.
# Without this the API would reject Acme.okta.com even though the
# SDK would accept it, and stored uids could disagree with the
# authenticated org domain.
self.uid = self.uid.strip().lower()
getattr(self, f"validate_{self.provider}_uid")(self.uid)
# Providers the SDK exposes but the API has no specific uid rule for
# (e.g. external providers) fall back to the field-level min-length
# check only, instead of failing on a missing validator.
uid_validator = getattr(self, f"validate_{self.provider}_uid", None)
if uid_validator is not None:
uid_validator(self.uid)
def save(self, *args, **kwargs):
self.full_clean()
+28
View File
@@ -6,7 +6,9 @@ from django.core.exceptions import ValidationError
from django.db import IntegrityError
from api.db_router import MainRouter
from api.exceptions import ModelValidationError
from api.models import (
Provider,
ProviderComplianceScore,
Resource,
ResourceTag,
@@ -525,3 +527,29 @@ class TestTenantComplianceSummaryModel:
assert summary1.id != summary2.id
assert summary1.requirements_passed != summary2.requirements_passed
@pytest.mark.django_db
class TestProviderDynamicValidation:
"""Provider validity is driven by the SDK's available providers, not a
static enum. Providers the SDK exposes are accepted; for those without a
`validate_<provider>_uid` method only the uid min-length floor applies."""
def test_accepts_provider_without_uid_validator(self, tenants_fixture):
tenant = tenants_fixture[0]
provider = Provider.objects.create(
tenant_id=tenant.id, provider="llm", uid="my-llm-account"
)
assert provider.provider == "llm"
def test_rejects_provider_not_available_in_sdk(self, tenants_fixture):
tenant = tenants_fixture[0]
with pytest.raises(ModelValidationError):
Provider.objects.create(
tenant_id=tenant.id, provider="does-not-exist", uid="whatever"
)
def test_uid_floor_still_enforced_for_external_provider(self, tenants_fixture):
tenant = tenants_fixture[0]
with pytest.raises(ValidationError):
Provider.objects.create(tenant_id=tenant.id, provider="llm", uid="ab")
+21 -1
View File
@@ -2,7 +2,27 @@ import pytest
from rest_framework.exceptions import ValidationError
from api.v1.serializer_utils.integrations import S3ConfigSerializer
from api.v1.serializers import ImageProviderSecret
from api.v1.serializers import ImageProviderSecret, ProviderEnumSerializerField
class TestProviderEnumSerializerField:
"""The provider field accepts whatever the SDK exposes (built-in or
external) and rejects anything else with `invalid_choice`."""
def test_accepts_sdk_available_provider(self):
field = ProviderEnumSerializerField()
assert field.run_validation("aws") == "aws"
def test_accepts_external_provider_absent_from_static_enum(self):
field = ProviderEnumSerializerField()
# `llm` is exposed by the SDK but is not part of the legacy static enum.
assert field.run_validation("llm") == "llm"
def test_rejects_unknown_provider(self):
field = ProviderEnumSerializerField()
with pytest.raises(ValidationError) as exc:
field.run_validation("does-not-exist")
assert exc.value.detail[0].code == "invalid_choice"
class TestS3ConfigSerializer:
+7 -1
View File
@@ -73,6 +73,7 @@ from api.v1.serializer_utils.lighthouse import (
from api.v1.serializer_utils.processors import ProcessorConfigField
from api.v1.serializer_utils.providers import ProviderSecretField
from prowler.lib.mutelist.mutelist import Mutelist
from prowler.providers.common.provider import Provider as SDKProvider
# Base
@@ -854,7 +855,12 @@ class ProviderGroupMembershipSerializer(RLSSerializer, BaseWriteSerializer):
# Providers
class ProviderEnumSerializerField(serializers.ChoiceField):
def __init__(self, **kwargs):
kwargs["choices"] = Provider.ProviderChoices.choices
# The SDK is the source of truth for which providers exist, so the
# accepted values track the installed providers (built-in or external)
# instead of a static enum.
kwargs["choices"] = [
(name, name) for name in SDKProvider.get_available_providers()
]
super().__init__(**kwargs)