Skip to content

Commit 7e96546

Browse files
fix(declarative): require AIRBYTE_ENABLE_UNSAFE_CODE to instantiate Custom* components from YAML (#1076)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent cc951be commit 7e96546

4 files changed

Lines changed: 123 additions & 0 deletions

File tree

airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,10 @@
477477
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
478478
ZipfileDecoder as ZipfileDecoderModel,
479479
)
480+
from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import (
481+
AirbyteCustomCodeNotPermittedError,
482+
custom_code_execution_permitted,
483+
)
480484
from airbyte_cdk.sources.declarative.partition_routers import (
481485
CartesianProductStreamSlicer,
482486
GroupingPartitionRouter,
@@ -1809,6 +1813,14 @@ def create_custom_component(self, model: Any, config: Config, **kwargs: Any) ->
18091813
:param config: The custom defined connector config
18101814
:return: The declarative component built from the Pydantic model to be used at runtime
18111815
"""
1816+
# 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():
1822+
raise AirbyteCustomCodeNotPermittedError
1823+
18121824
custom_component_class = self._get_class_from_fully_qualified_class_name(model.class_name)
18131825
component_fields = get_type_hints(custom_component_class)
18141826
model_args = model.dict()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#
2+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3+
#
4+
5+
import os
6+
7+
import pytest
8+
9+
from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import (
10+
ENV_VAR_ALLOW_CUSTOM_CODE,
11+
)
12+
13+
14+
@pytest.fixture(autouse=True)
15+
def _allow_custom_code():
16+
"""Enable custom-component code execution for the declarative unit tests.
17+
18+
`ModelToComponentFactory.create_custom_component` requires
19+
`AIRBYTE_ENABLE_UNSAFE_CODE` to be set before it will resolve and instantiate a
20+
component's `class_name`, matching the gate already applied to injected
21+
`components.py` code. These tests exercise custom components extensively, so enable
22+
the flag by default here. Tests that verify the gate itself opt out by deleting the
23+
env var (e.g. through their own `monkeypatch` fixture).
24+
"""
25+
previous = os.environ.get(ENV_VAR_ALLOW_CUSTOM_CODE)
26+
os.environ[ENV_VAR_ALLOW_CUSTOM_CODE] = "true"
27+
try:
28+
yield
29+
finally:
30+
if previous is None:
31+
os.environ.pop(ENV_VAR_ALLOW_CUSTOM_CODE, None)
32+
else:
33+
os.environ[ENV_VAR_ALLOW_CUSTOM_CODE] = previous
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#
2+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3+
#
4+
5+
import os
6+
7+
import pytest
8+
9+
from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import (
10+
ENV_VAR_ALLOW_CUSTOM_CODE,
11+
)
12+
13+
14+
@pytest.fixture(autouse=True)
15+
def _allow_custom_code():
16+
"""Enable custom-component code execution for the declarative unit tests.
17+
18+
`ModelToComponentFactory.create_custom_component` requires
19+
`AIRBYTE_ENABLE_UNSAFE_CODE` to be set before it will resolve and instantiate a
20+
component's `class_name`, matching the gate already applied to injected
21+
`components.py` code. These tests exercise custom components extensively, so enable
22+
the flag by default here. Tests that verify the gate itself opt out by deleting the
23+
env var (e.g. through their own `monkeypatch` fixture).
24+
"""
25+
previous = os.environ.get(ENV_VAR_ALLOW_CUSTOM_CODE)
26+
os.environ[ENV_VAR_ALLOW_CUSTOM_CODE] = "true"
27+
try:
28+
yield
29+
finally:
30+
if previous is None:
31+
os.environ.pop(ENV_VAR_ALLOW_CUSTOM_CODE, None)
32+
else:
33+
os.environ[ENV_VAR_ALLOW_CUSTOM_CODE] = previous

unit_tests/sources/declarative/parsers/test_model_to_component_factory.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@
107107
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
108108
SelectiveAuthenticator,
109109
)
110+
from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import (
111+
ENV_VAR_ALLOW_CUSTOM_CODE,
112+
AirbyteCustomCodeNotPermittedError,
113+
)
110114
from airbyte_cdk.sources.declarative.parsers.manifest_component_transformer import (
111115
ManifestComponentTransformer,
112116
)
@@ -2509,6 +2513,47 @@ def test_create_custom_components(manifest, field_name, expected_value, expected
25092513
assert getattr(custom_component, field_name) == expected_value
25102514

25112515

2516+
@pytest.mark.parametrize(
2517+
"class_name",
2518+
[
2519+
pytest.param(
2520+
"unit_tests.sources.declarative.parsers.testing_components.TestingSomeComponent",
2521+
id="bundled_custom_component_class",
2522+
),
2523+
pytest.param(
2524+
"os.getcwd",
2525+
id="arbitrary_importable_callable",
2526+
),
2527+
],
2528+
)
2529+
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.
2538+
"""
2539+
monkeypatch.delenv(ENV_VAR_ALLOW_CUSTOM_CODE, raising=False)
2540+
2541+
def _fail_if_resolved(*args, **kwargs):
2542+
raise AssertionError(
2543+
"`class_name` must not be resolved or imported when custom code is disabled"
2544+
)
2545+
2546+
monkeypatch.setattr(factory, "_get_class_from_fully_qualified_class_name", _fail_if_resolved)
2547+
2548+
manifest = {
2549+
"type": "CustomErrorHandler",
2550+
"class_name": class_name,
2551+
}
2552+
2553+
with pytest.raises(AirbyteCustomCodeNotPermittedError):
2554+
factory.create_component(CustomErrorHandlerModel, manifest, input_config)
2555+
2556+
25122557
def test_custom_components_do_not_contain_extra_fields():
25132558
custom_substream_partition_router_manifest = {
25142559
"type": "CustomPartitionRouter",

0 commit comments

Comments
 (0)