Skip to content

Commit 9f3659f

Browse files
trivikrsrchase
authored andcommitted
Add filterSensitiveLog for Structure types (smithy-lang#170)
* Add basic toString function * Add REDACTED to member if it has SensitiveTrait * Use getMemberTrait to find SensitiveTrait * Move code to StructuredMemberWriter * Add SENSITIVE_STRING for optional members only if they're present * chore: remove isRequired check as all members are either undefined or optional * feat: call toString on Structures * chore: rename toString to filterSensitiveLog * feat: add filterSensitiveLog for Array * feat: added support for list of lists * Move map open and end outside if-else block * Fix issues with writeFilterSensitiveLogForArray Noticed while writing TypeScript playground https://tiny.amazon.com/f5o7auri * Fix java.lang.StackOverflowError * Simplify code by storing member shape in memberShape * Use openBlock instead of write * Pass arrayMember in writeFilterSensitiveLogForArray * Fix bug with Array<Array<SimpleShape>> * filterSensitiveLog for MapShape inside Structure * Move reducer function definition outside memberShape comparison * Remove redundant code by doing iteration only for some shapes * Recursively call isIterationRequired to remove redundant code * filterSensitiveLog for Map inside Collection * Explicitly return any from filterSensitiveLog method * filterSensitiveLog for Collection inside Map * filterSensitiveLog for Map inside Map * Fix bug in filterSensitiveLog for Map of Map * chore: rename writeFilterSensitiveLogFor<X> to write<X>FilterSensitiveLog * Simplified method isIterationRequired * Add period at the end of comments * Removed braces for variable writes * Used positional parameters in write() calls * Use destructuring in writeMapFilterSensitiveLog * Throw error in filterSensitiveLog where the path should never reach * Make writeX methods private * Add writeStructureFilterSensitiveLog * Edge case for collection with sensitive trait * Edge case for map with Sensitive trait * Refactor to simplify code * Rename config -> structuredMemberWriter * Move writeFilterSensitiveLog above private functions * Remove shape from writeFilterSensitiveLog * Rename memberShape to memberTarget * Create getSanitizedMemberName method * Fix for ./gradlew test to be successful * Add tests for filterSensitiveLog * Updated documentation to clarify getSanitizedMemberName * Use structureTarget.hasTrait in writeStructureFilterSensitiveLog * Added tests for callsFilterIn<X>WithSensitiveData * Modify tests for sensitive List/Map/Structure * Add tests for member pointing to sensitive structure/list/map
1 parent e6bcd6e commit 9f3659f

13 files changed

+548
-13
lines changed

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/StructureGenerator.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,18 +158,28 @@ private void renderErrorStructure() {
158158
writer.openBlock("export interface $L extends $L {", symbol.getName(), extendsFrom);
159159
writer.write("name: $S;", shape.getId().getName());
160160
writer.write("$$fault: $S;", errorTrait.getValue());
161-
StructuredMemberWriter config = new StructuredMemberWriter(
161+
StructuredMemberWriter structuredMemberWriter = new StructuredMemberWriter(
162162
model, symbolProvider, shape.getAllMembers().values());
163-
config.writeMembers(writer, shape);
163+
structuredMemberWriter.writeMembers(writer, shape);
164164
writer.closeBlock("}"); // interface
165165
writer.write("");
166166
renderStructureNamespace();
167167
}
168168

169169
private void renderStructureNamespace() {
170170
writer.addImport("isa", "__isa", "@aws-sdk/smithy-client");
171+
writer.addImport("SENSITIVE_STRING", "SENSITIVE_STRING", "@aws-sdk/smithy-client");
171172
Symbol symbol = symbolProvider.toSymbol(shape);
172173
writer.openBlock("export namespace $L {", "}", symbol.getName(), () -> {
174+
String objectParam = "obj";
175+
writer.openBlock("export const filterSensitiveLog = ($L: $L): any => ({", "})",
176+
objectParam, symbol.getName(),
177+
() -> {
178+
StructuredMemberWriter structuredMemberWriter = new StructuredMemberWriter(
179+
model, symbolProvider, shape.getAllMembers().values());
180+
structuredMemberWriter.writeFilterSensitiveLog(writer, objectParam);
181+
}
182+
);
173183
writer.write("export const isa = (o: any): o is $L => __isa(o, $S);",
174184
symbol.getName(), shape.getId().getName()
175185
);

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/StructuredMemberWriter.java

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@
1818
import java.util.Collection;
1919
import java.util.HashSet;
2020
import java.util.Set;
21+
import software.amazon.smithy.codegen.core.CodegenException;
2122
import software.amazon.smithy.codegen.core.SymbolProvider;
2223
import software.amazon.smithy.model.Model;
24+
import software.amazon.smithy.model.shapes.CollectionShape;
25+
import software.amazon.smithy.model.shapes.MapShape;
2326
import software.amazon.smithy.model.shapes.MemberShape;
2427
import software.amazon.smithy.model.shapes.Shape;
28+
import software.amazon.smithy.model.shapes.StructureShape;
2529
import software.amazon.smithy.model.traits.IdempotencyTokenTrait;
30+
import software.amazon.smithy.model.traits.SensitiveTrait;
2631

2732
/**
2833
* Generates objects, interfaces, enums, etc.
@@ -53,7 +58,7 @@ void writeMembers(TypeScriptWriter writer, Shape shape) {
5358

5459
position++;
5560
boolean wroteDocs = !noDocs && writer.writeMemberDocs(model, member);
56-
String memberName = TypeScriptUtils.sanitizePropertyName(symbolProvider.toMemberName(member));
61+
String memberName = getSanitizedMemberName(member);
5762
String optionalSuffix = shape.isUnionShape() || !isRequiredMember(member) ? "?" : "";
5863
String typeSuffix = isRequiredMember(member) ? " | undefined" : "";
5964
writer.write("${L}${L}${L}: ${T}${L};", memberPrefix, memberName, optionalSuffix,
@@ -65,6 +70,173 @@ void writeMembers(TypeScriptWriter writer, Shape shape) {
6570
}
6671
}
6772

73+
void writeFilterSensitiveLog(TypeScriptWriter writer, String objectParam) {
74+
writer.write("...$L,", objectParam);
75+
for (MemberShape member : members) {
76+
if (isMemberOverwriteRequired(member)) {
77+
Shape memberTarget = model.expectShape(member.getTarget());
78+
String memberName = getSanitizedMemberName(member);
79+
String memberParam = String.format("%s.%s", objectParam, memberName);
80+
writer.openBlock("...($1L.$2L && { $2L: ", "}),", objectParam, memberName, () -> {
81+
if (member.getMemberTrait(model, SensitiveTrait.class).isPresent()) {
82+
// member is Sensitive, hide the value.
83+
writer.write("SENSITIVE_STRING");
84+
} else if (memberTarget instanceof StructureShape) {
85+
writeStructureFilterSensitiveLog(writer, memberTarget, memberParam);
86+
} else if (memberTarget instanceof CollectionShape) {
87+
MemberShape collectionMember = ((CollectionShape) memberTarget).getMember();
88+
writeCollectionFilterSensitiveLog(writer, collectionMember, memberParam);
89+
} else if (memberTarget instanceof MapShape) {
90+
MemberShape mapMember = ((MapShape) memberTarget).getValue();
91+
writeMapFilterSensitiveLog(writer, mapMember, memberParam);
92+
}
93+
});
94+
}
95+
}
96+
}
97+
98+
/**
99+
* Recursively writes filterSensitiveLog for StructureShape.
100+
*/
101+
private void writeStructureFilterSensitiveLog(
102+
TypeScriptWriter writer,
103+
Shape structureTarget,
104+
String structureParam
105+
) {
106+
if (structureTarget.hasTrait(SensitiveTrait.class)) {
107+
// member is Sensitive, hide the value.
108+
writer.write("SENSITIVE_STRING");
109+
return;
110+
}
111+
// Call filterSensitiveLog on Structure.
112+
writer.write("$T.filterSensitiveLog($L)", symbolProvider.toSymbol(structureTarget), structureParam);
113+
}
114+
115+
/**
116+
* Recursively writes filterSensitiveLog for CollectionShape.
117+
*/
118+
private void writeCollectionFilterSensitiveLog(
119+
TypeScriptWriter writer,
120+
MemberShape collectionMember,
121+
String collectionParam
122+
) {
123+
if (collectionMember.getMemberTrait(model, SensitiveTrait.class).isPresent()) {
124+
// member is Sensitive, hide the value.
125+
writer.write("SENSITIVE_STRING");
126+
return;
127+
}
128+
129+
writer.openBlock("$L.map(", ")", collectionParam, () -> {
130+
String itemParam = "item";
131+
Shape collectionMemberTarget = model.expectShape(collectionMember.getTarget());
132+
writer.write("$L => ", itemParam);
133+
if (collectionMemberTarget instanceof StructureShape) {
134+
writeStructureFilterSensitiveLog(writer, collectionMemberTarget, itemParam);
135+
} else if (collectionMemberTarget instanceof CollectionShape) {
136+
MemberShape nestedCollectionMember = ((CollectionShape) collectionMemberTarget).getMember();
137+
writeCollectionFilterSensitiveLog(writer, nestedCollectionMember, itemParam);
138+
} else if (collectionMemberTarget instanceof MapShape) {
139+
MemberShape mapMember = ((MapShape) collectionMemberTarget).getValue();
140+
writeMapFilterSensitiveLog(writer, mapMember, itemParam);
141+
} else {
142+
// This path should not reach because of recursive isIterationRequired.
143+
throw new CodegenException(String.format(
144+
"CollectionFilterSensitiveLog attempted for %s while it was not required",
145+
collectionMemberTarget.getType()
146+
));
147+
// For quick-fix in case of high severity issue:
148+
// comment out the exception above and uncomment the line below.
149+
// writer.write("$1L => $1L", itemParam);
150+
}
151+
});
152+
}
153+
154+
/**
155+
* Recursively writes filterSensitiveLog for MapShape.
156+
*/
157+
private void writeMapFilterSensitiveLog(TypeScriptWriter writer, MemberShape mapMember, String mapParam) {
158+
if (mapMember.getMemberTrait(model, SensitiveTrait.class).isPresent()) {
159+
// member is Sensitive, hide the value.
160+
writer.write("SENSITIVE_STRING");
161+
return;
162+
}
163+
164+
String accParam = "acc"; // accumulator for the reducer
165+
String keyParam = "key"; // key of the Object.entries() key-value pair
166+
String valueParam = "value"; // value of the Object.entries() key-value pair
167+
168+
// Reducer is common to all shapes.
169+
writer.openBlock("Object.entries($L).reduce(($L: any, [$L, $L]: [string, $T]) => ({", "}), {})",
170+
mapParam, accParam, keyParam, valueParam, symbolProvider.toSymbol(mapMember), () -> {
171+
writer.write("...$L,", accParam);
172+
Shape mapMemberTarget = model.expectShape(mapMember.getTarget());
173+
writer.openBlock("[$L]: ", ",", keyParam, () -> {
174+
if (mapMemberTarget instanceof StructureShape) {
175+
writeStructureFilterSensitiveLog(writer, mapMemberTarget, valueParam);
176+
} else if (mapMemberTarget instanceof CollectionShape) {
177+
MemberShape collectionMember = ((CollectionShape) mapMemberTarget).getMember();
178+
writeCollectionFilterSensitiveLog(writer, collectionMember, valueParam);
179+
} else if (mapMemberTarget instanceof MapShape) {
180+
MemberShape nestedMapMember = ((MapShape) mapMemberTarget).getValue();
181+
writeMapFilterSensitiveLog(writer, nestedMapMember, valueParam);
182+
} else {
183+
// This path should not reach because of recursive isIterationRequired.
184+
throw new CodegenException(String.format(
185+
"MapFilterSensitiveLog attempted for %s while it was not required",
186+
mapMemberTarget.getType()
187+
));
188+
// For quick-fix in case of high severity issue:
189+
// comment out the exception above and uncomment the line below.
190+
// writer.write("$L", valueParam);
191+
}
192+
193+
});
194+
}
195+
);
196+
}
197+
198+
/**
199+
* Identifies if iteration is required on member.
200+
*
201+
* @param member a {@link MemberShape} to check for iteration required.
202+
* @return Returns true if the iteration is required on member.
203+
*/
204+
private boolean isIterationRequired(MemberShape member) {
205+
Shape memberTarget = model.expectShape(member.getTarget());
206+
if (memberTarget instanceof StructureShape) {
207+
return true;
208+
} else if (memberTarget instanceof CollectionShape) {
209+
MemberShape collectionMember = ((CollectionShape) memberTarget).getMember();
210+
return isIterationRequired(collectionMember);
211+
} else if (memberTarget instanceof MapShape) {
212+
MemberShape mapMember = ((MapShape) memberTarget).getValue();
213+
return isIterationRequired(mapMember);
214+
}
215+
return false;
216+
}
217+
218+
/**
219+
* Identifies if member needs to be overwritten in filterSensitiveLog.
220+
*
221+
* @param member a {@link MemberShape} to check if overwrite is required.
222+
* @return Returns true if the overwrite is required on member.
223+
*/
224+
private boolean isMemberOverwriteRequired(MemberShape member) {
225+
return (
226+
member.getMemberTrait(model, SensitiveTrait.class).isPresent() || isIterationRequired(member)
227+
);
228+
}
229+
230+
/**
231+
* Returns the member name to be used in generation.
232+
*
233+
* @param member a {@link MemberShape} to be sanitized.
234+
* @return Returns the member name to be used in generation.
235+
*/
236+
private String getSanitizedMemberName(MemberShape member) {
237+
return TypeScriptUtils.sanitizePropertyName(symbolProvider.toMemberName(member));
238+
}
239+
68240
/**
69241
* Identifies if a member should be required on the generated interface.
70242
*

0 commit comments

Comments
 (0)