feat(api): expose only app-facing providers from the SDK

- Source provider-type choices and creation validation from get_app_providers()
- Hide CLI/SDK-only providers (llm, nhn, scaleway, stackit) from the API
This commit is contained in:
StylusFrost committed 2026-06-02 12:44:32 +02:00
1 parent 9537bb5a8c
commit 742d50b4be
5 files changed
+68 -37

No files matched your search

+2 -2
View File
@@ -51,6 +51,7 @@ from api.db_utils import (
one_week_from_now,
)
from api.exceptions import ModelValidationError
from api.provider_types import get_app_provider_types
from api.rls import (
BaseSecurityConstraint,
RowLevelSecurityConstraint,
@@ -58,7 +59,6 @@ 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())
@@ -502,7 +502,7 @@ class Provider(RowLevelSecurityProtectedModel):
def clean(self):
super().clean()
if self.provider not in SDKProvider.get_available_providers():
if self.provider not in get_app_provider_types():
raise ModelValidationError(
detail=f"{self.provider} is not a supported provider.",
code="invalid",
+15 -7
View File
@@ -4,12 +4,20 @@ from prowler.providers.common.provider import Provider as SDKProvider
@lru_cache(maxsize=1)
def get_provider_type_choices():
"""Provider-type choices from the SDK's available providers, so they cover
external providers and not just a static enum.
def get_app_provider_types():
"""App-facing provider types: SDK providers with ``sdk_only = False``.
Cached for the process lifetime; hot-installing a provider needs
coordinated cache invalidation (tracked separately) to show up here without
a restart. Shared by the filters and the provider serializer.
Single source of truth for which provider types the API exposes (filter
choices, serializer field, creation validation). Cached for the process
lifetime; hot-installing a provider needs cache invalidation (tracked
separately) to show up without a restart.
"""
return [(name, name) for name in SDKProvider.get_available_providers()]
return tuple(SDKProvider.get_app_providers())
@lru_cache(maxsize=1)
def get_provider_type_choices():
"""Provider-type choices from the app-facing providers, hiding providers
the API does not expose (``sdk_only = True``). Shared by the filters and
the provider serializer."""
return [(name, name) for name in get_app_provider_types()]
+20 -12
View File
@@ -3,21 +3,29 @@ from prowler.providers.common.provider import Provider as SDKProvider
class TestProviderTypeChoices:
"""Provider-type filter choices are driven by the SDK's available providers
so filtering covers external providers, not just a static enum."""
"""Provider-type filter choices are driven by the SDK's app-facing providers
(``sdk_only = False``) so filtering covers external providers that opt in,
while hiding providers the API does not expose (``sdk_only = True``)."""
def test_choices_track_sdk_available_providers(self):
available = set(SDKProvider.get_available_providers())
def test_choices_track_sdk_app_providers(self):
app_providers = set(SDKProvider.get_app_providers())
choices = get_provider_type_choices()
assert {value for value, _ in choices} == available
assert {value for value, _ in choices} == app_providers
def test_choices_include_provider_absent_from_legacy_enum(self):
from api.models import Provider
legacy = {value for value, _ in Provider.ProviderChoices.choices}
def test_choices_exclude_sdk_only_providers(self):
choice_values = {value for value, _ in get_provider_type_choices()}
# `llm` is exposed by the SDK but is not part of the legacy static enum.
assert "llm" in choice_values
assert "llm" not in legacy
# These built-ins are not implemented in the API, so they keep the
# default ``sdk_only = True`` and must not appear as filter choices,
# even though the SDK still lists them in get_available_providers().
available = set(SDKProvider.get_available_providers())
for sdk_only_provider in ("llm", "nhn", "scaleway", "stackit"):
assert sdk_only_provider in available
assert sdk_only_provider not in choice_values
def test_choices_include_app_exposed_builtins(self):
choice_values = {value for value, _ in get_provider_type_choices()}
assert "aws" in choice_values
assert "azure" in choice_values
+23 -10
View File
@@ -1,4 +1,5 @@
from datetime import datetime, timezone
from unittest.mock import patch
import pytest
from allauth.socialaccount.models import SocialApp
@@ -531,16 +532,18 @@ class TestTenantComplianceSummaryModel:
@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."""
"""Provider validity is driven by the SDK's app-facing providers
(``sdk_only = False``). sdk_only and unknown providers are rejected; an
app provider without a ``validate_<provider>_uid`` method only hits the
uid min-length floor."""
def test_accepts_provider_without_uid_validator(self, tenants_fixture):
def test_rejects_sdk_only_builtin(self, tenants_fixture):
# `llm` is a built-in but sdk_only, so the API must not accept it.
tenant = tenants_fixture[0]
provider = Provider.objects.create(
tenant_id=tenant.id, provider="llm", uid="my-llm-account"
)
assert provider.provider == "llm"
with pytest.raises(ModelValidationError):
Provider.objects.create(
tenant_id=tenant.id, provider="llm", uid="my-llm-account"
)
def test_rejects_provider_not_available_in_sdk(self, tenants_fixture):
tenant = tenants_fixture[0]
@@ -549,7 +552,17 @@ class TestProviderDynamicValidation:
tenant_id=tenant.id, provider="does-not-exist", uid="whatever"
)
def test_uid_floor_still_enforced_for_external_provider(self, tenants_fixture):
@patch("api.models.get_app_provider_types", return_value=("customext",))
def test_accepts_app_provider_without_uid_validator(self, _mock, tenants_fixture):
# An external provider that opts into the app and has no uid validator.
tenant = tenants_fixture[0]
provider = Provider.objects.create(
tenant_id=tenant.id, provider="customext", uid="my-account"
)
assert provider.provider == "customext"
@patch("api.models.get_app_provider_types", return_value=("customext",))
def test_uid_floor_still_enforced_for_app_provider(self, _mock, tenants_fixture):
tenant = tenants_fixture[0]
with pytest.raises(ValidationError):
Provider.objects.create(tenant_id=tenant.id, provider="llm", uid="ab")
Provider.objects.create(tenant_id=tenant.id, provider="customext", uid="ab")
@@ -81,17 +81,19 @@ class TestExternalProviderSecretValidation:
class TestProviderEnumSerializerField:
"""The provider field accepts whatever the SDK exposes (built-in or
external) and rejects anything else with `invalid_choice`."""
"""The provider field accepts app-facing providers (``sdk_only = False``)
and rejects sdk_only and unknown providers with `invalid_choice`."""
def test_accepts_sdk_available_provider(self):
def test_accepts_app_provider(self):
field = ProviderEnumSerializerField()
assert field.run_validation("aws") == "aws"
def test_accepts_external_provider_absent_from_static_enum(self):
def test_rejects_sdk_only_provider(self):
field = ProviderEnumSerializerField()
# `llm` is exposed by the SDK but is not part of the legacy static enum.
assert field.run_validation("llm") == "llm"
# `llm` is exposed by the SDK but is sdk_only, so the API rejects it.
with pytest.raises(ValidationError) as exc:
field.run_validation("llm")
assert exc.value.detail[0].code == "invalid_choice"
def test_rejects_unknown_provider(self):
field = ProviderEnumSerializerField()