Support JSON formatted secrets in SecretSourceResolver (#2018)
Co-authored-by: Joseph Petersen <josephp90@gmail.com>
This commit is contained in:
parent
befdf40784
commit
2d41195020
|
|
@ -50,18 +50,94 @@ For example, `Jenkins: "^${some_var}"` will produce the literal `Jenkins: "${som
|
|||
|
||||
=== Additional variable substitution
|
||||
|
||||
Currently we have `base64`, `readFile`, `decodeBase64` and `readFileBase64` all serving a different purpose.
|
||||
`decodeBase64` and `readFileBase64` are useful for credential plugin or any other plugin that needs binary base64 encoding of a file, in this specific case that would be useful for link:https://tools.ietf.org/html/rfc7292[a PKCS#12 certificate file].
|
||||
If you need to read a secret from a file, or perform additional en/decoding of the secret prior to use, the following helpers are available:
|
||||
|
||||
- `${base64:HELLO WORLD}` into `SEVMTE8gV09STEQ=`
|
||||
- `${readFile:/secret/file.txt}` into the content of a file.
|
||||
- `${base64:${readFile:/secret/file.txt}` into a base64 representation of the content in a file
|
||||
- `${base64:${readFile:${SECRET_FILE_PATH}}` nest it all together with regular secret expansion.
|
||||
- `${decodeBase64:${ENV_VAR}}` decodes environment variables encoded as base64.
|
||||
It might be used for passing multi-line variables like SSH keys through the environment.
|
||||
- `${readFileBase64:/secret/certificate.p12}` into a base64 representation of the binary file.
|
||||
- `${readFile:/secret/file.txt}` an alias for readFile
|
||||
- `${fileBase64:/secret/certificate.p12}` an alias for readFileBase64
|
||||
- `base64`
|
||||
- `readFile`
|
||||
- `decodeBase64`
|
||||
- `readFileBase64`
|
||||
- `json`
|
||||
|
||||
Helpers can be combined to do multiple transformations. Examples:
|
||||
|
||||
- `${base64:${readFile:/secret/file.txt}}`: read the file -> base64-encode it
|
||||
- `${base64:${readFile:${SECRET_FILE_PATH}}}`: expand the SECRET_FILE_PATH variable -> read the file -> base64-encode it
|
||||
|
||||
==== base64
|
||||
|
||||
Encodes the provided secret to base64.
|
||||
|
||||
Example:
|
||||
|
||||
`${base64:HELLO WORLD}` -> `SEVMTE8gV09STEQ=`
|
||||
|
||||
==== decodeBase64
|
||||
|
||||
Decodes the provided base64-encoded secret into a plain text string.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
${decodeBase64:${ENV_VAR}}
|
||||
```
|
||||
|
||||
`decodeBase64` (and its related `readFileBase64`) may be useful for the credential plugin or any other plugin that needs binary base64 encoding of a file. This might help when handling link:https://tools.ietf.org/html/rfc7292[a PKCS#12 certificate files] or passing an SSH key as an environment variable.
|
||||
|
||||
==== file
|
||||
|
||||
Alias for `readFile`.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
${file:/secret/file.txt}
|
||||
```
|
||||
|
||||
==== fileBase64
|
||||
|
||||
Alias for `readFileBase64`.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
${fileBase64:/secret/certificate.p12}
|
||||
```
|
||||
|
||||
==== readFile
|
||||
|
||||
Reads the secret from the specified file.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
${readFile:/secret/file.txt}
|
||||
```
|
||||
|
||||
==== readFileBase64
|
||||
|
||||
Read the specified binary file into a base64 representation.
|
||||
|
||||
```
|
||||
${readFileBase64:/secret/certificate.p12}
|
||||
```
|
||||
|
||||
==== json
|
||||
|
||||
Parse the string secret as JSON, then extract the value for the specified `<key>`.
|
||||
|
||||
(An empty string is returned if the JSON secret does not contain `<key>`).
|
||||
|
||||
```
|
||||
${json:<key>:${ENV_VAR}}
|
||||
```
|
||||
|
||||
This can help in situations where the backend has the option to store JSON secrets, e.g. AWS Secrets Manager.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
${json:username:${ENV_VAR}}
|
||||
```
|
||||
|
||||
=== Security and compatibility considerations
|
||||
|
||||
|
|
@ -91,7 +167,7 @@ It led to a security vulnerability which was addressed in JCasC `1.25` (SECURITY
|
|||
In JCasC there is a link:https://jenkins.io/doc/developer/extensions/configuration-as-code/#secretsource[SecretSource extension point] which allows resolving variables passed to JCasC.
|
||||
We can provide these initial secrets in the following ways:
|
||||
|
||||
- link:https://github.com/jenkinsci/aws-secrets-manager-credentials-provider-plugin#secretsource[AWS Secrets Manager]
|
||||
- link:https://github.com/jenkinsci/aws-secrets-manager-secret-source-plugin[AWS Secrets Manager]
|
||||
- link:https://github.com/jenkinsci/configuration-as-code-secret-ssm-plugin[AWS Systems Manager Parameter Store]
|
||||
- link:https://github.com/jenkinsci/azure-keyvault-plugin#secretsource[Azure KeyVault]
|
||||
- Docker Secrets
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ 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;
|
||||
|
||||
|
|
@ -46,6 +47,7 @@ public class SecretSourceResolver {
|
|||
map.put("file", FileStringLookup.INSTANCE);
|
||||
map.put("readFile", FileStringLookup.INSTANCE);
|
||||
map.put("decodeBase64", DecodeBase64Lookup.INSTANCE);
|
||||
map.put("json", JsonLookup.INSTANCE);
|
||||
map = Collections.unmodifiableMap(map);
|
||||
|
||||
substitutor = new StringSubstitutor(
|
||||
|
|
@ -199,4 +201,29 @@ public class SecretSourceResolver {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -348,6 +348,60 @@ public class SecretSourceResolverTest {
|
|||
assertThat(actualBytes, equalTo(expectedBytes));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolve_Json() {
|
||||
String input = "{ \"a\": 1, \"b\": 2 }";
|
||||
environment.set("FOO", input);
|
||||
String output = resolve("${json:a:${FOO}}");
|
||||
assertThat(output, equalTo("1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that prettified / multi-line JSON is supported
|
||||
*/
|
||||
@Test
|
||||
public void resolve_JsonWithNewlineBetweenTokens() {
|
||||
String input = "{ \n \"a\": 1, \n \"b\": 2 }";
|
||||
environment.set("FOO", input);
|
||||
String output = resolve("${json:a:${FOO}}");
|
||||
assertThat(output, equalTo("1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolve_JsonWithNewlineInValue() {
|
||||
String input = "{ \"a\": \"hello\\nworld\", \"b\": 2 }";
|
||||
environment.set("FOO", input);
|
||||
String output = resolve("${json:a:${FOO}}");
|
||||
assertThat(output, equalTo("hello\nworld"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolve_JsonWithSpaceInKey() {
|
||||
String input = "{ \"abc def\": 1, \"b\": 2 }";
|
||||
environment.set("FOO", input);
|
||||
String output = resolve("${json:abc def:${FOO}}");
|
||||
assertThat(output, equalTo("1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a mix of JSON and other lookups
|
||||
*/
|
||||
@Test
|
||||
public void resolve_JsonFromFile() throws Exception {
|
||||
String input = getPath("secret.json").toAbsolutePath().toString();
|
||||
String output = resolve("${json:Our secret:${file:" + input + "}}");
|
||||
assertThat(output, equalTo("Hello World"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolve_JsonMissingKey() {
|
||||
String input ="{ \"b\": 2 }";
|
||||
environment.set("FOO", input);
|
||||
String output = resolve("${json:a:${FOO}}");
|
||||
assertTrue(logContains("Configuration import: JSON secret did not contain the specified key 'a'. Will default to empty string."));
|
||||
assertThat(output, equalTo(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-1446")
|
||||
@WithoutJenkins
|
||||
|
|
|
|||
Loading…
Reference in New Issue