mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-16 16:34:24 +00:00
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:
1 parent
9537bb5a8c
commit
742d50b4be
5 files changed
+68
-37
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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()]
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user