|
56 | 56 | ConcurrentDeclarativeSource, |
57 | 57 | TestLimits, |
58 | 58 | ) |
| 59 | +from airbyte_cdk.sources.declarative.parsers.custom_code_compiler import ( |
| 60 | + ENV_VAR_ALLOW_CUSTOM_CODE, |
| 61 | + AirbyteCustomCodeNotPermittedError, |
| 62 | +) |
59 | 63 | from airbyte_cdk.sources.declarative.retrievers.simple_retriever import SimpleRetriever |
60 | 64 | from airbyte_cdk.sources.declarative.stream_slicers import StreamSlicerTestReadDecorator |
61 | 65 | from airbyte_cdk.sources.streams.concurrent.default_stream import DefaultStream |
@@ -1031,6 +1035,117 @@ def test_create_source(): |
1031 | 1035 | assert source._constructor._disable_cache |
1032 | 1036 |
|
1033 | 1037 |
|
| 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 | + |
1034 | 1149 | def request_log_message(request: dict) -> AirbyteMessage: |
1035 | 1150 | return AirbyteMessage( |
1036 | 1151 | type=Type.LOG, |
|
0 commit comments