diff --git a/.gitignore b/.gitignore index ec2f3a5..25626f7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ buildNumber.properties !/.mvn/wrapper/maven-wrapper.jar work/ *.dylib +deployment.out diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProvider.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProvider.java index f6c9321..1bd91cb 100644 --- a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProvider.java +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProvider.java @@ -4,32 +4,37 @@ import com.azure.security.keyvault.secrets.models.SecretProperties; import com.cloudbees.plugins.credentials.Credentials; import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.CredentialsStore; import com.cloudbees.plugins.credentials.common.IdCredentials; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import com.google.common.annotations.VisibleForTesting; import com.microsoft.jenkins.keyvault.SecretClientCache; -import com.microsoft.jenkins.keyvault.SecretStringCredentials; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; import hudson.model.ItemGroup; import hudson.model.ModelObject; import hudson.security.ACL; +import hudson.util.Secret; +import java.net.MalformedURLException; +import java.net.URL; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.GlobalConfiguration; import jenkins.model.Jenkins; import org.acegisecurity.Authentication; import org.apache.commons.lang3.StringUtils; +import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.string.AzureSecretStringCredentials; +import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.usernamepassword.AzureUsernamePasswordCredentials; @Extension @@ -37,14 +42,15 @@ public class AzureCredentialsProvider extends CredentialsProvider { private static final Logger LOG = Logger.getLogger(AzureCredentialsProvider.class.getName()); private static final String CACHE_KEY = "key"; + private static final String DEFAULT_TYPE = "string"; private final AzureCredentialsStore store = new AzureCredentialsStore(this); private final LoadingCache> cache = Caffeine.newBuilder() - .maximumSize(1L) - .expireAfterWrite(Duration.ofMinutes(120)) - .refreshAfterWrite(Duration.ofMinutes(10)) - .build(key -> fetchCredentials()); + .maximumSize(1L) + .expireAfterWrite(Duration.ofMinutes(120)) + .refreshAfterWrite(Duration.ofMinutes(10)) + .build(key -> fetchCredentials()); public void refreshCredentials() { cache.refresh(CACHE_KEY); @@ -93,7 +99,7 @@ static String getSecretName(String itemId) { private static Collection fetchCredentials() { AzureKeyVaultGlobalConfiguration azureKeyVaultGlobalConfiguration = GlobalConfiguration.all() - .get(AzureKeyVaultGlobalConfiguration.class); + .get(AzureKeyVaultGlobalConfiguration.class); if (azureKeyVaultGlobalConfiguration == null) { throw new AzureKeyVaultException("No global key vault url configured."); } @@ -106,14 +112,34 @@ private static Collection fetchCredentials() { for (SecretProperties secretItem : client.listPropertiesOfSecrets()) { String id = secretItem.getId(); Map tags = secretItem.getTags(); - if (tags != null && tags.containsKey("username")) { - AzureKeyVaultUsernamePasswordCredentials cred = new AzureKeyVaultUsernamePasswordCredentials( - CredentialsScope.GLOBAL, getSecretName(id), tags.get("username"), id, credentialID, id - ); - credentials.add(cred); - } else { - SecretStringCredentials cred = new SecretStringCredentials(CredentialsScope.GLOBAL, getSecretName(id), id, credentialID, id); - credentials.add(cred); + + if (tags == null) { + tags = new HashMap<>(); + } + + String type = tags.getOrDefault("type", DEFAULT_TYPE); + + // initial implementation didn't require a type + if (tags.containsKey("username") && type.equals(DEFAULT_TYPE)) { + type = "username"; + } + + switch (type) { + case "string": { + AzureSecretStringCredentials cred = new AzureSecretStringCredentials(getSecretName(id), "", new KeyVaultSecretRetriever(client, id)); + credentials.add(cred); + } + break; + case "username": { + AzureUsernamePasswordCredentials cred = new AzureUsernamePasswordCredentials( + getSecretName(id), tags.get("username"), "", new KeyVaultSecretRetriever(client, id) + ); + credentials.add(cred); + } + break; + default: { + throw new IllegalStateException("Unknown type: " + type); + } } } return credentials; @@ -123,6 +149,43 @@ CredentialsScope.GLOBAL, getSecretName(id), tags.get("username"), id, credential } } + private static class KeyVaultSecretRetriever implements Supplier { + + private final transient SecretClient client; + private final String secretId; + + public KeyVaultSecretRetriever(SecretClient secretClient, String secretId) { + this.client = secretClient; + this.secretId = secretId; + } + + public String retrieveSecret() { + int NAME_POSITION = 2; + int VERSION_POSITION = 3; + URL secretIdentifierUrl; + try { + secretIdentifierUrl = new URL(secretId); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + + // old SDK supports secret identifier which is a full URI to the secret + // the new SDK doesn't seem to support it to we parse it to get the values we need + // https://mine.vault.azure.net/secrets// + String[] split = secretIdentifierUrl.getPath().split("/"); + + if (split.length == NAME_POSITION + 1) { + return client.getSecret(split[NAME_POSITION]).getValue(); + } + return client.getSecret(split[NAME_POSITION], split[VERSION_POSITION]).getValue(); + } + + @Override + public Secret get() { + return Secret.fromString(retrieveSecret()); + } + } + @Override public CredentialsStore getStore(ModelObject object) { return object == Jenkins.get() ? store : null; diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/Snapshot.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/Snapshot.java new file mode 100644 index 0000000..504b5e7 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/Snapshot.java @@ -0,0 +1,20 @@ +package org.jenkinsci.plugins.azurekeyvaultplugin.credentials; + +import java.io.Serializable; +import java.util.function.Supplier; + +public class Snapshot implements Supplier, Serializable { + + private static final long serialVersionUID = 1L; + + private final T value; + + public Snapshot(T value) { + this.value = value; + } + + @Override + public T get() { + return value; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/string/AzureSecretStringCredentials.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/string/AzureSecretStringCredentials.java new file mode 100644 index 0000000..dc3c520 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/string/AzureSecretStringCredentials.java @@ -0,0 +1,43 @@ +package org.jenkinsci.plugins.azurekeyvaultplugin.credentials.string; + +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.util.Secret; +import java.util.function.Supplier; +import org.jenkinsci.plugins.azurekeyvaultplugin.AzureCredentialsProvider; +import org.jenkinsci.plugins.plaincredentials.StringCredentials; +import org.jenkinsci.plugins.plaincredentials.impl.Messages; +import org.jvnet.localizer.ResourceBundleHolder; + +public class AzureSecretStringCredentials extends BaseStandardCredentials implements StringCredentials { + + private final Supplier value; + + public AzureSecretStringCredentials(String id, String description, Supplier value) { + super(id, description); + this.value = value; + } + + @NonNull + @Override + public Secret getSecret() { + return value.get(); + } + + @Extension + @SuppressWarnings("unused") + public static class DescriptorImpl extends BaseStandardCredentialsDescriptor { + @Override + @NonNull + public String getDisplayName() { + return ResourceBundleHolder.get(Messages.class).format("StringCredentialsImpl.secret_text"); + } + + @Override + public boolean isApplicable(CredentialsProvider provider) { + return provider instanceof AzureCredentialsProvider; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/string/AzureSecretStringCredentialsSnapshotTaker.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/string/AzureSecretStringCredentialsSnapshotTaker.java new file mode 100644 index 0000000..1565535 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/string/AzureSecretStringCredentialsSnapshotTaker.java @@ -0,0 +1,27 @@ +package org.jenkinsci.plugins.azurekeyvaultplugin.credentials.string; + +import com.cloudbees.plugins.credentials.CredentialsSnapshotTaker; +import hudson.Extension; +import hudson.util.Secret; +import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.Snapshot; + +@Extension +@SuppressWarnings("unused") +public class AzureSecretStringCredentialsSnapshotTaker extends CredentialsSnapshotTaker { + @Override + public Class type() { + return AzureSecretStringCredentials.class; + } + + @Override + public AzureSecretStringCredentials snapshot(AzureSecretStringCredentials credential) { + SecretSnapshot secretSnapshot = new SecretSnapshot(credential.getSecret()); + return new AzureSecretStringCredentials(credential.getId(), credential.getDescription(), secretSnapshot); + } + + private static class SecretSnapshot extends Snapshot { + SecretSnapshot(Secret value) { + super(value); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultUsernamePasswordCredentials.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/usernamepassword/AzureUsernamePasswordCredentials.java similarity index 55% rename from src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultUsernamePasswordCredentials.java rename to src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/usernamepassword/AzureUsernamePasswordCredentials.java index 732a954..6bf556a 100644 --- a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultUsernamePasswordCredentials.java +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/usernamepassword/AzureUsernamePasswordCredentials.java @@ -1,39 +1,37 @@ -package org.jenkinsci.plugins.azurekeyvaultplugin; +package org.jenkinsci.plugins.azurekeyvaultplugin.credentials.usernamepassword; -import com.azure.security.keyvault.secrets.models.KeyVaultSecret; -import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials; import com.cloudbees.plugins.credentials.impl.Messages; -import com.microsoft.jenkins.keyvault.BaseSecretCredentials; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.Util; import hudson.util.Secret; +import java.util.function.Supplier; +import org.jenkinsci.plugins.azurekeyvaultplugin.AzureCredentialsProvider; import org.jvnet.localizer.ResourceBundleHolder; -public class AzureKeyVaultUsernamePasswordCredentials extends BaseSecretCredentials implements StandardUsernamePasswordCredentials { - final protected CredentialsScope scope; - final protected String username; +public class AzureUsernamePasswordCredentials extends BaseStandardCredentials implements StandardUsernamePasswordCredentials { + private final Supplier password; + private final String username; - - public AzureKeyVaultUsernamePasswordCredentials( - CredentialsScope scope, + public AzureUsernamePasswordCredentials( String id, String username, String description, - String servicePrincipalId, - String secretIdentifier) { - super(scope, id, description, servicePrincipalId, secretIdentifier); - this.scope = scope; + Supplier password + ) { + super(id, description); + this.password = password; this.username = Util.fixNull(username); } @NonNull @Override public Secret getPassword() { - final KeyVaultSecret secretBundle = getKeyVaultSecret(); - return Secret.fromString(secretBundle.getValue()); + return password.get(); } @NonNull @@ -42,11 +40,6 @@ public String getUsername() { return this.username; } - @Override - public CredentialsScope getScope() { - return this.scope; - } - @Extension(ordinal = 1) public static class DescriptorImpl extends BaseStandardCredentialsDescriptor { @@ -66,5 +59,10 @@ public String getDisplayName() { public String getIconClassName() { return "icon-credentials-userpass"; } + + @Override + public boolean isApplicable(CredentialsProvider provider) { + return provider instanceof AzureCredentialsProvider; + } } } diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/usernamepassword/AzureUsernamePasswordCredentialsSnapshotTaker.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/usernamepassword/AzureUsernamePasswordCredentialsSnapshotTaker.java new file mode 100644 index 0000000..9444521 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/usernamepassword/AzureUsernamePasswordCredentialsSnapshotTaker.java @@ -0,0 +1,27 @@ +package org.jenkinsci.plugins.azurekeyvaultplugin.credentials.usernamepassword; + +import com.cloudbees.plugins.credentials.CredentialsSnapshotTaker; +import hudson.Extension; +import hudson.util.Secret; +import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.Snapshot; + +@Extension +@SuppressWarnings("unused") +public class AzureUsernamePasswordCredentialsSnapshotTaker extends CredentialsSnapshotTaker { + @Override + public Class type() { + return AzureUsernamePasswordCredentials.class; + } + + @Override + public AzureUsernamePasswordCredentials snapshot(AzureUsernamePasswordCredentials credential) { + SecretSnapshot secretSnapshot = new SecretSnapshot(credential.getPassword()); + return new AzureUsernamePasswordCredentials(credential.getId(), credential.getUsername(), credential.getDescription(), secretSnapshot); + } + + private static class SecretSnapshot extends Snapshot { + SecretSnapshot(Secret value) { + super(value); + } + } +}