mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
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:
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user