Add `decodeBase64` substitutor for reading base64-encoded variables (#1526)

* Add `readBase64` substitutor for reading base64-encoded variables

* Fix the SpotBugs issue with the encoding

* Swith to decodeBase64()
This commit is contained in:
Oleg Nenashev 2020-11-02 15:54:45 +01:00 committed by GitHub
parent 35d7025dc8
commit ebee63be35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 108 additions and 11 deletions

View File

@ -4,7 +4,8 @@ Requires `credentials` >= 2.2.0
All values with `"${SOME_SECRET}"` is resolved by our Secret Sources Resolver you can [read more about which sources are supported](../../docs/features/secrets.adoc#secret-sources)
Since JCasC version v1.42 we have added support for variable expansion for `base64`, `readFileBase64` and `readFile`
Since JCasC version v1.42 we have added support for variable expansion for `base64`, `readFileBase64` and `readFile`.
More variable expansion options have been added later.
[Read more about the syntax](../../docs/features/secrets.adoc#passing-secrets-through-variables)
You can also see an [example below](#example)

View File

@ -50,13 +50,15 @@ For example, `Jenkins: "^${some_var}"` will produce the literal `Jenkins: "${som
=== Additional variable substitution
Currently we have `base64`, `readFile` and `readFileBase64` all serving a different purpose.
`readFileBase64` is 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].
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].
- `${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

View File

@ -6,8 +6,10 @@ import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import io.jenkins.plugins.casc.misc.ConfiguredWithCode;
import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import jenkins.model.Jenkins;
import org.junit.Rule;
@ -15,9 +17,11 @@ import org.junit.Test;
import org.junit.rules.RuleChain;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.LoggerRule;
import org.jvnet.hudson.test.TestExtension;
import static io.jenkins.plugins.casc.misc.Util.assertNotInLog;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
@ -58,11 +62,57 @@ public class SSHCredentialsTest {
assertThat("There should be no private key in the exported YAML", exportedConfig, not(containsString(PRIVATE_KEY)));
}
@Test
@ConfiguredWithCode("SSHCredentialsTest_Multiline_Key.yml")
@Issue("https://github.com/jenkinsci/configuration-as-code-plugin/issues/1189")
public void shouldSupportMultilineCertificates() throws Exception {
BasicSSHUserPrivateKey certKey = getCredentials(BasicSSHUserPrivateKey.class);
assertThat("Private key roundtrip failed",
certKey.getPrivateKey().trim(), equalTo(MySSHKeySecretSource.PRIVATE_SSH_KEY.trim()));
}
@Test
@ConfiguredWithCode("SSHCredentialsTest_Singleline_Key.yml")
@Issue("https://github.com/jenkinsci/configuration-as-code-plugin/issues/1189")
public void shouldSupportSinglelineBase64Certificates() throws Exception {
BasicSSHUserPrivateKey certKey = getCredentials(BasicSSHUserPrivateKey.class);
assertThat("Private key roundtrip failed",
certKey.getPrivateKey().trim().replace("\r\n", "\n"), equalTo(MySSHKeySecretSource.PRIVATE_SSH_KEY));
}
private <T extends Credentials> T getCredentials(Class<T> clazz) {
List<T> creds = CredentialsProvider.lookupCredentials(
clazz, Jenkins.getInstanceOrNull(),
null, Collections.emptyList());
assertEquals(1, creds.size());
assertEquals("There should be only one credential", 1, creds.size());
return (T)creds.get(0);
}
@TestExtension
public static class MySSHKeySecretSource extends SecretSource {
private static final String PRIVATE_SSH_KEY =
"-----BEGIN OPENSSH PRIVATE KEY-----\n" +
"b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n" +
"QyNTUxOQAAACCYdvz4LdHg0G5KFS8PlauuOwVBms6Y70FaL4JY1YVahgAAAKCjJ1l+oydZ\n" +
"fgAAAAtzc2gtZWQyNTUxOQAAACCYdvz4LdHg0G5KFS8PlauuOwVBms6Y70FaL4JY1YVahg\n" +
"AAAEBWrtFZGX1yOg1/esgm34TPE5Zw8EXQ1OuxcgYGIaRRVph2/Pgt0eDQbkoVLw+Vq647\n" +
"BUGazpjvQVovgljVhVqGAAAAGW9uZW5hc2hldkBMQVBUT1AtMjVLNjVMT1MBAgME\n" +
"-----END OPENSSH PRIVATE KEY-----";
// encoded with "base64 -w 0"
private static final String PRIVATE_SSH_KEY_BASE64 = "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0NCmIzQmxibk56YUMxclpYa3RkakVBQUFBQUJHNXZibVVBQUFBRWJtOXVaUUFBQUFBQUFBQUJBQUFBTXdBQUFBdHpjMmd0WlcNClF5TlRVeE9RQUFBQ0NZZHZ6NExkSGcwRzVLRlM4UGxhdXVPd1ZCbXM2WTcwRmFMNEpZMVlWYWhnQUFBS0NqSjFsK295ZFoNCmZnQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQ1lkdno0TGRIZzBHNUtGUzhQbGF1dU93VkJtczZZNzBGYUw0SlkxWVZhaGcNCkFBQUVCV3J0RlpHWDF5T2cxL2VzZ20zNFRQRTVadzhFWFExT3V4Y2dZR0lhUlJWcGgyL1BndDBlRFFia29WTHcrVnE2NDcNCkJVR2F6cGp2UVZvdmdsalZoVnFHQUFBQUdXOXVaVzVoYzJobGRrQk1RVkJVVDFBdE1qVkxOalZNVDFNQkFnTUUNCi0tLS0tRU5EIE9QRU5TU0ggUFJJVkFURSBLRVktLS0tLQ0K";
@Override
public Optional<String> reveal(String secret) throws IOException {
if (secret.equals("MY_PRIVATE_KEY")) {
return Optional.of(PRIVATE_SSH_KEY);
}
if (secret.equals("SSH_AGENT_PRIVATE_KEY_BASE64")) {
return Optional.of(PRIVATE_SSH_KEY_BASE64);
}
return Optional.empty();
}
}
}

View File

@ -0,0 +1,16 @@
jenkins:
systemMessage: Jenkins with SSH Credentials for JCasC test
credentials:
system:
domainCredentials:
- credentials:
- basicSSHUserPrivateKey:
scope: SYSTEM
id: "userid2"
username: "username-of-userid2"
passphrase: "passphrase-of-userid2"
description: "the description of userid2"
privateKeySource:
directEntry:
privateKey: ${MY_PRIVATE_KEY}

View File

@ -0,0 +1,16 @@
jenkins:
systemMessage: Jenkins with SSH Credentials for JCasC test
credentials:
system:
domainCredentials:
- credentials:
- basicSSHUserPrivateKey:
scope: SYSTEM
id: "userid2"
username: "username-of-userid2"
passphrase: "passphrase-of-userid2"
description: "the description of userid2"
privateKeySource:
directEntry:
privateKey: ${decodeBase64:${SSH_AGENT_PRIVATE_KEY_BASE64}}

View File

@ -37,13 +37,15 @@ public class SecretSourceResolver {
public SecretSourceResolver(ConfigurationContext configurationContext) {
substitutor = new StringSubstitutor(
StringLookupFactory.INSTANCE.interpolatorStringLookup(ImmutableMap
.of("base64", Base64Lookup.INSTANCE,
"fileBase64", FileBase64Lookup.INSTANCE,
"readFileBase64", FileBase64Lookup.INSTANCE,
"file", FileStringLookup.INSTANCE,
"readFile", FileStringLookup.INSTANCE
),
StringLookupFactory.INSTANCE.interpolatorStringLookup(
ImmutableMap.<String, org.apache.commons.text.lookup.StringLookup> builder()
.put("base64", Base64Lookup.INSTANCE)
.put("fileBase64", FileBase64Lookup.INSTANCE)
.put("readFileBase64", FileBase64Lookup.INSTANCE)
.put("file", FileStringLookup.INSTANCE)
.put("readFile", FileStringLookup.INSTANCE)
.put("decodeBase64", DecodeBase64Lookup.INSTANCE)
.build(),
new ConfigurationContextStringLookup(configurationContext), false))
.setEscapeChar(escapedWith)
.setVariablePrefix(enclosedBy)
@ -165,6 +167,16 @@ public class SecretSourceResolver {
}
}
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();