Skip to content

Commit 9dc4189

Browse files
authored
Fix copySecrets for top level oneOfs (#20848)
1 parent 4eca4a4 commit 9dc4189

File tree

2 files changed

+106
-36
lines changed

2 files changed

+106
-36
lines changed

airbyte-config/config-persistence/src/main/java/io/airbyte/config/persistence/split_secrets/JsonSecretsProcessor.java

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -104,59 +104,67 @@ public JsonNode copySecrets(final JsonNode src, final JsonNode dst, final JsonNo
104104
if (!isValidJsonSchema(schema)) {
105105
return dst;
106106
}
107+
107108
Preconditions.checkArgument(dst.isObject());
108109
Preconditions.checkArgument(src.isObject());
109110

110111
final ObjectNode dstCopy = dst.deepCopy();
112+
return copySecretsRecursive(src, dstCopy, schema);
113+
}
114+
115+
return src;
116+
}
111117

118+
// This function is modifying dstCopy in place.
119+
private JsonNode copySecretsRecursive(final JsonNode src, JsonNode dstCopy, final JsonNode schema) {
120+
// todo (cgardens) this is not safe. should throw.
121+
if (!isValidJsonSchema(schema)) {
122+
return dstCopy;
123+
}
124+
125+
Preconditions.checkArgument(dstCopy.isObject());
126+
Preconditions.checkArgument(src.isObject());
127+
128+
final Optional<String> combinationKey = findJsonCombinationNode(schema);
129+
if (combinationKey.isPresent()) {
130+
final var arrayNode = (ArrayNode) schema.get(combinationKey.get());
131+
for (int i = 0; i < arrayNode.size(); i++) {
132+
final JsonNode childSchema = arrayNode.get(i);
133+
/*
134+
* when traversing a oneOf or anyOf if multiple schema in the oneOf or anyOf have the SAME key, but
135+
* a different type, then, without this test, we can try to apply the wrong schema to the object
136+
* resulting in errors because of type mismatches.
137+
*/
138+
if (VALIDATOR.test(childSchema, dstCopy)) {
139+
// Absorb field values if any of the combination option is declaring it as secrets
140+
copySecretsRecursive(src, dstCopy, childSchema);
141+
}
142+
}
143+
} else {
112144
final ObjectNode properties = (ObjectNode) schema.get(PROPERTIES_FIELD);
113145
for (final String key : Jsons.keys(properties)) {
114-
// If the source object doesn't have this key then we have nothing to copy, so we should skip to the
115-
// next key.
116-
if (!src.has(key)) {
146+
// If the source or destination doesn't have this key then we have nothing to copy, so we should
147+
// skip to the next key.
148+
if (!src.has(key) || !dstCopy.has(key)) {
117149
continue;
118150
}
119151

120152
final JsonNode fieldSchema = properties.get(key);
121153
// We only copy the original secret if the destination object isn't attempting to overwrite it
122154
// I.e. if the destination object's value is set to the mask, then we can copy the original secret
123-
if (JsonSecretsProcessor.isSecret(fieldSchema) && dst.has(key) && AirbyteSecretConstants.SECRETS_MASK.equals(dst.get(key).asText())) {
124-
dstCopy.set(key, src.get(key));
125-
} else if (dstCopy.has(key)) {
126-
// If the destination has this key, then we should consider copying it
127-
128-
// Check if this schema is a combination node; if it is, find a matching sub-schema and copy based
129-
// on that sub-schema
130-
final var combinationKey = findJsonCombinationNode(fieldSchema);
131-
if (combinationKey.isPresent()) {
132-
var combinationCopy = dstCopy.get(key);
133-
final var arrayNode = (ArrayNode) fieldSchema.get(combinationKey.get());
134-
for (int i = 0; i < arrayNode.size(); i++) {
135-
final JsonNode childSchema = arrayNode.get(i);
136-
/*
137-
* when traversing a oneOf or anyOf if multiple schema in the oneOf or anyOf have the SAME key, but
138-
* a different type, then, without this test, we can try to apply the wrong schema to the object
139-
* resulting in errors because of type mismatches.
140-
*/
141-
if (VALIDATOR.test(childSchema, combinationCopy)) {
142-
// Absorb field values if any of the combination option is declaring it as secrets
143-
combinationCopy = copySecrets(src.get(key), combinationCopy, childSchema);
144-
}
145-
}
146-
dstCopy.set(key, combinationCopy);
147-
} else {
148-
// Otherwise, this is just a plain old json node; recurse into it. If it's not actually an object,
149-
// the recursive call will exit immediately.
150-
final JsonNode copiedField = copySecrets(src.get(key), dstCopy.get(key), fieldSchema);
151-
dstCopy.set(key, copiedField);
152-
}
155+
if (JsonSecretsProcessor.isSecret(fieldSchema) && AirbyteSecretConstants.SECRETS_MASK.equals(dstCopy.get(key).asText())) {
156+
((ObjectNode) dstCopy).set(key, src.get(key));
157+
158+
} else {
159+
// Otherwise, this is just a plain old json node; recurse into it. If it's not actually an object,
160+
// the recursive call will exit immediately.
161+
final JsonNode copiedField = copySecretsRecursive(src.get(key), dstCopy.get(key), fieldSchema);
162+
((ObjectNode) dstCopy).set(key, copiedField);
153163
}
154164
}
155-
156-
return dstCopy;
157165
}
158166

159-
return src;
167+
return dstCopy;
160168
}
161169

162170
static boolean isSecret(final JsonNode obj) {

airbyte-config/config-persistence/src/test/java/io/airbyte/config/persistence/split_secrets/JsonSecretsProcessorTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,68 @@ void doesNotCopySecretsInNestedNonCombinationNodeWhenDestinationMissing() throws
495495
assertEquals(expected, copied);
496496
}
497497

498+
@Test
499+
void testCopySecretsWithTopLevelOneOf() {
500+
final JsonNode schema = Jsons.deserialize("""
501+
{
502+
"$schema": "http://json-schema.org/draft-07/schema#",
503+
"title": "E2E Test Destination Spec",
504+
"type": "object",
505+
"oneOf": [
506+
{
507+
"title": "Silent",
508+
"required": ["type"],
509+
"properties": {
510+
"a_secret": {
511+
"type": "string",
512+
"airbyte_secret": true
513+
}
514+
}
515+
},
516+
{
517+
"title": "Throttled",
518+
"required": ["type", "millis_per_record"],
519+
"properties": {
520+
"type": {
521+
"type": "string",
522+
"const": "THROTTLED",
523+
"default": "THROTTLED"
524+
},
525+
"millis_per_record": {
526+
"description": "Number of milli-second to pause in between records.",
527+
"type": "integer"
528+
}
529+
}
530+
}
531+
]
532+
}
533+
""");
534+
535+
final JsonNode source = Jsons.deserialize("""
536+
{
537+
"type": "THROTTLED",
538+
"a_secret": "woot"
539+
}
540+
""");
541+
542+
final JsonNode destination = Jsons.deserialize("""
543+
{
544+
"type": "THROTTLED",
545+
"a_secret": "**********"
546+
}
547+
""");
548+
549+
final JsonNode result = processor.copySecrets(source, destination, schema);
550+
final JsonNode expected = Jsons.deserialize("""
551+
{
552+
"type": "THROTTLED",
553+
"a_secret": "woot"
554+
}
555+
""");
556+
557+
assertEquals(expected, result);
558+
}
559+
498560
@Nested
499561
class NoOpTest {
500562

0 commit comments

Comments
 (0)