-
Notifications
You must be signed in to change notification settings - Fork 723
/
SecretSourceResolver.java
253 lines (215 loc) · 9.29 KB
/
SecretSourceResolver.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
package io.jenkins.plugins.casc;
import static io.vavr.API.unchecked;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.text.StringSubstitutor;
import org.apache.commons.text.TextStringBuilder;
import org.apache.commons.text.lookup.StringLookup;
import org.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
/**
* Resolves secret variables and converts escaped internal variables.
*/
public class SecretSourceResolver {
private static final String enclosedBy = "${";
private static final String enclosedIn = "}";
private static final char escapedWith = '^';
private static final String escapeEnclosedBy = escapedWith + enclosedBy;
private static final Logger LOGGER = Logger.getLogger(SecretSourceResolver.class.getName());
private final StringSubstitutor nullSubstitutor;
private final StringSubstitutor substitutor;
public SecretSourceResolver(ConfigurationContext configurationContext) {
// TODO update to use Map.of in JDK11+
Map<String, org.apache.commons.text.lookup.StringLookup> map = new HashMap<>(8);
map.put("base64", Base64Lookup.INSTANCE);
map.put("fileBase64", FileBase64Lookup.INSTANCE);
map.put("readFileBase64", FileBase64Lookup.INSTANCE);
map.put("file", FileStringLookup.INSTANCE);
map.put("readFile", FileStringLookup.INSTANCE);
map.put("sysProp", SystemPropertyLookup.INSTANCE);
map.put("decodeBase64", DecodeBase64Lookup.INSTANCE);
map.put("json", JsonLookup.INSTANCE);
map = Collections.unmodifiableMap(map);
substitutor = new StringSubstitutor(new FixedInterpolatorStringLookup(
map, new ConfigurationContextStringLookup(configurationContext)))
.setEscapeChar(escapedWith)
.setVariablePrefix(enclosedBy)
.setVariableSuffix(enclosedIn)
.setEnableSubstitutionInVariables(true)
.setPreserveEscapes(true);
nullSubstitutor = new StringSubstitutor(UnresolvedLookup.INSTANCE)
.setEscapeChar(escapedWith)
.setVariablePrefix(enclosedBy)
.setVariableSuffix(enclosedIn);
}
/**
* Encodes String so that it can be safely represented in the YAML after export.
* @param toEncode String to encode
* @return Encoded string
* @since 1.25
*/
public String encode(@CheckForNull String toEncode) {
if (toEncode == null) {
return null;
}
return toEncode.replace(enclosedBy, escapeEnclosedBy);
}
/**
* Resolve string with potential secrets
*
* @param context Configuration context
* @param toInterpolate potential variables that need to revealed
* @return original string with any secrets that could be resolved if secrets could not be
* resolved they will be defaulted to default value defined by ':-', otherwise default to empty
* String. Secrets are defined as anything enclosed by '${}'
* @since 1.42
* @deprecated use {@link #resolve(String)}} instead.
*/
@Deprecated
@Restricted(DoNotUse.class)
public static String resolve(ConfigurationContext context, String toInterpolate) {
return context.getSecretSourceResolver().resolve(toInterpolate);
}
/**
* Resolve string with potential secrets
*
* @param toInterpolate potential variables that need to revealed
* @return original string with any secrets that could be resolved if secrets could not be
* resolved they will be defaulted to default value defined by ':-', otherwise default to empty
* String. Secrets are defined as anything enclosed by '${}'
*/
public String resolve(String toInterpolate) {
if (StringUtils.isBlank(toInterpolate) || !toInterpolate.contains(enclosedBy)) {
return toInterpolate;
}
final TextStringBuilder buf = new TextStringBuilder(toInterpolate);
substitutor.replaceIn(buf);
nullSubstitutor.replaceIn(buf);
return buf.toString();
}
static class UnresolvedLookup implements StringLookup {
static final UnresolvedLookup INSTANCE = new UnresolvedLookup();
private UnresolvedLookup() {}
@Override
public String lookup(String key) {
LOGGER.log(
Level.WARNING,
String.format(
"Configuration import: Found unresolved variable '%s'. Will default to empty string", key));
return "";
}
}
static class ConfigurationContextStringLookup implements StringLookup {
private final ConfigurationContext context;
private ConfigurationContextStringLookup(ConfigurationContext context) {
this.context = context;
}
@Override
public String lookup(String key) {
return context.getSecretSources().stream()
.map(source -> unchecked(() -> source.reveal(key)).apply())
.flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty))
.findFirst()
.orElse(null);
}
}
static class SystemPropertyLookup implements StringLookup {
static final SystemPropertyLookup INSTANCE = new SystemPropertyLookup();
@Override
public String lookup(@NonNull final String key) {
final String output = System.getProperty(key);
if (output == null) {
LOGGER.log(
Level.WARNING,
String.format(
"Configuration import: System Properties did not contain the specified key '%s'. Will default to empty string.",
key));
return "";
}
return output;
}
}
static class FileStringLookup implements StringLookup {
static final FileStringLookup INSTANCE = new FileStringLookup();
@Override
public String lookup(@NonNull final String key) {
try {
return new String(Files.readAllBytes(Paths.get(key)), StandardCharsets.UTF_8);
} catch (IOException | InvalidPathException e) {
LOGGER.log(
Level.WARNING,
String.format(
"Configuration import: Error looking up file '%s' with UTF-8 encoding. Will default to empty string",
key),
e);
return null;
}
}
}
static class Base64Lookup implements StringLookup {
static final Base64Lookup INSTANCE = new Base64Lookup();
@Override
public String lookup(@NonNull final String key) {
return Base64.getEncoder().encodeToString(key.getBytes(StandardCharsets.UTF_8));
}
}
static class DecodeBase64Lookup implements StringLookup {
static final DecodeBase64Lookup INSTANCE = new DecodeBase64Lookup();
@Override
public String lookup(@NonNull final String key) {
return new String(Base64.getDecoder().decode(key.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
}
}
static class FileBase64Lookup implements StringLookup {
static final FileBase64Lookup INSTANCE = new FileBase64Lookup();
@Override
public String lookup(@NonNull final String key) {
try {
byte[] fileContent = Files.readAllBytes(Paths.get(key));
return Base64.getEncoder().encodeToString(fileContent);
} catch (IOException | InvalidPathException e) {
LOGGER.log(
Level.WARNING,
String.format(
"Configuration import: Error looking up file '%s'. Will default to empty string", key),
e);
return null;
}
}
}
static class JsonLookup implements StringLookup {
static final JsonLookup INSTANCE = new JsonLookup();
private JsonLookup() {}
@Override
public String lookup(@NonNull final String key) {
final String[] components = key.split(":", 2);
final String jsonFieldName = components[0];
final String json = components[1];
final JSONObject root = new JSONObject(json);
final String output = root.optString(jsonFieldName, null);
if (output == null) {
LOGGER.log(
Level.WARNING,
String.format(
"Configuration import: JSON secret did not contain the specified key '%s'. Will default to empty string.",
jsonFieldName));
return "";
}
return output;
}
}
}