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:
parent
35d7025dc8
commit
ebee63be35
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -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}}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue