Skip to content

Commit cedec3e

Browse files
fix(declarative): allow bundled custom components without AIRBYTE_ENABLE_UNSAFE_CODE
Only gate Custom* components when the manifest itself is supplied through the config, which is the untrusted case. Manifests bundled in a published connector image are trusted, so their custom components must keep working in environments that do not set AIRBYTE_ENABLE_UNSAFE_CODE, such as Airbyte Cloud. Co-Authored-By: syed.khadeer@airbyte.io <cloud-support@airbyte.io>
1 parent 7e96546 commit cedec3e

4 files changed

Lines changed: 39 additions & 80 deletions

File tree

airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@
478478
ZipfileDecoder as ZipfileDecoderModel,
479479
)
480480
from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import (
481+
INJECTED_MANIFEST,
481482
AirbyteCustomCodeNotPermittedError,
482483
custom_code_execution_permitted,
483484
)
@@ -1814,11 +1815,11 @@ def create_custom_component(self, model: Any, config: Config, **kwargs: Any) ->
18141815
:return: The declarative component built from the Pydantic model to be used at runtime
18151816
"""
18161817
# Instantiating a custom component means importing and executing arbitrary code referenced
1817-
# by `class_name`. This is only permitted when custom code execution is explicitly enabled,
1818-
# mirroring the gate applied to injected `components.py` code. Without this check, a manifest
1819-
# could point `class_name` at any importable callable and have it invoked, bypassing the
1820-
# `AIRBYTE_ENABLE_UNSAFE_CODE` protection.
1821-
if not custom_code_execution_permitted():
1818+
# by `class_name`. When the manifest itself comes from the config, it is untrusted input and
1819+
# could point `class_name` at any importable callable, so it honors the same
1820+
# `AIRBYTE_ENABLE_UNSAFE_CODE` gate as injected `components.py` code. Manifests bundled in a
1821+
# published connector image are trusted and may always use their bundled custom components.
1822+
if config.get(INJECTED_MANIFEST) and not custom_code_execution_permitted():
18221823
raise AirbyteCustomCodeNotPermittedError
18231824

18241825
custom_component_class = self._get_class_from_fully_qualified_class_name(model.class_name)

unit_tests/legacy/sources/declarative/conftest.py

Lines changed: 0 additions & 33 deletions
This file was deleted.

unit_tests/sources/declarative/conftest.py

Lines changed: 0 additions & 33 deletions
This file was deleted.

unit_tests/sources/declarative/parsers/test_model_to_component_factory.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
)
110110
from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import (
111111
ENV_VAR_ALLOW_CUSTOM_CODE,
112+
INJECTED_MANIFEST,
112113
AirbyteCustomCodeNotPermittedError,
113114
)
114115
from airbyte_cdk.sources.declarative.parsers.manifest_component_transformer import (
@@ -2527,14 +2528,14 @@ def test_create_custom_components(manifest, field_name, expected_value, expected
25272528
],
25282529
)
25292530
def test_create_custom_component_requires_custom_code_enabled(class_name, monkeypatch):
2530-
"""A `Custom*` component must not be instantiated unless custom code execution is
2531-
explicitly enabled via `AIRBYTE_ENABLE_UNSAFE_CODE`.
2532-
2533-
Resolving and instantiating a component's `class_name` executes arbitrary
2534-
importable code, so it must honor the same gate as injected `components.py` code.
2535-
The gate must fire regardless of whether `class_name` points at a bundled custom
2536-
component or at an arbitrary importable callable, and it must fire before the
2537-
referenced module is imported.
2531+
"""A `Custom*` component declared by a config-injected manifest must not be instantiated
2532+
unless custom code execution is explicitly enabled via `AIRBYTE_ENABLE_UNSAFE_CODE`.
2533+
2534+
A manifest provided through the config is untrusted input, and resolving its
2535+
`class_name` executes arbitrary importable code, so it must honor the same gate as
2536+
injected `components.py` code. The gate must fire regardless of whether `class_name`
2537+
points at a bundled custom component or at an arbitrary importable callable, and it
2538+
must fire before the referenced module is imported.
25382539
"""
25392540
monkeypatch.delenv(ENV_VAR_ALLOW_CUSTOM_CODE, raising=False)
25402541

@@ -2551,7 +2552,30 @@ def _fail_if_resolved(*args, **kwargs):
25512552
}
25522553

25532554
with pytest.raises(AirbyteCustomCodeNotPermittedError):
2554-
factory.create_component(CustomErrorHandlerModel, manifest, input_config)
2555+
factory.create_component(
2556+
CustomErrorHandlerModel,
2557+
manifest,
2558+
{**input_config, INJECTED_MANIFEST: {"type": "DeclarativeSource"}},
2559+
)
2560+
2561+
2562+
def test_create_custom_component_permitted_for_bundled_manifest(monkeypatch):
2563+
"""A manifest bundled in a connector image may use its bundled custom components.
2564+
2565+
Published manifest-only connectors ship their own `manifest.yaml` and `components.py`
2566+
inside a trusted image, so their `Custom*` components must keep working in environments
2567+
that do not set `AIRBYTE_ENABLE_UNSAFE_CODE`, such as Airbyte Cloud.
2568+
"""
2569+
monkeypatch.delenv(ENV_VAR_ALLOW_CUSTOM_CODE, raising=False)
2570+
2571+
manifest = {
2572+
"type": "CustomErrorHandler",
2573+
"class_name": "unit_tests.sources.declarative.parsers.testing_components.TestingSomeComponent",
2574+
}
2575+
2576+
component = factory.create_component(CustomErrorHandlerModel, manifest, input_config)
2577+
2578+
assert isinstance(component, TestingSomeComponent)
25552579

25562580

25572581
def test_custom_components_do_not_contain_extra_fields():

0 commit comments

Comments
 (0)