diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/CredentialsFactory.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/CredentialsFactory.java index 7dd01198..2fdfeb46 100644 --- a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/CredentialsFactory.java +++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/CredentialsFactory.java @@ -40,14 +40,17 @@ public static Optional create(String arn, String name, Stri final String type = tags.getOrDefault(Tags.type, ""); final String username = tags.getOrDefault(Tags.username, ""); final String filename = tags.getOrDefault(Tags.filename, name); + final Boolean maskUsername = tags.getOrDefault(Tags.maskUsername, "").equalsIgnoreCase("false") + ? false + : true; switch (type) { case Type.string: return Optional.of(new AwsStringCredentials(name, description, new SecretSupplier(client, arn))); case Type.usernamePassword: - return Optional.of(new AwsUsernamePasswordCredentials(name, description, new SecretSupplier(client, arn), username)); + return Optional.of(new AwsUsernamePasswordCredentials(name, description, new SecretSupplier(client, arn), username, maskUsername)); case Type.sshUserPrivateKey: - return Optional.of(new AwsSshUserPrivateKey(name, description, new StringSupplier(client, arn), username)); + return Optional.of(new AwsSshUserPrivateKey(name, description, new StringSupplier(client, arn), username, maskUsername)); case Type.certificate: return Optional.of(new AwsCertificateCredentials(name, description, new SecretBytesSupplier(client, arn))); case Type.file: diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Tags.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Tags.java index 202e739a..7d9a9870 100644 --- a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Tags.java +++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Tags.java @@ -9,6 +9,7 @@ public abstract class Tags { public static final String filename = namespace + "filename"; public static final String type = namespace + "type"; public static final String username = namespace + "username"; + public static final String maskUsername = namespace + "maskUsername"; private Tags() { diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsSshUserPrivateKey.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsSshUserPrivateKey.java index bdee603f..14bebb35 100644 --- a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsSshUserPrivateKey.java +++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsSshUserPrivateKey.java @@ -19,11 +19,13 @@ public class AwsSshUserPrivateKey extends BaseStandardCredentials implements SSH private final Supplier privateKey; private final String username; + private final Boolean maskUsername; - public AwsSshUserPrivateKey(String id, String description, Supplier privateKey, String username) { + public AwsSshUserPrivateKey(String id, String description, Supplier privateKey, String username, Boolean maskUsername) { super(id, description); this.privateKey = privateKey; this.username = username; + this.maskUsername = maskUsername; } @NonNull @@ -38,6 +40,11 @@ public Secret getPassphrase() { return NO_PASSPHRASE; } + @Override + public boolean isUsernameSecret() { + return maskUsername; + } + @NonNull @Override public List getPrivateKeys() { diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsSshUserPrivateKeySnapshotTaker.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsSshUserPrivateKeySnapshotTaker.java index d09ade40..cf6d8f0e 100644 --- a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsSshUserPrivateKeySnapshotTaker.java +++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsSshUserPrivateKeySnapshotTaker.java @@ -14,7 +14,7 @@ public Class type() { @Override public AwsSshUserPrivateKey snapshot(AwsSshUserPrivateKey credential) { - return new AwsSshUserPrivateKey(credential.getId(), credential.getDescription(), new StringSnapshot(credential.getPrivateKey()), credential.getUsername()); + return new AwsSshUserPrivateKey(credential.getId(), credential.getDescription(), new StringSnapshot(credential.getPrivateKey()), credential.getUsername(), credential.isUsernameSecret()); } private static class StringSnapshot extends Snapshot { @@ -23,4 +23,3 @@ private static class StringSnapshot extends Snapshot { } } } - diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsUsernamePasswordCredentials.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsUsernamePasswordCredentials.java index 972a2352..93578d45 100644 --- a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsUsernamePasswordCredentials.java +++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsUsernamePasswordCredentials.java @@ -16,11 +16,13 @@ public class AwsUsernamePasswordCredentials extends BaseStandardCredentials impl private final Supplier password; private final String username; + private final Boolean maskUsername; - public AwsUsernamePasswordCredentials(String id, String description, Supplier password, String username) { + public AwsUsernamePasswordCredentials(String id, String description, Supplier password, String username, Boolean maskUsername) { super(id, description); this.password = password; this.username = username; + this.maskUsername = maskUsername; } @NonNull @@ -35,6 +37,11 @@ public String getUsername() { return username; } + @Override + public boolean isUsernameSecret() { + return maskUsername; + } + @Extension @SuppressWarnings("unused") public static class DescriptorImpl extends BaseStandardCredentialsDescriptor { diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsUsernamePasswordCredentialsSnapshotTaker.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsUsernamePasswordCredentialsSnapshotTaker.java index b8a966cd..d876299a 100644 --- a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsUsernamePasswordCredentialsSnapshotTaker.java +++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsUsernamePasswordCredentialsSnapshotTaker.java @@ -15,7 +15,7 @@ public Class type() { @Override public AwsUsernamePasswordCredentials snapshot(AwsUsernamePasswordCredentials credential) { - return new AwsUsernamePasswordCredentials(credential.getId(), credential.getDescription(), new SecretSnapshot(credential.getPassword()), credential.getUsername()); + return new AwsUsernamePasswordCredentials(credential.getId(), credential.getDescription(), new SecretSnapshot(credential.getPassword()), credential.getUsername(), credential.isUsernameSecret()); } private static class SecretSnapshot extends Snapshot { diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/SSHUserPrivateKeyIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/SSHUserPrivateKeyIT.java index 6b76e4a7..bcc7e3df 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/SSHUserPrivateKeyIT.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/SSHUserPrivateKeyIT.java @@ -35,7 +35,7 @@ public class SSHUserPrivateKeyIT implements CredentialsTests { @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportListView() { // Given - final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY, true); // When final var credentialList = jenkins.getCredentials().list(SSHUserPrivateKey.class); @@ -45,11 +45,25 @@ public void shouldSupportListView() { .containsOption(secret.getName(), secret.getName()); } + @Test + @ConfiguredWithCode(value = "/integration.yml") + public void shouldSupportListViewUnmasked() { + // Given + final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY, false); + + // When + final var credentialList = jenkins.getCredentials().list(SSHUserPrivateKey.class); + + // Then + assertThat(credentialList) + .containsOption(USERNAME, secret.getName()); + } + @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveId() { // Given - final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY, true); // When final var credential = lookup(SSHUserPrivateKey.class, secret.getName()); @@ -63,7 +77,7 @@ public void shouldHaveId() { @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveUsername() { // Given - final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY, true); // When final var credential = lookup(SSHUserPrivateKey.class, secret.getName()); @@ -77,7 +91,7 @@ public void shouldHaveUsername() { @ConfiguredWithCode(value = "/integration.yml") public void shouldHavePrivateKey() { // Given - final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY, true); // When final var credential = lookup(SSHUserPrivateKey.class, secret.getName()); @@ -91,7 +105,7 @@ public void shouldHavePrivateKey() { @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveEmptyPassphrase() { // Given - final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY, true); // When final var credential = lookup(SSHUserPrivateKey.class, secret.getName()); @@ -104,7 +118,7 @@ public void shouldHaveEmptyPassphrase() { @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveDescriptorIcon() { - final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY, true); final var ours = lookup(SSHUserPrivateKey.class, secret.getName()); @@ -118,7 +132,7 @@ public void shouldHaveDescriptorIcon() { @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportWithCredentialsBinding() { // Given - final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY, true); // When final var run = runPipeline("", @@ -134,11 +148,31 @@ public void shouldSupportWithCredentialsBinding() { .hasLogContaining("Credential: {username: ****, keyFile: ****}"); } + @Test + @ConfiguredWithCode(value = "/integration.yml") + public void shouldSupportWithCredentialsBindingUnmasked() { + // Given + final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY, false); + + // When + final var run = runPipeline("", + "node {", + " withCredentials([sshUserPrivateKey(credentialsId: '" + secret.getName() + "', keyFileVariable: 'KEYFILE', usernameVariable: 'USERNAME')]) {", + " echo \"Credential: {username: $USERNAME, keyFile: $KEYFILE}\"", + " }", + "}"); + + // Then + assertThat(run) + .hasResult(hudson.model.Result.SUCCESS) + .hasLogContaining("Credential: {username: joe, keyFile: ****}"); + } + @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportEnvironmentBinding() { // Given - final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY, true); // When final var run = runPipeline("", @@ -162,11 +196,39 @@ public void shouldSupportEnvironmentBinding() { .hasLogContaining("{variable: ****, username: ****}"); } + @Test + @ConfiguredWithCode(value = "/integration.yml") + public void shouldSupportEnvironmentBindingUnmasked() { + // Given + final var secret = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY, false); + + // When + final var run = runPipeline("", + "pipeline {", + " agent any", + " stages {", + " stage('Example') {", + " environment {", + " FOO = credentials('" + secret.getName() + "')", + " }", + " steps {", + " echo \"{variable: $FOO, username: $FOO_USR}\"", + " }", + " }", + " }", + "}"); + + // Then + assertThat(run) + .hasResult(hudson.model.Result.SUCCESS) + .hasLogContaining("{variable: ****, username: joe}"); + } + @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportSnapshots() { // Given - final CreateSecretResult foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final CreateSecretResult foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY, true); final SSHUserPrivateKey before = lookup(SSHUserPrivateKey.class, foo.getName()); // When @@ -180,10 +242,11 @@ public void shouldSupportSnapshots() { .hasId(before.getId()); } - private CreateSecretResult createSshUserPrivateKeySecret(String username, String privateKey) { + private CreateSecretResult createSshUserPrivateKeySecret(String username, String privateKey, Boolean maskUsername) { final var tags = List.of( AwsTags.type(Type.sshUserPrivateKey), - AwsTags.username(username)); + AwsTags.username(username), + AwsTags.maskUsername(String.valueOf(maskUsername))); final var request = new CreateSecretRequest() .withName(CredentialNames.random()) diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StandardUsernamePasswordCredentialsIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StandardUsernamePasswordCredentialsIT.java index 06ed6c15..7a305c4d 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StandardUsernamePasswordCredentialsIT.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StandardUsernamePasswordCredentialsIT.java @@ -34,7 +34,7 @@ public class StandardUsernamePasswordCredentialsIT implements CredentialsTests { @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportListView() { // Given - final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD); + final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD, true); // When final var credentialList = jenkins.getCredentials().list(StandardUsernamePasswordCredentials.class); @@ -44,11 +44,25 @@ public void shouldSupportListView() { .containsOption(secret.getName(), secret.getName()); } + @Test + @ConfiguredWithCode(value = "/integration.yml") + public void shouldSupportListViewUnmasked() { + // Given + final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD, false); + + // When + final var credentialList = jenkins.getCredentials().list(StandardUsernamePasswordCredentials.class); + + // Then + assertThat(credentialList) + .containsOption(USERNAME + "/******", secret.getName()); + } + @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHavePassword() { // Given - final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD); + final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD, true); // When final var credential = @@ -63,7 +77,7 @@ public void shouldHavePassword() { @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveUsername() { // Given - final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD); + final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD, true); // When final var credential = @@ -78,7 +92,7 @@ public void shouldHaveUsername() { @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveId() { // Given - final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD); + final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD, true); // When final var credential = @@ -93,7 +107,7 @@ public void shouldHaveId() { @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportWithCredentialsBinding() { // Given - final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD); + final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD, true); // When final var run = runPipeline("", @@ -107,11 +121,29 @@ public void shouldSupportWithCredentialsBinding() { .hasLogContaining("Credential: {username: ****, password: ****}"); } + @Test + @ConfiguredWithCode(value = "/integration.yml") + public void shouldSupportWithCredentialsBindingUnmasked() { + // Given + final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD, false); + + // When + final var run = runPipeline("", + "withCredentials([usernamePassword(credentialsId: '" + secret.getName() + "', usernameVariable: 'USR', passwordVariable: 'PSW')]) {", + " echo \"Credential: {username: $USR, password: $PSW}\"", + "}"); + + // Then + assertThat(run) + .hasResult(hudson.model.Result.SUCCESS) + .hasLogContaining("Credential: {username: " + USERNAME + ", password: ****}"); + } + @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportEnvironmentBinding() { // Given - final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD); + final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD, true); // When final var run = runPipeline("", @@ -135,11 +167,39 @@ public void shouldSupportEnvironmentBinding() { .hasLogContaining("{variable: ****, username: ****, password: ****}"); } + @Test + @ConfiguredWithCode(value = "/integration.yml") + public void shouldSupportEnvironmentBindingUnmasked() { + // Given + final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD, false); + + // When + final var run = runPipeline("", + "pipeline {", + " agent none", + " stages {", + " stage('Example') {", + " environment {", + " FOO = credentials('" + secret.getName() + "')", + " }", + " steps {", + " echo \"{variable: $FOO, username: $FOO_USR, password: $FOO_PSW}\"", + " }", + " }", + " }", + "}"); + + // Then + assertThat(run) + .hasResult(hudson.model.Result.SUCCESS) + .hasLogContaining("{variable: ****, username: " + USERNAME + ", password: ****}"); + } + @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportSnapshots() { // Given - final CreateSecretResult foo = createUsernamePasswordSecret(USERNAME, PASSWORD); + final CreateSecretResult foo = createUsernamePasswordSecret(USERNAME, PASSWORD, true); final StandardUsernamePasswordCredentials before = jenkins.getCredentials().lookup(StandardUsernamePasswordCredentials.class, foo.getName()); // When @@ -155,7 +215,7 @@ public void shouldSupportSnapshots() { @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveDescriptorIcon() { - final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD); + final var secret = createUsernamePasswordSecret(USERNAME, PASSWORD, true); final var ours = jenkins.getCredentials().lookup(StandardUsernamePasswordCredentials.class, secret.getName()); @@ -165,10 +225,11 @@ public void shouldHaveDescriptorIcon() { .hasSameDescriptorIconAs(theirs); } - private CreateSecretResult createUsernamePasswordSecret(String username, String password) { + private CreateSecretResult createUsernamePasswordSecret(String username, String password, Boolean maskUsername) { final var tags = List.of( AwsTags.type(Type.usernamePassword), - AwsTags.username(username)); + AwsTags.username(username), + AwsTags.maskUsername(String.valueOf(maskUsername))); final var request = new CreateSecretRequest() .withName(CredentialNames.random()) diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AwsTags.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AwsTags.java index 7f5e1db7..014b04e6 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AwsTags.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AwsTags.java @@ -19,6 +19,10 @@ public static Tag username(String username) { return AwsTags.tag(Tags.username, username); } + public static Tag maskUsername(String maskUsername) { + return AwsTags.tag(Tags.maskUsername, maskUsername); + } + public static Tag type(String type) { return tag(Tags.type, type); }