Skip to content

Commit 8e78bdd

Browse files
fix(manifest-server): keep caller-supplied manifests gated via custom_components_trusted
Co-Authored-By: syed.khadeer@airbyte.io <cloud-support@airbyte.io>
1 parent cedec3e commit 8e78bdd

4 files changed

Lines changed: 46 additions & 5 deletions

File tree

airbyte_cdk/manifest_server/command_processor/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ def build_source(
7979
state=state,
8080
source_config=definition,
8181
config=config,
82+
# The manifest is supplied by the API caller rather than bundled in a connector image, so
83+
# any custom components it references are untrusted code.
84+
custom_components_trusted=False,
8285
normalize_manifest=should_normalize,
8386
migrate_manifest=should_migrate,
8487
emit_connector_builder_messages=True,

airbyte_cdk/sources/declarative/concurrent_declarative_source.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ def __init__(
150150
normalize_manifest: bool = False,
151151
limits: Optional[TestLimits] = None,
152152
config_path: Optional[str] = None,
153+
custom_components_trusted: bool = True,
153154
**kwargs: Any,
154155
) -> None:
155156
self.logger = logging.getLogger(f"airbyte.{self.name}")
@@ -174,6 +175,7 @@ def __init__(
174175
# the declarative models into runtime components. Concurrent sources will continue to checkpoint
175176
# incremental streams running in full refresh.
176177
component_factory = ModelToComponentFactory(
178+
custom_components_trusted=custom_components_trusted,
177179
emit_connector_builder_messages=emit_connector_builder_messages,
178180
message_repository=ConcurrentMessageRepository(queue, message_repository),
179181
configured_catalog=catalog,
@@ -197,6 +199,7 @@ def __init__(
197199
component_factory
198200
if component_factory
199201
else ModelToComponentFactory(
202+
custom_components_trusted=custom_components_trusted,
200203
emit_connector_builder_messages=emit_connector_builder_messages,
201204
max_concurrent_async_job_count=source_config.get("max_concurrent_async_job_count"),
202205
)

airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -705,8 +705,10 @@ def __init__(
705705
max_concurrent_async_job_count: Optional[int] = None,
706706
configured_catalog: Optional[ConfiguredAirbyteCatalog] = None,
707707
api_budget: Optional[APIBudget] = None,
708+
custom_components_trusted: bool = True,
708709
):
709710
self._init_mappings()
711+
self._custom_components_trusted = custom_components_trusted
710712
self._limit_pages_fetched_per_slice = limit_pages_fetched_per_slice
711713
self._limit_slices_fetched = limit_slices_fetched
712714
self._emit_connector_builder_messages = emit_connector_builder_messages
@@ -1815,11 +1817,15 @@ def create_custom_component(self, model: Any, config: Config, **kwargs: Any) ->
18151817
:return: The declarative component built from the Pydantic model to be used at runtime
18161818
"""
18171819
# Instantiating a custom component means importing and executing arbitrary code referenced
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():
1820+
# by `class_name`. Manifests supplied by a caller, whether through the config or directly to
1821+
# the manifest server, are untrusted input and could point `class_name` at any importable
1822+
# callable, so they honor the same `AIRBYTE_ENABLE_UNSAFE_CODE` gate as injected
1823+
# `components.py` code. Manifests bundled in a published connector image are trusted and may
1824+
# always use their bundled custom components.
1825+
manifest_is_untrusted = not self._custom_components_trusted or bool(
1826+
config.get(INJECTED_MANIFEST)
1827+
)
1828+
if manifest_is_untrusted and not custom_code_execution_permitted():
18231829
raise AirbyteCustomCodeNotPermittedError
18241830

18251831
custom_component_class = self._get_class_from_fully_qualified_class_name(model.class_name)
@@ -4141,6 +4147,7 @@ def create_parent_stream_config_with_substream_wrapper(
41414147
)
41424148

41434149
substream_factory = ModelToComponentFactory(
4150+
custom_components_trusted=self._custom_components_trusted,
41444151
connector_state_manager=connector_state_manager,
41454152
limit_pages_fetched_per_slice=self._limit_pages_fetched_per_slice,
41464153
limit_slices_fetched=self._limit_slices_fetched,

unit_tests/sources/declarative/parsers/test_model_to_component_factory.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2578,6 +2578,34 @@ def test_create_custom_component_permitted_for_bundled_manifest(monkeypatch):
25782578
assert isinstance(component, TestingSomeComponent)
25792579

25802580

2581+
def test_create_custom_component_requires_custom_code_enabled_when_untrusted(monkeypatch):
2582+
"""A caller-supplied manifest must honor the gate even when it never passes through the config.
2583+
2584+
The manifest server receives the manifest as a request payload rather than through the
2585+
config, so its untrusted provenance is signalled by `custom_components_trusted=False`.
2586+
"""
2587+
monkeypatch.delenv(ENV_VAR_ALLOW_CUSTOM_CODE, raising=False)
2588+
2589+
untrusted_factory = ModelToComponentFactory(custom_components_trusted=False)
2590+
2591+
def _fail_if_resolved(*args, **kwargs):
2592+
raise AssertionError(
2593+
"`class_name` must not be resolved or imported when custom code is disabled"
2594+
)
2595+
2596+
monkeypatch.setattr(
2597+
untrusted_factory, "_get_class_from_fully_qualified_class_name", _fail_if_resolved
2598+
)
2599+
2600+
manifest = {
2601+
"type": "CustomErrorHandler",
2602+
"class_name": "unit_tests.sources.declarative.parsers.testing_components.TestingSomeComponent",
2603+
}
2604+
2605+
with pytest.raises(AirbyteCustomCodeNotPermittedError):
2606+
untrusted_factory.create_component(CustomErrorHandlerModel, manifest, input_config)
2607+
2608+
25812609
def test_custom_components_do_not_contain_extra_fields():
25822610
custom_substream_partition_router_manifest = {
25832611
"type": "CustomPartitionRouter",

0 commit comments

Comments
 (0)