Skip to content

Commit ba36d7a

Browse files
Improve secret detection with caching and setter support (#2824)
1 parent 2ee8907 commit ba36d7a

2 files changed

Lines changed: 113 additions & 48 deletions

File tree

plugin/src/main/java/io/jenkins/plugins/casc/Attribute.java

Lines changed: 56 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
import java.util.Collections;
2020
import java.util.HashSet;
2121
import java.util.List;
22+
import java.util.Map;
2223
import java.util.Objects;
2324
import java.util.Optional;
2425
import java.util.Set;
26+
import java.util.concurrent.ConcurrentHashMap;
2527
import java.util.logging.Level;
2628
import java.util.logging.Logger;
2729
import java.util.stream.Collectors;
@@ -48,9 +50,12 @@ public class Attribute<Owner, Type> {
4850
// Nop
4951
};
5052

51-
// TODO: Concurrent cache?
52-
// private static final HashMap<Class, Boolean> SECRET_ATTRIBUTE_CACHE =
53-
// new HashMap<>();
53+
private static final ClassValue<Map<String, Boolean>> SECRET_ATTRIBUTE_CACHE = new ClassValue<>() {
54+
@Override
55+
protected Map<String, Boolean> computeValue(@NonNull Class<?> type) {
56+
return new ConcurrentHashMap<>();
57+
}
58+
};
5459

5560
protected final String name;
5661
protected final Class type;
@@ -398,7 +403,6 @@ private static Field locatePrivateFieldInHierarchy(Class<?> clazz, @NonNull Stri
398403
return ExtraFieldUtils.getFieldNoForce(clazz, fieldName);
399404
}
400405

401-
// TODO: consider Boolean and third condition
402406
/**
403407
* This is a method which tries to guess whether an attribute is {@link Secret}.
404408
* @param targetClass Class of the target object. {@code null} if unknown
@@ -416,48 +420,50 @@ public static boolean calculateIfSecret(@CheckForNull Class<?> targetClass, @Non
416420
}
417421

418422
if (targetClass == null) {
419-
LOGGER.log(
420-
Level.FINER,
421-
"Attribute {0} is assumed to be non-secret, because there is no class instance in the call. "
422-
+ "This call is used only for fast-fetch caching, and the result may be adjusted later",
423-
new Object[] {fieldName});
424423
return false; // All methods below require a known target class
425424
}
426425

427-
// TODO: Cache decisions?
426+
return SECRET_ATTRIBUTE_CACHE.get(targetClass).computeIfAbsent(fieldName, key -> {
427+
Method m = locateGetter(targetClass, fieldName);
428+
if (m != null && m.getReturnType() == Secret.class) {
429+
LOGGER.log(
430+
Level.FINER,
431+
"Attribute {0}#{1} is secret, because there is a getter {2} which returns a Secret type",
432+
new Object[] {targetClass.getName(), fieldName, m});
433+
return true;
434+
}
428435

429-
Method m = locateGetter(targetClass, fieldName);
430-
if (m != null && m.getReturnType() == Secret.class) {
431-
LOGGER.log(
432-
Level.FINER,
433-
"Attribute {0}#{1} is secret, because there is a getter {2} which returns a Secret type",
434-
new Object[] {targetClass.getName(), fieldName, m});
435-
return true;
436-
}
436+
Field f = locatePublicField(targetClass, fieldName);
437+
if (f != null && f.getType() == Secret.class) {
438+
LOGGER.log(
439+
Level.FINER,
440+
"Attribute {0}#{1} is secret, because there is a public field {2} which has a Secret type",
441+
new Object[] {targetClass.getName(), fieldName, f});
442+
return true;
443+
}
437444

438-
Field f = locatePublicField(targetClass, fieldName);
439-
if (f != null && f.getType() == Secret.class) {
440-
LOGGER.log(
441-
Level.FINER,
442-
"Attribute {0}#{1} is secret, because there is a public field {2} which has a Secret type",
443-
new Object[] {targetClass.getName(), fieldName, f});
444-
return true;
445-
}
445+
f = locatePrivateFieldInHierarchy(targetClass, fieldName);
446+
if (f != null && f.getType() == Secret.class) {
447+
LOGGER.log(
448+
Level.FINER,
449+
"Attribute {0}#{1} is secret, because there is a private field {2} which has a Secret type",
450+
new Object[] {targetClass.getName(), fieldName, f});
451+
return true;
452+
}
446453

447-
f = locatePrivateFieldInHierarchy(targetClass, fieldName);
448-
if (f != null && f.getType() == Secret.class) {
449-
LOGGER.log(
450-
Level.FINER,
451-
"Attribute {0}#{1} is secret, because there is a private field {2} which has a Secret type",
452-
new Object[] {targetClass.getName(), fieldName, f});
453-
return true;
454-
}
454+
if (hasSecretSetter(targetClass, fieldName)) {
455+
LOGGER.log(
456+
Level.FINER,
457+
"Attribute {0}#{1} is secret, because there is a setter which takes a Secret type",
458+
new Object[] {targetClass.getName(), fieldName});
459+
return true;
460+
}
455461

456-
// TODO(oleg_nenashev): Consider setters? Gonna be more interesting since there might be many of them
457-
LOGGER.log(Level.FINER, "Attribute {0}#{1} is not a secret, because all checks have passed", new Object[] {
458-
targetClass.getName(), fieldName
462+
LOGGER.log(Level.FINER, "Attribute {0}#{1} is not a secret, because all checks have passed", new Object[] {
463+
targetClass.getName(), fieldName
464+
});
465+
return false;
459466
});
460-
return false;
461467
}
462468

463469
private Type _getValue(Owner target) throws ConfiguratorException {
@@ -545,4 +551,16 @@ public int hashCode() {
545551
public static <T, V> Setter<T, V> noop() {
546552
return NOOP;
547553
}
554+
555+
private static boolean hasSecretSetter(Class<?> clazz, @NonNull String fieldName) {
556+
final String setterName = "set" + StringUtils.capitalize(fieldName);
557+
for (Method method : clazz.getMethods()) {
558+
if (method.getName().equals(setterName) && method.getParameterCount() == 1) {
559+
if (method.getParameterTypes()[0] == Secret.class) {
560+
return true;
561+
}
562+
}
563+
}
564+
return false;
565+
}
548566
}

plugin/src/test/java/io/jenkins/plugins/casc/AttributeTest.java

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,26 @@ void checkNonSecretPatterns() {
7070
assertFieldIsNotSecret(NonSecretField.class, "passwordPath");
7171
}
7272

73+
@Test
74+
void checkSecretFromSetter() {
75+
assertFieldIsSecret(SecretFromSetter.class, "secretField");
76+
}
77+
78+
@Test
79+
void checkUnknownClassCondition() {
80+
assertFalse(
81+
Attribute.calculateIfSecret(null, "someField"),
82+
"calculateIfSecret should return false when targetClass is unknown");
83+
}
84+
7385
public static void assertFieldIsSecret(Class<?> clazz, String fieldName) {
7486
String displayName = clazz != null ? (clazz.getName() + "#" + fieldName) : fieldName;
75-
assertTrue(Attribute.calculateIfSecret(clazz, fieldName), "Field is not secret: " + displayName);
87+
assertTrue(Attribute.calculateIfSecret(clazz, fieldName), "Field should be a secret: " + displayName);
7688
}
7789

7890
public static void assertFieldIsNotSecret(Class<?> clazz, String fieldName) {
7991
String displayName = clazz != null ? (clazz.getName() + "#" + fieldName) : fieldName;
80-
assertFalse(
81-
Attribute.calculateIfSecret(clazz, fieldName),
82-
"Field is a secret while it should not be considered as one: " + displayName);
92+
assertFalse(Attribute.calculateIfSecret(clazz, fieldName), "Field should not be a secret: " + displayName);
8393
}
8494

8595
public static class WellDefinedField {
@@ -104,21 +114,22 @@ public WellDefinedField2(Secret secret) {
104114

105115
public static class SecretFromGetter {
106116

107-
Secret secretField;
117+
private final String hiddenData;
108118

109119
@DataBoundConstructor
110120
public SecretFromGetter(String secretField) {
111-
this.secretField = Secret.fromString(secretField);
121+
this.hiddenData = secretField;
112122
}
113123

114-
public Secret getSecret() {
115-
return secretField;
124+
@SuppressWarnings("unused")
125+
public Secret getSecretField() {
126+
return Secret.fromString(hiddenData);
116127
}
117128
}
118129

119130
public static class SecretFromGetter2 extends SecretFromGetter {
120-
public SecretFromGetter2(String secret) {
121-
super(secret);
131+
public SecretFromGetter2(String secretField) {
132+
super(secretField);
122133
}
123134
}
124135

@@ -183,4 +194,40 @@ public String getMySecretValue() {
183194
return mySecretValueField.getPlainText();
184195
}
185196
}
197+
198+
public static class SecretFromSetter {
199+
200+
private String secretField;
201+
202+
@DataBoundConstructor
203+
public SecretFromSetter() {}
204+
205+
@SuppressWarnings("unused")
206+
public String getSecretField() {
207+
return secretField;
208+
}
209+
210+
@SuppressWarnings("unused")
211+
public void setSecretField(Secret secretField) {
212+
this.secretField = secretField.getPlainText();
213+
}
214+
}
215+
216+
@Test
217+
void checkCalculationIsIdempotent() {
218+
boolean firstTrue = Attribute.calculateIfSecret(SecretFromSetter.class, "secretField");
219+
boolean secondTrue = Attribute.calculateIfSecret(SecretFromSetter.class, "secretField");
220+
assertTrue(firstTrue);
221+
assertTrue(secondTrue, "Subsequent calls should return the same TRUE result");
222+
223+
boolean firstFalse = Attribute.calculateIfSecret(NonSecretField.class, "passwordPath");
224+
boolean secondFalse = Attribute.calculateIfSecret(NonSecretField.class, "passwordPath");
225+
assertFalse(firstFalse);
226+
assertFalse(secondFalse, "Subsequent calls should return the same FALSE result");
227+
228+
boolean firstUnknown = Attribute.calculateIfSecret(null, "someField");
229+
boolean secondUnknown = Attribute.calculateIfSecret(null, "someField");
230+
assertFalse(firstUnknown);
231+
assertFalse(secondUnknown, "Subsequent calls should return the same fallback FALSE result");
232+
}
186233
}

0 commit comments

Comments
 (0)