From fb8a6f54b91050576bedcb7160e77efafcc66d5a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 01:14:51 +0000 Subject: [PATCH 1/3] fix(declarative): require AIRBYTE_ENABLE_UNSAFE_CODE to instantiate custom components Co-Authored-By: AJ Steers --- .../parsers/model_to_component_factory.py | 12 ++++++ unit_tests/conftest.py | 18 +++++++++ .../test_model_to_component_factory.py | 38 +++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py b/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py index c2551b4859..9de12ea879 100644 --- a/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +++ b/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py @@ -477,6 +477,10 @@ from airbyte_cdk.sources.declarative.models.declarative_component_schema import ( ZipfileDecoder as ZipfileDecoderModel, ) +from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import ( + AirbyteCustomCodeNotPermittedError, + custom_code_execution_permitted, +) from airbyte_cdk.sources.declarative.partition_routers import ( CartesianProductStreamSlicer, GroupingPartitionRouter, @@ -1809,6 +1813,14 @@ def create_custom_component(self, model: Any, config: Config, **kwargs: Any) -> :param config: The custom defined connector config :return: The declarative component built from the Pydantic model to be used at runtime """ + # Instantiating a custom component means importing and executing arbitrary code referenced + # by `class_name`. This is only permitted when custom code execution is explicitly enabled, + # mirroring the gate applied to injected `components.py` code. Without this check, a manifest + # could point `class_name` at any importable callable and have it invoked, bypassing the + # `AIRBYTE_ENABLE_UNSAFE_CODE` protection. + if not custom_code_execution_permitted(): + raise AirbyteCustomCodeNotPermittedError + custom_component_class = self._get_class_from_fully_qualified_class_name(model.class_name) component_fields = get_type_hints(custom_component_class) model_args = model.dict() diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py index 3a21552bc8..aa6b5f1327 100644 --- a/unit_tests/conftest.py +++ b/unit_tests/conftest.py @@ -7,6 +7,24 @@ import freezegun import pytest +from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import ( + ENV_VAR_ALLOW_CUSTOM_CODE, +) + + +@pytest.fixture(autouse=True) +def _allow_custom_code(monkeypatch): + """Enable custom-component code execution for unit tests by default. + + `ModelToComponentFactory.create_custom_component` requires + `AIRBYTE_ENABLE_UNSAFE_CODE` to be set before it will resolve and instantiate a + component's `class_name`, matching the gate already applied to injected + `components.py` code. The unit tests exercise custom components extensively, so + enable the flag by default here. Tests that verify the gate itself opt out by + deleting the env var through their own `monkeypatch` fixture. + """ + monkeypatch.setenv(ENV_VAR_ALLOW_CUSTOM_CODE, "true") + @pytest.fixture() def mock_sleep(monkeypatch): diff --git a/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py b/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py index 5149f46d2b..1f1ee9e671 100644 --- a/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py +++ b/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py @@ -107,6 +107,10 @@ from airbyte_cdk.sources.declarative.models.declarative_component_schema import ( SelectiveAuthenticator, ) +from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import ( + ENV_VAR_ALLOW_CUSTOM_CODE, + AirbyteCustomCodeNotPermittedError, +) from airbyte_cdk.sources.declarative.parsers.manifest_component_transformer import ( ManifestComponentTransformer, ) @@ -2509,6 +2513,40 @@ def test_create_custom_components(manifest, field_name, expected_value, expected assert getattr(custom_component, field_name) == expected_value +@pytest.mark.parametrize( + "class_name", + [ + pytest.param( + "unit_tests.sources.declarative.parsers.testing_components.TestingSomeComponent", + id="bundled_custom_component_class", + ), + pytest.param( + "os.getcwd", + id="arbitrary_importable_callable", + ), + ], +) +def test_create_custom_component_requires_custom_code_enabled(class_name, monkeypatch): + """A `Custom*` component must not be instantiated unless custom code execution is + explicitly enabled via `AIRBYTE_ENABLE_UNSAFE_CODE`. + + Resolving and instantiating a component's `class_name` executes arbitrary + importable code, so it must honor the same gate as injected `components.py` code. + The gate must fire regardless of whether `class_name` points at a bundled custom + component or at an arbitrary importable callable, and it must fire before the + referenced module is imported. + """ + monkeypatch.delenv(ENV_VAR_ALLOW_CUSTOM_CODE, raising=False) + + manifest = { + "type": "CustomErrorHandler", + "class_name": class_name, + } + + with pytest.raises(AirbyteCustomCodeNotPermittedError): + factory.create_component(CustomErrorHandlerModel, manifest, input_config) + + def test_custom_components_do_not_contain_extra_fields(): custom_substream_partition_router_manifest = { "type": "CustomPartitionRouter", From b01106fd6f1e5717ec701caa6a8c407c9cb5d29b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 01:18:45 +0000 Subject: [PATCH 2/3] test(declarative): assert custom code gate fires before class resolution Co-Authored-By: AJ Steers --- .../declarative/parsers/test_model_to_component_factory.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py b/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py index 1f1ee9e671..bdf061a400 100644 --- a/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py +++ b/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py @@ -2538,6 +2538,13 @@ def test_create_custom_component_requires_custom_code_enabled(class_name, monkey """ monkeypatch.delenv(ENV_VAR_ALLOW_CUSTOM_CODE, raising=False) + def _fail_if_resolved(*args, **kwargs): + raise AssertionError( + "`class_name` must not be resolved or imported when custom code is disabled" + ) + + monkeypatch.setattr(factory, "_get_class_from_fully_qualified_class_name", _fail_if_resolved) + manifest = { "type": "CustomErrorHandler", "class_name": class_name, From 2dee31c2047e5e5d492006abf3130377d123b5f7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 01:29:23 +0000 Subject: [PATCH 3/3] test(declarative): scope custom-code fixture to declarative test trees Co-Authored-By: AJ Steers --- unit_tests/conftest.py | 18 ---------- .../legacy/sources/declarative/conftest.py | 33 +++++++++++++++++++ unit_tests/sources/declarative/conftest.py | 33 +++++++++++++++++++ 3 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 unit_tests/legacy/sources/declarative/conftest.py create mode 100644 unit_tests/sources/declarative/conftest.py diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py index aa6b5f1327..3a21552bc8 100644 --- a/unit_tests/conftest.py +++ b/unit_tests/conftest.py @@ -7,24 +7,6 @@ import freezegun import pytest -from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import ( - ENV_VAR_ALLOW_CUSTOM_CODE, -) - - -@pytest.fixture(autouse=True) -def _allow_custom_code(monkeypatch): - """Enable custom-component code execution for unit tests by default. - - `ModelToComponentFactory.create_custom_component` requires - `AIRBYTE_ENABLE_UNSAFE_CODE` to be set before it will resolve and instantiate a - component's `class_name`, matching the gate already applied to injected - `components.py` code. The unit tests exercise custom components extensively, so - enable the flag by default here. Tests that verify the gate itself opt out by - deleting the env var through their own `monkeypatch` fixture. - """ - monkeypatch.setenv(ENV_VAR_ALLOW_CUSTOM_CODE, "true") - @pytest.fixture() def mock_sleep(monkeypatch): diff --git a/unit_tests/legacy/sources/declarative/conftest.py b/unit_tests/legacy/sources/declarative/conftest.py new file mode 100644 index 0000000000..c4eaab4b17 --- /dev/null +++ b/unit_tests/legacy/sources/declarative/conftest.py @@ -0,0 +1,33 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + +import os + +import pytest + +from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import ( + ENV_VAR_ALLOW_CUSTOM_CODE, +) + + +@pytest.fixture(autouse=True) +def _allow_custom_code(): + """Enable custom-component code execution for the declarative unit tests. + + `ModelToComponentFactory.create_custom_component` requires + `AIRBYTE_ENABLE_UNSAFE_CODE` to be set before it will resolve and instantiate a + component's `class_name`, matching the gate already applied to injected + `components.py` code. These tests exercise custom components extensively, so enable + the flag by default here. Tests that verify the gate itself opt out by deleting the + env var (e.g. through their own `monkeypatch` fixture). + """ + previous = os.environ.get(ENV_VAR_ALLOW_CUSTOM_CODE) + os.environ[ENV_VAR_ALLOW_CUSTOM_CODE] = "true" + try: + yield + finally: + if previous is None: + os.environ.pop(ENV_VAR_ALLOW_CUSTOM_CODE, None) + else: + os.environ[ENV_VAR_ALLOW_CUSTOM_CODE] = previous diff --git a/unit_tests/sources/declarative/conftest.py b/unit_tests/sources/declarative/conftest.py new file mode 100644 index 0000000000..c4eaab4b17 --- /dev/null +++ b/unit_tests/sources/declarative/conftest.py @@ -0,0 +1,33 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + +import os + +import pytest + +from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import ( + ENV_VAR_ALLOW_CUSTOM_CODE, +) + + +@pytest.fixture(autouse=True) +def _allow_custom_code(): + """Enable custom-component code execution for the declarative unit tests. + + `ModelToComponentFactory.create_custom_component` requires + `AIRBYTE_ENABLE_UNSAFE_CODE` to be set before it will resolve and instantiate a + component's `class_name`, matching the gate already applied to injected + `components.py` code. These tests exercise custom components extensively, so enable + the flag by default here. Tests that verify the gate itself opt out by deleting the + env var (e.g. through their own `monkeypatch` fixture). + """ + previous = os.environ.get(ENV_VAR_ALLOW_CUSTOM_CODE) + os.environ[ENV_VAR_ALLOW_CUSTOM_CODE] = "true" + try: + yield + finally: + if previous is None: + os.environ.pop(ENV_VAR_ALLOW_CUSTOM_CODE, None) + else: + os.environ[ENV_VAR_ALLOW_CUSTOM_CODE] = previous