Skip to content

Commit d0e5851

Browse files
fix(declarative): mark connector-builder and SDM remote manifests untrusted
Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
1 parent 8e78bdd commit d0e5851

3 files changed

Lines changed: 122 additions & 0 deletions

File tree

airbyte_cdk/cli/source_declarative_manifest/_run.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ def create_declarative_source(
212212
state=state,
213213
source_config=cast(dict[str, Any], config["__injected_declarative_manifest"]),
214214
config_path=parsed_args.config if hasattr(parsed_args, "config") else None,
215+
# The manifest is supplied by the caller (via config or --manifest-path) rather than
216+
# bundled in a connector image, so any custom components it references are untrusted
217+
# code. Bundled manifest-only connectors take the local-manifest path instead.
218+
custom_components_trusted=False,
215219
)
216220
except Exception as error:
217221
print(

airbyte_cdk/connector_builder/connector_builder_handler.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ def create_source(
8484
migrate_manifest=should_migrate_manifest(config),
8585
normalize_manifest=should_normalize_manifest(config),
8686
limits=limits,
87+
# The manifest is supplied by the Connector Builder caller rather than bundled in a
88+
# connector image, so any custom components it references are untrusted code.
89+
custom_components_trusted=False,
8790
)
8891

8992

unit_tests/connector_builder/test_connector_builder_handler.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@
5656
ConcurrentDeclarativeSource,
5757
TestLimits,
5858
)
59+
from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import (
60+
ENV_VAR_ALLOW_CUSTOM_CODE,
61+
AirbyteCustomCodeNotPermittedError,
62+
)
5963
from airbyte_cdk.sources.declarative.retrievers.simple_retriever import SimpleRetriever
6064
from airbyte_cdk.sources.declarative.stream_slicers import StreamSlicerTestReadDecorator
6165
from airbyte_cdk.sources.streams.concurrent.default_stream import DefaultStream
@@ -1031,6 +1035,117 @@ def test_create_source():
10311035
assert source._constructor._disable_cache
10321036

10331037

1038+
def test_create_source_marks_manifest_untrusted():
1039+
"""A Connector Builder manifest is caller-supplied, so its factory must be untrusted.
1040+
1041+
This is the invariant that gates custom-component execution independently of the config
1042+
contents, so it cannot be defeated by a manifest that manipulates its own config.
1043+
"""
1044+
source = create_source(
1045+
config={"__injected_declarative_manifest": MANIFEST}, limits=None, catalog=None, state=None
1046+
)
1047+
1048+
assert source._constructor._custom_components_trusted is False
1049+
1050+
1051+
_GATE_BYPASS_STREAM = {
1052+
"type": "DeclarativeStream",
1053+
"name": "s",
1054+
"retriever": {
1055+
"type": "SimpleRetriever",
1056+
"requester": {
1057+
"type": "HttpRequester",
1058+
"url_base": "https://example.com",
1059+
"path": "/",
1060+
},
1061+
"record_selector": {
1062+
"type": "RecordSelector",
1063+
"extractor": {
1064+
"type": "CustomRecordExtractor",
1065+
"class_name": "not_a_real_module.NotAThing",
1066+
},
1067+
},
1068+
},
1069+
"schema_loader": {
1070+
"type": "InlineSchemaLoader",
1071+
"schema": {"type": "object", "properties": {}},
1072+
},
1073+
}
1074+
1075+
1076+
def test_spec_level_custom_component_is_gated(monkeypatch):
1077+
"""A `Custom*` component in `spec.config_normalization_rules` must honor the gate.
1078+
1079+
The spec component is built with an empty config, so the config-key provenance signal is
1080+
absent there; the untrusted-factory flag is what keeps the gate active. A non-existent
1081+
`class_name` proves the gate fires *before* the class is resolved (otherwise the failure
1082+
would be a class-resolution `ValueError`, not `AirbyteCustomCodeNotPermittedError`).
1083+
"""
1084+
monkeypatch.delenv(ENV_VAR_ALLOW_CUSTOM_CODE, raising=False)
1085+
manifest = {
1086+
"type": "DeclarativeSource",
1087+
"version": "6.0.0",
1088+
"check": {"type": "CheckStream", "stream_names": ["s"]},
1089+
"spec": {
1090+
"type": "Spec",
1091+
"connection_specification": {"type": "object", "properties": {}},
1092+
"config_normalization_rules": {
1093+
"type": "ConfigNormalizationRules",
1094+
"transformations": [
1095+
{
1096+
"type": "CustomConfigTransformation",
1097+
"class_name": "not_a_real_module.NotAThing",
1098+
}
1099+
],
1100+
},
1101+
},
1102+
"streams": [_GATE_BYPASS_STREAM],
1103+
}
1104+
1105+
with pytest.raises(AirbyteCustomCodeNotPermittedError):
1106+
create_source(
1107+
config={"__injected_declarative_manifest": manifest},
1108+
limits=None,
1109+
catalog=None,
1110+
state=None,
1111+
)
1112+
1113+
1114+
def test_config_strip_does_not_bypass_custom_code_gate(monkeypatch):
1115+
"""A manifest that strips its own provenance key must not escape the gate.
1116+
1117+
A plain `ConfigRemoveFields` removing `__injected_declarative_manifest` runs before streams
1118+
are built, defeating the config-key signal. The untrusted-factory flag does not live in the
1119+
config, so a stream-level `Custom*` component stays gated.
1120+
"""
1121+
monkeypatch.delenv(ENV_VAR_ALLOW_CUSTOM_CODE, raising=False)
1122+
manifest = {
1123+
"type": "DeclarativeSource",
1124+
"version": "6.0.0",
1125+
"check": {"type": "CheckStream", "stream_names": ["s"]},
1126+
"spec": {
1127+
"type": "Spec",
1128+
"connection_specification": {"type": "object", "properties": {}},
1129+
"config_normalization_rules": {
1130+
"type": "ConfigNormalizationRules",
1131+
"transformations": [
1132+
{
1133+
"type": "ConfigRemoveFields",
1134+
"field_pointers": [["__injected_declarative_manifest"]],
1135+
}
1136+
],
1137+
},
1138+
},
1139+
"streams": [_GATE_BYPASS_STREAM],
1140+
}
1141+
config = {"__injected_declarative_manifest": manifest}
1142+
1143+
source = create_source(config=config, limits=None, catalog=None, state=None)
1144+
1145+
with pytest.raises(AirbyteCustomCodeNotPermittedError):
1146+
source.streams(config)
1147+
1148+
10341149
def request_log_message(request: dict) -> AirbyteMessage:
10351150
return AirbyteMessage(
10361151
type=Type.LOG,

0 commit comments

Comments
 (0)