diff --git a/api/changelog.d/oci-regionless-api-legacy-region.changed.md b/api/changelog.d/oci-regionless-api-legacy-region.changed.md new file mode 100644 index 0000000000..087b027c88 --- /dev/null +++ b/api/changelog.d/oci-regionless-api-legacy-region.changed.md @@ -0,0 +1 @@ +OCI provider secrets no longer require `region`; legacy `region` input is accepted for backwards compatibility but ignored before storing or scanning diff --git a/api/src/backend/api/tests/test_serializers.py b/api/src/backend/api/tests/test_serializers.py index 0ee674d22c..8e77d63604 100644 --- a/api/src/backend/api/tests/test_serializers.py +++ b/api/src/backend/api/tests/test_serializers.py @@ -3,7 +3,12 @@ from api.v1.serializer_utils.integrations import ( JiraCredentialSerializer, S3ConfigSerializer, ) -from api.v1.serializers import ImageProviderSecret, KubernetesProviderSecret +from api.v1.serializer_utils.providers import ProviderSecretField +from api.v1.serializers import ( + ImageProviderSecret, + KubernetesProviderSecret, + OracleCloudProviderSecret, +) from rest_framework.exceptions import ValidationError @@ -190,6 +195,64 @@ class TestImageProviderSecret: assert "non_field_errors" in serializer.errors +class TestOracleCloudProviderSecret: + def valid_secret(self, **overrides): + secret = { + "user": "ocid1.user.oc1..aaaaaaaexample", + "fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99", + "key_content": "fake-base64-key-content", + "tenancy": "ocid1.tenancy.oc1..aaaaaaaexample", + } + secret.update(overrides) + return secret + + def test_accepts_regionless_secret(self): + serializer = OracleCloudProviderSecret(data=self.valid_secret()) + + assert serializer.is_valid(), serializer.errors + assert "region" not in serializer.validated_data + + def test_accepts_and_ignores_region_field(self): + secret = self.valid_secret(region="us-phoenix-1") + serializer = OracleCloudProviderSecret(data=secret) + + assert serializer.is_valid(), serializer.errors + + assert "region" not in serializer.validated_data + + @pytest.mark.parametrize( + "legacy_field, legacy_value", + [ + ("region", None), + ("region", ""), + ("region", {"name": "us-ashburn-1"}), + ], + ) + def test_accepts_and_ignores_any_legacy_region_value( + self, legacy_field, legacy_value + ): + serializer = OracleCloudProviderSecret( + data=self.valid_secret(**{legacy_field: legacy_value}) + ) + + assert serializer.is_valid(), serializer.errors + + assert legacy_field not in serializer.validated_data + + +class TestProviderSecretFieldSchema: + def test_oraclecloud_schema_includes_legacy_region_field(self): + schema = ProviderSecretField._spectacular_annotation["field"] + oraclecloud_schema = next( + credential_schema + for credential_schema in schema["oneOf"] + if credential_schema["title"] + == "Oracle Cloud Infrastructure (OCI) API Key Credentials" + ) + + assert oraclecloud_schema["properties"]["region"]["deprecated"] is True + + class TestKubernetesProviderSecret: def test_valid_static_kubeconfig_is_accepted(self): kubeconfig_content = """ diff --git a/api/src/backend/api/tests/test_utils.py b/api/src/backend/api/tests/test_utils.py index 9e96685477..4b5e8e1694 100644 --- a/api/src/backend/api/tests/test_utils.py +++ b/api/src/backend/api/tests/test_utils.py @@ -171,6 +171,53 @@ class TestInitializeProwlerProvider: key="value", mutelist_content={"key": "value"} ) + @patch("api.utils.return_prowler_provider") + def test_initialize_oraclecloud_provider_removes_region_string( + self, mock_return_prowler_provider + ): + provider = MagicMock() + provider.provider = Provider.ProviderChoices.ORACLECLOUD.value + provider.secret.secret = { + "user": "ocid1.user.oc1..fake", + "fingerprint": "00:11:22:33:44:55:66:77", + "key_content": "fake-base64-key-content", + "tenancy": "ocid1.tenancy.oc1..fake", + "region": "us-ashburn-1", + } + mock_return_prowler_provider.return_value = MagicMock() + + initialize_prowler_provider(provider) + + mock_return_prowler_provider.return_value.assert_called_once_with( + user="ocid1.user.oc1..fake", + fingerprint="00:11:22:33:44:55:66:77", + key_content="fake-base64-key-content", + tenancy="ocid1.tenancy.oc1..fake", + ) + + @patch("api.utils.return_prowler_provider") + def test_initialize_oraclecloud_provider_without_region_omits_scan_filter( + self, mock_return_prowler_provider + ): + provider = MagicMock() + provider.provider = Provider.ProviderChoices.ORACLECLOUD.value + provider.secret.secret = { + "user": "ocid1.user.oc1..fake", + "fingerprint": "00:11:22:33:44:55:66:77", + "key_content": "fake-base64-key-content", + "tenancy": "ocid1.tenancy.oc1..fake", + } + mock_return_prowler_provider.return_value = MagicMock() + + initialize_prowler_provider(provider) + + mock_return_prowler_provider.return_value.assert_called_once_with( + user="ocid1.user.oc1..fake", + fingerprint="00:11:22:33:44:55:66:77", + key_content="fake-base64-key-content", + tenancy="ocid1.tenancy.oc1..fake", + ) + class TestProwlerProviderConnectionTest: @patch("api.utils.return_prowler_provider") @@ -185,6 +232,37 @@ class TestProwlerProviderConnectionTest: key="value", provider_id="1234567890", raise_on_exception=False ) + @patch("api.utils.return_prowler_provider") + def test_oraclecloud_connection_test_uses_direct_credentials_without_region( + self, mock_return_prowler_provider + ): + provider = MagicMock() + provider.uid = "ocid1.tenancy.oc1..aaaaaaaexample" + provider.provider = Provider.ProviderChoices.ORACLECLOUD.value + provider.secret.secret = { + "user": "ocid1.user.oc1..aaaaaaaexample", + "fingerprint": "00:11:22:33:44:55:66:77", + "key_content": "fake-base64-key-content", + "tenancy": "ocid1.tenancy.oc1..aaaaaaaexample", + } + mock_return_prowler_provider.return_value = MagicMock() + + prowler_provider_connection_test(provider) + + mock_return_prowler_provider.return_value.test_connection.assert_called_once_with( + user="ocid1.user.oc1..aaaaaaaexample", + fingerprint="00:11:22:33:44:55:66:77", + key_content="fake-base64-key-content", + tenancy="ocid1.tenancy.oc1..aaaaaaaexample", + region=getattr( + OraclecloudProvider, + "_bootstrap_region", + OraclecloudProvider._home_region, + ), + provider_id="ocid1.tenancy.oc1..aaaaaaaexample", + raise_on_exception=False, + ) + @pytest.mark.django_db @patch("api.utils.return_prowler_provider") def test_prowler_provider_connection_test_without_secret( @@ -356,7 +434,7 @@ class TestGetProwlerProviderKwargs: expected_result = {**secret_dict, **expected_extra_kwargs} assert result == expected_result - def test_get_prowler_provider_kwargs_oraclecloud_converts_region_string_to_set( + def test_get_prowler_provider_kwargs_oraclecloud_removes_region( self, ): secret_dict = { @@ -377,8 +455,13 @@ class TestGetProwlerProviderKwargs: result = get_prowler_provider_kwargs(provider) - expected_result = {**secret_dict, "region": {"us-ashburn-1"}} - assert result == expected_result + assert result == { + "user": "ocid1.user.oc1..fake", + "fingerprint": "00:11:22:33:44:55:66:77", + "key_content": "-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----", + "tenancy": "ocid1.tenancy.oc1..fake", + "pass_phrase": "fake-passphrase", + } def test_get_prowler_provider_kwargs_with_mutelist(self): provider_uid = "provider_uid" diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index bfe8e57bc9..96d366e847 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -2917,6 +2917,48 @@ class TestProviderGroupViewSet: @pytest.mark.django_db class TestProviderSecretViewSet: + @staticmethod + def _oraclecloud_secret(**overrides): + secret = { + "user": "ocid1.user.oc1..aaaaaaaakldibrbov4ubh25aqdeiroklxjngwka7u6w7no3glmdq3n5sxtkq", + "fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99", + "key_content": "test-key-content", + "tenancy": "ocid1.tenancy.oc1..aaaaaaaa3dwoazoox4q7wrvriywpokp5grlhgnkwtyt6dmwyou7no6mdmzda", + } + secret.update(overrides) + return secret + + def _create_oraclecloud_secret( + self, + authenticated_client, + oraclecloud_provider, + secret, + name="OCI Secret", + ): + data = { + "data": { + "type": "provider-secrets", + "attributes": { + "name": name, + "secret_type": ProviderSecret.TypeChoices.STATIC, + "secret": secret, + }, + "relationships": { + "provider": { + "data": { + "type": "providers", + "id": str(oraclecloud_provider.id), + } + } + }, + } + } + return authenticated_client.post( + reverse("providersecret-list"), + data=json.dumps(data), + content_type="application/vnd.api+json", + ) + def test_provider_secrets_list(self, authenticated_client, provider_secret_fixture): response = authenticated_client.get(reverse("providersecret-list")) assert response.status_code == status.HTTP_200_OK @@ -3076,7 +3118,6 @@ current-context: test-context "fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99", "key_content": "-----BEGIN RSA PRIVATE KEY-----\ntest-key-content\n-----END RSA PRIVATE KEY-----", "tenancy": "ocid1.tenancy.oc1..aaaaaaaa3dwoazoox4q7wrvriywpokp5grlhgnkwtyt6dmwyou7no6mdmzda", - "region": "us-ashburn-1", }, ), # OCI with API key credentials (with key_file) @@ -3088,7 +3129,6 @@ current-context: test-context "fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99", "key_file": "/path/to/oci_api_key.pem", "tenancy": "ocid1.tenancy.oc1..aaaaaaaa3dwoazoox4q7wrvriywpokp5grlhgnkwtyt6dmwyou7no6mdmzda", - "region": "us-ashburn-1", }, ), # OCI with API key credentials (with passphrase) @@ -3100,7 +3140,6 @@ current-context: test-context "fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99", "key_content": "-----BEGIN RSA PRIVATE KEY-----\ntest-encrypted-key\n-----END RSA PRIVATE KEY-----", "tenancy": "ocid1.tenancy.oc1..aaaaaaaa3dwoazoox4q7wrvriywpokp5grlhgnkwtyt6dmwyou7no6mdmzda", - "region": "us-ashburn-1", "pass_phrase": "my-secure-passphrase", }, ), @@ -3258,6 +3297,103 @@ current-context: test-context == data["data"]["relationships"]["provider"]["data"]["id"] ) + def test_provider_secrets_create_oraclecloud_without_region_stores_no_region( + self, + authenticated_client, + oraclecloud_provider, + ): + response = self._create_oraclecloud_secret( + authenticated_client, + oraclecloud_provider, + self._oraclecloud_secret(), + ) + + assert response.status_code == status.HTTP_201_CREATED + provider_secret = ProviderSecret.objects.get() + assert "region" not in provider_secret.secret + + def test_provider_secrets_create_oraclecloud_accepts_and_ignores_region( + self, + authenticated_client, + oraclecloud_provider, + ): + response = self._create_oraclecloud_secret( + authenticated_client, + oraclecloud_provider, + self._oraclecloud_secret( + key_content=" test-key-content ", region=" us-ashburn-1 " + ), + ) + + assert response.status_code == status.HTTP_201_CREATED + provider_secret = ProviderSecret.objects.get() + assert provider_secret.secret["key_content"] == "test-key-content" + assert "region" not in provider_secret.secret + + def test_provider_secrets_update_oraclecloud_without_region_stores_no_region( + self, + authenticated_client, + oraclecloud_provider, + ): + create_response = self._create_oraclecloud_secret( + authenticated_client, + oraclecloud_provider, + self._oraclecloud_secret(), + ) + provider_secret = ProviderSecret.objects.get( + id=create_response.json()["data"]["id"] + ) + data = { + "data": { + "type": "provider-secrets", + "id": str(provider_secret.id), + "attributes": {"secret": self._oraclecloud_secret()}, + } + } + + response = authenticated_client.patch( + reverse("providersecret-detail", kwargs={"pk": provider_secret.id}), + data=json.dumps(data), + content_type="application/vnd.api+json", + ) + + assert response.status_code == status.HTTP_200_OK + provider_secret.refresh_from_db() + assert "region" not in provider_secret.secret + + def test_provider_secrets_update_oraclecloud_accepts_and_ignores_region( + self, + authenticated_client, + oraclecloud_provider, + ): + create_response = self._create_oraclecloud_secret( + authenticated_client, + oraclecloud_provider, + self._oraclecloud_secret(), + ) + provider_secret = ProviderSecret.objects.get( + id=create_response.json()["data"]["id"] + ) + data = { + "data": { + "type": "provider-secrets", + "id": str(provider_secret.id), + "attributes": { + "secret": self._oraclecloud_secret(region=" us-ashburn-1 ") + }, + } + } + + response = authenticated_client.patch( + reverse("providersecret-detail", kwargs={"pk": provider_secret.id}), + data=json.dumps(data), + content_type="application/vnd.api+json", + ) + + assert response.status_code == status.HTTP_200_OK + provider_secret.refresh_from_db() + assert "region" not in provider_secret.secret + @pytest.mark.parametrize( "attributes, error_code, error_pointer", ( diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index bb636b1bfa..8e73b96a39 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -252,12 +252,6 @@ def get_prowler_provider_kwargs( **prowler_provider_kwargs, "filter_accounts": [provider.uid], } - elif provider.provider == Provider.ProviderChoices.ORACLECLOUD.value: - if isinstance(prowler_provider_kwargs.get("region"), str): - prowler_provider_kwargs = { - **prowler_provider_kwargs, - "region": {prowler_provider_kwargs["region"]}, - } elif provider.provider == Provider.ProviderChoices.OPENSTACK.value: # clouds_yaml_content, clouds_yaml_cloud and provider_id are validated # in the provider itself, so it's not needed here. @@ -288,6 +282,11 @@ def get_prowler_provider_kwargs( **{k: v for k, v in prowler_provider_kwargs.items() if v}, } + elif provider.provider == Provider.ProviderChoices.ORACLECLOUD.value: + prowler_provider_kwargs = _normalize_oraclecloud_provider_kwargs( + prowler_provider_kwargs + ) + if mutelist_processor: mutelist_content = mutelist_processor.configuration.get("Mutelist", {}) # IaC and Image providers don't support mutelist (both use Trivy's built-in logic) @@ -300,6 +299,40 @@ def get_prowler_provider_kwargs( return prowler_provider_kwargs +def _normalize_oraclecloud_provider_kwargs(secret: dict) -> dict: + """Normalize external OCI secret fields into SDK provider kwargs.""" + prowler_provider_kwargs = secret.copy() + prowler_provider_kwargs.pop("region", None) + + return prowler_provider_kwargs + + +def _normalize_oraclecloud_connection_test_kwargs(secret: dict) -> dict: + """Normalize external OCI secret fields into test_connection kwargs.""" + from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider + + prowler_provider_kwargs = secret.copy() + prowler_provider_kwargs.pop("region", None) + + if ( + prowler_provider_kwargs.get("user") + and prowler_provider_kwargs.get("fingerprint") + and prowler_provider_kwargs.get("tenancy") + and ( + prowler_provider_kwargs.get("key_content") + or prowler_provider_kwargs.get("key_file") + ) + ): + # Connection validation needs one OCI endpoint, but scans remain unfiltered. + prowler_provider_kwargs["region"] = getattr( + OraclecloudProvider, + "_bootstrap_region", + OraclecloudProvider._home_region, + ) + + return prowler_provider_kwargs + + def initialize_prowler_provider( provider: Provider, mutelist_processor: Processor | None = None, @@ -402,6 +435,15 @@ def prowler_provider_connection_test(provider: Provider) -> Connection: if prowler_provider_kwargs.get("registry_token"): image_kwargs["registry_token"] = prowler_provider_kwargs["registry_token"] return prowler_provider.test_connection(**image_kwargs) + elif provider.provider == Provider.ProviderChoices.ORACLECLOUD.value: + oraclecloud_kwargs = _normalize_oraclecloud_connection_test_kwargs( + prowler_provider_kwargs + ) + return prowler_provider.test_connection( + **oraclecloud_kwargs, + provider_id=provider.uid, + raise_on_exception=False, + ) else: return prowler_provider.test_connection( **prowler_provider_kwargs, diff --git a/api/src/backend/api/v1/serializer_utils/providers.py b/api/src/backend/api/v1/serializer_utils/providers.py index 50c1c7376c..49b593049f 100644 --- a/api/src/backend/api/v1/serializer_utils/providers.py +++ b/api/src/backend/api/v1/serializer_utils/providers.py @@ -295,16 +295,21 @@ from rest_framework_json_api import serializers "type": "string", "description": "The OCID of the tenancy.", }, - "region": { - "type": "string", - "description": "The OCI region identifier (e.g., us-ashburn-1, us-phoenix-1).", - }, "pass_phrase": { "type": "string", "description": "The passphrase for the private key, if encrypted.", }, + "region": { + "type": "string", + "deprecated": True, + "description": "Legacy OCI region field accepted for backwards compatibility but ignored; OCI scans all regions.", + }, }, - "required": ["user", "fingerprint", "tenancy", "region"], + "required": ["user", "fingerprint", "tenancy"], + "anyOf": [ + {"required": ["key_file"]}, + {"required": ["key_content"]}, + ], }, { "type": "object", diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 0f08c8f4ba..750174e7a8 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1672,6 +1672,7 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer): validation_error.detail[f"secret/{key}"] = value del validation_error.detail[key] raise validation_error + return serializer.validated_data class AwsProviderSecret(serializers.Serializer): @@ -1813,14 +1814,32 @@ class IacProviderSecret(serializers.Serializer): resource_name = "provider-secrets" +class LegacyOCIRegionField(serializers.Field): + def to_internal_value(self, data): + return data + + def to_representation(self, value): + return value + + class OracleCloudProviderSecret(serializers.Serializer): user = serializers.CharField() fingerprint = serializers.CharField() key_file = serializers.CharField(required=False) key_content = serializers.CharField(required=False) tenancy = serializers.CharField() - region = serializers.CharField() pass_phrase = serializers.CharField(required=False) + region = LegacyOCIRegionField(required=False, allow_null=True) + + def validate(self, attrs): + attrs.pop("region", None) + + if "key_file" not in attrs and "key_content" not in attrs: + raise serializers.ValidationError( + {"key_file": "Either key_file or key_content must be provided."} + ) + + return attrs class Meta: resource_name = "provider-secrets" @@ -1965,7 +1984,11 @@ class ProviderSecretCreateSerializer(RLSSerializer, BaseWriteProviderSecretSeria secret = attrs.get("secret") validated_attrs = super().validate(attrs) - self.validate_secret_based_on_provider(provider.provider, secret_type, secret) + validated_secret = self.validate_secret_based_on_provider( + provider.provider, secret_type, secret + ) + if provider.provider == Provider.ProviderChoices.ORACLECLOUD.value: + validated_attrs["secret"] = validated_secret return validated_attrs @@ -1997,7 +2020,11 @@ class ProviderSecretUpdateSerializer(BaseWriteProviderSecretSerializer): secret = attrs.get("secret") validated_attrs = super().validate(attrs) - self.validate_secret_based_on_provider(provider.provider, secret_type, secret) + validated_secret = self.validate_secret_based_on_provider( + provider.provider, secret_type, secret + ) + if provider.provider == Provider.ProviderChoices.ORACLECLOUD.value: + validated_attrs["secret"] = validated_secret return validated_attrs diff --git a/ui/changelog.d/oci-regionless-provider-e2e.fixed.md b/ui/changelog.d/oci-regionless-provider-e2e.fixed.md new file mode 100644 index 0000000000..2e413a6fa8 --- /dev/null +++ b/ui/changelog.d/oci-regionless-provider-e2e.fixed.md @@ -0,0 +1 @@ +OCI provider E2E tests no longer require or submit a region when adding or updating credentials diff --git a/ui/components/providers/workflow/forms/via-credentials/oraclecloud-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/oraclecloud-credentials-form.tsx index 97508bdc42..ec989868ea 100644 --- a/ui/components/providers/workflow/forms/via-credentials/oraclecloud-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/via-credentials/oraclecloud-credentials-form.tsx @@ -48,16 +48,6 @@ export const OracleCloudCredentialsForm = ({ variant="bordered" isRequired /> - { @@ -1313,7 +1307,6 @@ export class ProvidersPage extends BasePage { await expect(this.ociUserIdInput).toBeVisible(); await expect(this.ociFingerprintInput).toBeVisible(); await expect(this.ociKeyContentInput).toBeVisible(); - await expect(this.ociRegionInput).toBeVisible(); } async verifyOCIUpdateCredentialsPageLoaded(): Promise { @@ -1324,7 +1317,6 @@ export class ProvidersPage extends BasePage { await expect(this.ociUserIdInput).toBeVisible(); await expect(this.ociFingerprintInput).toBeVisible(); await expect(this.ociKeyContentInput).toBeVisible(); - await expect(this.ociRegionInput).toBeVisible(); } async selectAlibabaCloudProvider(): Promise { diff --git a/ui/tests/providers/providers.md b/ui/tests/providers/providers.md index e8139b2b3d..a2914c2cde 100644 --- a/ui/tests/providers/providers.md +++ b/ui/tests/providers/providers.md @@ -667,7 +667,7 @@ **Preconditions:** - Admin user authentication required (admin.auth.setup setup) -- Environment variables configured: E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, E2E_OCI_REGION +- Environment variables configured: E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT - Remove any existing provider with the same Tenancy ID before starting the test - This test must be run serially and never in parallel with other tests, as it requires the Tenancy ID not to be already registered beforehand. @@ -678,7 +678,7 @@ 3. Select OCI provider type 4. Fill provider details (tenancy ID and alias) 5. Verify OCI credentials page is loaded -6. Fill OCI credentials (user ID, fingerprint, key content, region) +6. Fill OCI credentials (user ID, fingerprint, key content) 7. Confirm provider connection without launching a scan 8. Verify return to Providers page 9. Verify provider exists in Providers table @@ -696,7 +696,7 @@ - Connect account page displays OCI option - Provider details form accepts tenancy ID and alias - OCI credentials page loads -- Credentials form accepts all required fields (user ID, fingerprint, key content, region) +- Credentials form accepts all required fields (user ID, fingerprint, key content) - Launch step appears - Successful return to Providers page after closing the launch step - Provider exists in Providers table (verified by tenancy ID) @@ -726,7 +726,7 @@ **Preconditions:** - Admin user authentication required (admin.auth.setup setup) -- Environment variables configured: E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, E2E_OCI_REGION +- Environment variables configured: E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT - An OCI provider with the specified Tenancy ID must already exist (run PROVIDER-E2E-012 first) - This test must be run serially and never in parallel with other tests @@ -738,7 +738,7 @@ 4. Click "Update Credentials" option 5. Verify update credentials page is loaded 6. Verify OCI credentials form fields are visible (confirms providerUid is loaded) -7. Fill OCI credentials (user ID, fingerprint, key content, region) +7. Fill OCI credentials (user ID, fingerprint, key content) 8. Click Next to submit 9. Verify successful navigation to test connection page @@ -756,7 +756,7 @@ - OCI provider row is visible in providers table - Row actions dropdown opens and displays "Update Credentials" option - Update credentials page URL contains correct parameters -- OCI credentials form displays all fields (tenancy ID, user ID, fingerprint, key content, region) +- OCI credentials form displays all required fields (tenancy ID, user ID, fingerprint, key content) - Form submission succeeds (no silent failures due to missing provider UID) - Successful redirect to test connection page diff --git a/ui/tests/providers/providers.spec.ts b/ui/tests/providers/providers.spec.ts index 4dc8f010ed..b461808c65 100644 --- a/ui/tests/providers/providers.spec.ts +++ b/ui/tests/providers/providers.spec.ts @@ -1029,12 +1029,11 @@ test.describe("Add Provider", () => { const userId = process.env.E2E_OCI_USER_ID ?? ""; const fingerprint = process.env.E2E_OCI_FINGERPRINT ?? ""; const keyContent = process.env.E2E_OCI_KEY_CONTENT ?? ""; - const region = process.env.E2E_OCI_REGION ?? ""; // Setup before each test test.beforeEach(async ({ page }) => { test.skip( - !tenancyId || !userId || !fingerprint || !keyContent || !region, + !tenancyId || !userId || !fingerprint || !keyContent, "OCI E2E env vars are not set", ); providersPage = new ProvidersPage(page); @@ -1071,7 +1070,6 @@ test.describe("Add Provider", () => { userId: userId, fingerprint: fingerprint, keyContent: keyContent, - region: region, }; // Navigate to providers page @@ -1516,12 +1514,11 @@ test.describe("Update Provider Credentials", () => { const userId = process.env.E2E_OCI_USER_ID ?? ""; const fingerprint = process.env.E2E_OCI_FINGERPRINT ?? ""; const keyContent = process.env.E2E_OCI_KEY_CONTENT ?? ""; - const region = process.env.E2E_OCI_REGION ?? ""; // Setup before each test test.beforeEach(async ({ page }) => { test.skip( - !tenancyId || !userId || !fingerprint || !keyContent || !region, + !tenancyId || !userId || !fingerprint || !keyContent, "OCI E2E env vars are not set", ); providersPage = new ProvidersPage(page); @@ -1543,7 +1540,6 @@ test.describe("Update Provider Credentials", () => { userId: userId, fingerprint: fingerprint, keyContent: keyContent, - region: region, }; // Navigate to providers page diff --git a/ui/types/components.ts b/ui/types/components.ts index cb18986e24..ef9bc2541a 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -280,7 +280,6 @@ export type OCICredentials = { [ProviderCredentialFields.OCI_FINGERPRINT]: string; [ProviderCredentialFields.OCI_KEY_CONTENT]: string; [ProviderCredentialFields.OCI_TENANCY]: string; - [ProviderCredentialFields.OCI_REGION]: string; [ProviderCredentialFields.OCI_PASS_PHRASE]?: string; [ProviderCredentialFields.PROVIDER_ID]: string; }; diff --git a/ui/types/env.d.ts b/ui/types/env.d.ts index b07ed75286..515d7a6c03 100644 --- a/ui/types/env.d.ts +++ b/ui/types/env.d.ts @@ -134,7 +134,6 @@ declare global { E2E_OCI_USER_ID?: string; E2E_OCI_FINGERPRINT?: string; E2E_OCI_KEY_CONTENT?: string; - E2E_OCI_REGION?: string; // E2E Alibaba Cloud E2E_ALIBABACLOUD_ACCOUNT_ID?: string; diff --git a/ui/types/formSchemas.test.ts b/ui/types/formSchemas.test.ts index 58eb33128f..211ee35e55 100644 --- a/ui/types/formSchemas.test.ts +++ b/ui/types/formSchemas.test.ts @@ -220,3 +220,23 @@ users: expect(result.success).toBe(true); }); }); + +describe("addCredentialsFormSchema - oraclecloud", () => { + const BASE_OCI_VALUES = { + [ProviderCredentialFields.PROVIDER_ID]: "provider-oci-1", + [ProviderCredentialFields.PROVIDER_TYPE]: "oraclecloud", + [ProviderCredentialFields.OCI_USER]: "ocid1.user.oc1..example", + [ProviderCredentialFields.OCI_FINGERPRINT]: "aa:bb:cc:dd", + [ProviderCredentialFields.OCI_KEY_CONTENT]: + "-----BEGIN PRIVATE KEY-----\nMIIEvQ...\n-----END PRIVATE KEY-----", + [ProviderCredentialFields.OCI_TENANCY]: "ocid1.tenancy.oc1..example", + } as const; + + it("accepts OCI API key credentials without region", () => { + const schema = addCredentialsFormSchema("oraclecloud"); + + const result = schema.safeParse(BASE_OCI_VALUES); + + expect(result.success).toBe(true); + }); +}); diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index 79c8ddc574..0e13f67814 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -288,9 +288,6 @@ export const addCredentialsFormSchema = ( [ProviderCredentialFields.OCI_TENANCY]: z .string() .min(1, "Tenancy OCID is required"), - [ProviderCredentialFields.OCI_REGION]: z - .string() - .min(1, "Region is required"), [ProviderCredentialFields.OCI_PASS_PHRASE]: z .union([z.string(), z.literal("")]) .optional(),