From 9f30b40179fc639e2488f2227c48d3fcb0f46319 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Fri, 29 Oct 2021 20:13:58 +0100 Subject: [PATCH] Add FolderCredentialsProvider --- pom.xml | 5 + .../AzureKeyVaultGlobalConfiguration.java | 1 + .../AzureKeyVaultUtil.java | 2 +- .../string/AzureSecretStringCredentials.java | 4 +- .../AzureUsernamePasswordCredentials.java | 4 +- .../provider/CredentialsProviderHelper.java | 15 + .../provider/KeyVaultSecretRetriever.java | 44 ++ .../FolderAzureCredentialsProvider.java | 428 ++++++++++++++++++ .../global}/AzureCredentialsProvider.java | 50 +- .../global}/AzureCredentialsStore.java | 2 +- .../config.jelly | 2 +- .../config.jelly | 16 + .../help-credentialID.html | 5 + .../help-keyVaultURL.html | 1 + .../global}/AzureCredentialsProviderTest.java | 4 +- 15 files changed, 531 insertions(+), 52 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/CredentialsProviderHelper.java create mode 100644 src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/KeyVaultSecretRetriever.java create mode 100644 src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider.java rename src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/{ => provider/global}/AzureCredentialsProvider.java (79%) rename src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/{ => provider/global}/AzureCredentialsStore.java (98%) create mode 100644 src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider/FolderAzureKeyVaultCredentialsProperty/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider/FolderAzureKeyVaultCredentialsProperty/help-credentialID.html create mode 100644 src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider/FolderAzureKeyVaultCredentialsProperty/help-keyVaultURL.html rename src/test/java/org/jenkinsci/plugins/azurekeyvaultplugin/{ => provider/global}/AzureCredentialsProviderTest.java (82%) diff --git a/pom.xml b/pom.xml index eb313a0..9f022f4 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,11 @@ configuration-as-code true + + org.jenkins-ci.plugins + cloudbees-folder + true + diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultGlobalConfiguration.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultGlobalConfiguration.java index 56ca045..f4a88be 100644 --- a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultGlobalConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultGlobalConfiguration.java @@ -22,6 +22,7 @@ import jenkins.model.Jenkins; import org.apache.commons.lang3.StringUtils; import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.azurekeyvaultplugin.provider.global.AzureCredentialsProvider; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultUtil.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultUtil.java index 38d2d92..2e5b7fd 100644 --- a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultUtil.java +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultUtil.java @@ -43,7 +43,7 @@ import javax.xml.bind.DatatypeConverter; import jenkins.model.Jenkins; -class AzureKeyVaultUtil { +public class AzureKeyVaultUtil { private static final char[] EMPTY_CHAR_ARRAY = new char[0]; private static final String PKCS12 = "PKCS12"; 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 index dc3c520..0ebfe2a 100644 --- a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/string/AzureSecretStringCredentials.java +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/string/AzureSecretStringCredentials.java @@ -6,7 +6,7 @@ import hudson.Extension; import hudson.util.Secret; import java.util.function.Supplier; -import org.jenkinsci.plugins.azurekeyvaultplugin.AzureCredentialsProvider; +import org.jenkinsci.plugins.azurekeyvaultplugin.provider.CredentialsProviderHelper; import org.jenkinsci.plugins.plaincredentials.StringCredentials; import org.jenkinsci.plugins.plaincredentials.impl.Messages; import org.jvnet.localizer.ResourceBundleHolder; @@ -37,7 +37,7 @@ public String getDisplayName() { @Override public boolean isApplicable(CredentialsProvider provider) { - return provider instanceof AzureCredentialsProvider; + return CredentialsProviderHelper.isAzureCredentialsProvider(provider); } } } diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/usernamepassword/AzureUsernamePasswordCredentials.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/usernamepassword/AzureUsernamePasswordCredentials.java index 6bf556a..6edc41e 100644 --- a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/usernamepassword/AzureUsernamePasswordCredentials.java +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/usernamepassword/AzureUsernamePasswordCredentials.java @@ -9,7 +9,7 @@ import hudson.Util; import hudson.util.Secret; import java.util.function.Supplier; -import org.jenkinsci.plugins.azurekeyvaultplugin.AzureCredentialsProvider; +import org.jenkinsci.plugins.azurekeyvaultplugin.provider.CredentialsProviderHelper; import org.jvnet.localizer.ResourceBundleHolder; @@ -62,7 +62,7 @@ public String getIconClassName() { @Override public boolean isApplicable(CredentialsProvider provider) { - return provider instanceof AzureCredentialsProvider; + return CredentialsProviderHelper.isAzureCredentialsProvider(provider); } } } diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/CredentialsProviderHelper.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/CredentialsProviderHelper.java new file mode 100644 index 0000000..c5ce85e --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/CredentialsProviderHelper.java @@ -0,0 +1,15 @@ +package org.jenkinsci.plugins.azurekeyvaultplugin.provider; + +import com.cloudbees.plugins.credentials.CredentialsProvider; +import org.jenkinsci.plugins.azurekeyvaultplugin.provider.folder.FolderAzureCredentialsProvider; +import org.jenkinsci.plugins.azurekeyvaultplugin.provider.global.AzureCredentialsProvider; + +public class CredentialsProviderHelper { + + private CredentialsProviderHelper() { + } + + public static boolean isAzureCredentialsProvider(CredentialsProvider provider) { + return provider instanceof AzureCredentialsProvider || provider instanceof FolderAzureCredentialsProvider; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/KeyVaultSecretRetriever.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/KeyVaultSecretRetriever.java new file mode 100644 index 0000000..aec5960 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/KeyVaultSecretRetriever.java @@ -0,0 +1,44 @@ +package org.jenkinsci.plugins.azurekeyvaultplugin.provider; + +import com.azure.security.keyvault.secrets.SecretClient; +import hudson.util.Secret; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.function.Supplier; + +public 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()); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider.java new file mode 100644 index 0000000..3ac9c43 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider.java @@ -0,0 +1,428 @@ +package org.jenkinsci.plugins.azurekeyvaultplugin.provider.folder; + +import com.azure.security.keyvault.secrets.SecretClient; +import com.azure.security.keyvault.secrets.models.SecretProperties; +import com.cloudbees.hudson.plugins.folder.AbstractFolder; +import com.cloudbees.hudson.plugins.folder.AbstractFolderProperty; +import com.cloudbees.hudson.plugins.folder.AbstractFolderPropertyDescriptor; +import com.cloudbees.plugins.credentials.Credentials; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsStore; +import com.cloudbees.plugins.credentials.CredentialsStoreAction; +import com.cloudbees.plugins.credentials.common.IdCredentials; +import com.cloudbees.plugins.credentials.domains.Domain; +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 edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import hudson.Extension; +import hudson.ExtensionList; +import hudson.model.Item; +import hudson.model.ItemGroup; +import hudson.model.ModelObject; +import hudson.security.ACL; +import hudson.security.Permission; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; +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.Objects; +import java.util.WeakHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.model.Jenkins; +import net.jcip.annotations.GuardedBy; +import org.acegisecurity.Authentication; +import org.apache.commons.lang3.StringUtils; +import org.jenkins.ui.icon.Icon; +import org.jenkins.ui.icon.IconSet; +import org.jenkins.ui.icon.IconType; +import org.jenkinsci.plugins.azurekeyvaultplugin.AzureKeyVaultException; +import org.jenkinsci.plugins.azurekeyvaultplugin.AzureKeyVaultUtil; +import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.string.AzureSecretStringCredentials; +import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.usernamepassword.AzureUsernamePasswordCredentials; +import org.jenkinsci.plugins.azurekeyvaultplugin.provider.KeyVaultSecretRetriever; +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.verb.POST; + + +@Extension(optional = true) +public class FolderAzureCredentialsProvider extends CredentialsProvider { + private static final Logger LOG = Logger.getLogger(FolderAzureCredentialsProvider.class.getName()); + + private static final String CACHE_KEY = "key"; + private static final String DEFAULT_TYPE = "string"; + + @GuardedBy("self") + private static final WeakHashMap, FolderAzureKeyVaultCredentialsProperty> emptyProperties = + new WeakHashMap<>(); + + private final LoadingCache> cache = Caffeine.newBuilder() + .maximumSize(1L) + .expireAfterWrite(Duration.ofMinutes(120)) + .refreshAfterWrite(Duration.ofMinutes(10)) + .build(FolderAzureCredentialsProvider::fetchCredentials); + + public void refreshCredentials() { + cache.invalidateAll(); + } + + private static final class CacheKey { + String credentialID; + String url; + String itemName; + + public CacheKey(String credentialID, String url, String itemName) { + this.credentialID = credentialID; + this.url = url; + this.itemName = itemName; + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CacheKey cacheKey = (CacheKey) o; + return Objects.equals(credentialID, cacheKey.credentialID) && Objects.equals(url, cacheKey.url) && Objects.equals(itemName, cacheKey.itemName); + } + + @Override + public int hashCode() { + return Objects.hash(credentialID, url, itemName); + } + } + + public static FolderAzureCredentialsProvider get() { + return ExtensionList.lookupSingleton(FolderAzureCredentialsProvider.class); + } + + @NonNull + @Override + public List getCredentials(@NonNull Class aClass, @Nullable ItemGroup itemGroup, + @Nullable Authentication authentication) { + if (ACL.SYSTEM.equals(authentication)) { + while (itemGroup != null) { + if (itemGroup instanceof AbstractFolder) { + final AbstractFolder folder = (AbstractFolder) itemGroup; + FolderAzureKeyVaultCredentialsProperty property = folder.getProperties() + .get(FolderAzureKeyVaultCredentialsProperty.class); + if (property != null) { + final ArrayList list = new ArrayList<>(); + try { + CacheKey key = new CacheKey(property.getCredentialID(), property.getUrl(), property.getOwner().getFullName()); + Collection credentials = cache.get(key); + if (credentials == null) { + throw new IllegalStateException("Cache is not working"); + } + + for (IdCredentials credential : credentials) { + if (aClass.isAssignableFrom(credential.getClass())) { + // cast to keep generics happy even though we are assignable.. + list.add(aClass.cast(credential)); + } + LOG.log(Level.FINEST, "getCredentials {0} does not match", credential.getId()); + } + } catch (RuntimeException e) { + LOG.log(Level.WARNING, "Error retrieving secrets from Azure KeyVault: " + e.getMessage(), e); + return Collections.emptyList(); + } + return list; + } + } + if (itemGroup instanceof Item) { + itemGroup = ((Item) itemGroup).getParent(); + } else { + break; + } + } + } + + return Collections.emptyList(); + } + + @VisibleForTesting + static String getSecretName(String itemId) { + if (StringUtils.isEmpty(itemId)) { + throw new AzureKeyVaultException("Empty id for key vault item."); + } + int index = itemId.lastIndexOf('/'); + if (index < 0) { + throw new AzureKeyVaultException("Wrong pattern for key vault item id."); + } + return itemId.substring(index + 1); + } + + private static Collection fetchCredentials(CacheKey key) { + + String credentialID = key.credentialID; + try { + SecretClient client = SecretClientCache.get(credentialID, key.url); + + List credentials = new ArrayList<>(); + // TODO refactor out duplicate + for (SecretProperties secretItem : client.listPropertiesOfSecrets()) { + String id = secretItem.getId(); + Map tags = secretItem.getTags(); + + 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), id, new KeyVaultSecretRetriever(client, id)); + credentials.add(cred); + } + break; + case "username": { + AzureUsernamePasswordCredentials cred = new AzureUsernamePasswordCredentials( + getSecretName(id), tags.get("username"), id, new KeyVaultSecretRetriever(client, id) + ); + credentials.add(cred); + } + break; + default: { + throw new IllegalStateException("Unknown type: " + type); + } + } + } + return credentials; + } catch (Exception e) { + LOG.log(Level.WARNING, "Error retrieving secrets from Azure KeyVault: " + e.getMessage(), e); + return Collections.emptyList(); + } + } + + @Override + public CredentialsStore getStore(ModelObject object) { + if (object instanceof AbstractFolder) { + final AbstractFolder folder = (AbstractFolder) object; + FolderAzureKeyVaultCredentialsProperty property = folder.getProperties() + .get(FolderAzureKeyVaultCredentialsProperty.class); + if (property != null) { + return property.getStore(); + } + synchronized (emptyProperties) { + property = emptyProperties.get(folder); + if (property == null) { + property = new FolderAzureKeyVaultCredentialsProperty(folder); + emptyProperties.put(folder, property); + } + } + return property.getStore(); + } + return null; + } + + @Override + public String getIconClassName() { + return "icon-azure-key-vault-credentials-store"; + } + + public static class FolderAzureKeyVaultCredentialsProperty extends AbstractFolderProperty> { + + private final FolderAzureCredentialsStore store = new FolderAzureCredentialsStore(); + private String url; + private String credentialID; + + public FolderAzureKeyVaultCredentialsProperty(AbstractFolder owner) { + setOwner(owner); + } + + @DataBoundConstructor + public FolderAzureKeyVaultCredentialsProperty(String url, String credentialID) { + this.url = url; + this.credentialID = credentialID; + } + + public String getUrl() { + return url; + } + + public String getCredentialID() { + return credentialID; + } + + public FolderAzureCredentialsStore getStore() { + return store; + } + + @NonNull + private List getCredentials(@NonNull Domain domain) { + if (Domain.global().equals(domain) && store.hasPermission(CredentialsProvider.VIEW)) { + FolderAzureCredentialsProvider provider = FolderAzureCredentialsProvider.get(); + return provider.getCredentials(Credentials.class, (ItemGroup) owner, ACL.SYSTEM); + } else { + return Collections.emptyList(); + } + } + + @Extension(optional = true) + public static class DescriptorImpl extends AbstractFolderPropertyDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Azure Key Vault?"; + } + + @SuppressWarnings("unused") + @POST + public ListBoxModel doFillCredentialIDItems(@AncestorInPath Item context) { + return AzureKeyVaultUtil.doFillCredentialIDItems(context); + } + + @POST + @SuppressWarnings("unused") + public FormValidation doTestConnection( + @AncestorInPath Item context, + @QueryParameter("url") final String keyVaultURL, + @QueryParameter("credentialID") final String credentialID + ) { + if (context == null) { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + } else { + context.checkPermission(Item.CONFIGURE); + } + + if (keyVaultURL == null) { + return FormValidation.error("Key vault url is required"); + } + + if (credentialID == null) { + return FormValidation.error("Credential ID is required"); + } + + try { + // TODO needs to add item context + SecretClient client = SecretClientCache.get(credentialID, keyVaultURL); + + Long numberOfSecrets = client.listPropertiesOfSecrets().stream().count(); + return FormValidation.ok(String.format("Success, found %d secrets in the vault", numberOfSecrets)); + } catch (RuntimeException e) { + LOG.log(Level.WARNING, "Failed testing connection", e); + return FormValidation.error(e, e.getMessage()); + } + } + + } + + private class FolderAzureCredentialsStore extends CredentialsStore { + + private final FolderAzureCredentialsStoreAction action = new FolderAzureCredentialsStoreAction(); + + @NonNull + @Override + public ModelObject getContext() { + return owner; + } + + @Override + public boolean hasPermission(@NonNull Authentication authentication, @NonNull Permission permission) { + return owner.getACL().hasPermission(authentication, permission); + } + + @NonNull + @Override + public List getCredentials(@NonNull Domain domain) { + return FolderAzureKeyVaultCredentialsProperty.this.getCredentials(domain); + } + + @Override + public boolean addCredentials(@NonNull Domain domain, @NonNull Credentials credentials) { + throw new UnsupportedOperationException( + "Jenkins may not add credentials to Azure Key Vault"); + } + + @Override + public boolean removeCredentials(@NonNull Domain domain, @NonNull Credentials credentials) { + throw new UnsupportedOperationException( + "Jenkins may not remove credentials in Azure Key Vault"); + } + + @Override + public boolean updateCredentials(@NonNull Domain domain, @NonNull Credentials credentials, + @NonNull Credentials credentials1) { + throw new UnsupportedOperationException( + "Jenkins may not update credentials in Azure Key Vault"); + } + + @Nullable + @Override + public CredentialsStoreAction getStoreAction() { + return action; + } + + } + + public final class FolderAzureCredentialsStoreAction extends CredentialsStoreAction { + + private static final String ICON_CLASS = "icon-azure-key-vault-credentials-store"; + + private FolderAzureCredentialsStoreAction() { + addIcons(); + } + + private void addIcons() { + IconSet.icons.addIcon(new Icon(ICON_CLASS + " icon-sm", + "azure-keyvault/images/16x16/icon.png", + Icon.ICON_SMALL_STYLE, IconType.PLUGIN)); + IconSet.icons.addIcon(new Icon(ICON_CLASS + " icon-md", + "azure-keyvault/images/24x24/icon.png", + Icon.ICON_MEDIUM_STYLE, IconType.PLUGIN)); + IconSet.icons.addIcon(new Icon(ICON_CLASS + " icon-lg", + "azure-keyvault/images/32x32/icon.png", + Icon.ICON_LARGE_STYLE, IconType.PLUGIN)); + IconSet.icons.addIcon(new Icon(ICON_CLASS + " icon-xlg", + "azure-keyvault/images/48x48/icon.png", + Icon.ICON_XLARGE_STYLE, IconType.PLUGIN)); + } + + @Override + @NonNull + public CredentialsStore getStore() { + return FolderAzureKeyVaultCredentialsProperty.this.getStore(); + } + + @Override + public String getIconFileName() { + return isVisible() + ? "/plugin/azure-keyvault/images/32x32/icon.png" + : null; + } + + @Override + public String getIconClassName() { + return isVisible() + ? ICON_CLASS + : null; + } + + @Override + public String getDisplayName() { + return "Azure Key Vault"; + } + } + + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProvider.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/global/AzureCredentialsProvider.java similarity index 79% rename from src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProvider.java rename to src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/global/AzureCredentialsProvider.java index 1bd91cb..fbd5356 100644 --- a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProvider.java +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/global/AzureCredentialsProvider.java @@ -1,4 +1,4 @@ -package org.jenkinsci.plugins.azurekeyvaultplugin; +package org.jenkinsci.plugins.azurekeyvaultplugin.provider.global; import com.azure.security.keyvault.secrets.SecretClient; import com.azure.security.keyvault.secrets.models.SecretProperties; @@ -16,9 +16,6 @@ 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; @@ -26,15 +23,17 @@ 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.AzureKeyVaultException; +import org.jenkinsci.plugins.azurekeyvaultplugin.AzureKeyVaultGlobalConfiguration; import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.string.AzureSecretStringCredentials; import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.usernamepassword.AzureUsernamePasswordCredentials; +import org.jenkinsci.plugins.azurekeyvaultplugin.provider.KeyVaultSecretRetriever; @Extension @@ -126,13 +125,13 @@ private static Collection fetchCredentials() { switch (type) { case "string": { - AzureSecretStringCredentials cred = new AzureSecretStringCredentials(getSecretName(id), "", new KeyVaultSecretRetriever(client, id)); + AzureSecretStringCredentials cred = new AzureSecretStringCredentials(getSecretName(id), id, new KeyVaultSecretRetriever(client, id)); credentials.add(cred); } break; case "username": { AzureUsernamePasswordCredentials cred = new AzureUsernamePasswordCredentials( - getSecretName(id), tags.get("username"), "", new KeyVaultSecretRetriever(client, id) + getSecretName(id), tags.get("username"), id, new KeyVaultSecretRetriever(client, id) ); credentials.add(cred); } @@ -149,43 +148,6 @@ private static Collection fetchCredentials() { } } - 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/AzureCredentialsStore.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/global/AzureCredentialsStore.java similarity index 98% rename from src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsStore.java rename to src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/global/AzureCredentialsStore.java index dd7c1cc..5c2dd9d 100644 --- a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsStore.java +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/global/AzureCredentialsStore.java @@ -1,4 +1,4 @@ -package org.jenkinsci.plugins.azurekeyvaultplugin; +package org.jenkinsci.plugins.azurekeyvaultplugin.provider.global; import com.cloudbees.plugins.credentials.Credentials; import com.cloudbees.plugins.credentials.CredentialsProvider; diff --git a/src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultGlobalConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultGlobalConfiguration/config.jelly index 6629fa6..e74bdb7 100644 --- a/src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultGlobalConfiguration/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/AzureKeyVaultGlobalConfiguration/config.jelly @@ -1,6 +1,6 @@ - + diff --git a/src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider/FolderAzureKeyVaultCredentialsProperty/config.jelly b/src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider/FolderAzureKeyVaultCredentialsProperty/config.jelly new file mode 100644 index 0000000..a2e9c4c --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider/FolderAzureKeyVaultCredentialsProperty/config.jelly @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider/FolderAzureKeyVaultCredentialsProperty/help-credentialID.html b/src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider/FolderAzureKeyVaultCredentialsProperty/help-credentialID.html new file mode 100644 index 0000000..56ece82 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider/FolderAzureKeyVaultCredentialsProperty/help-credentialID.html @@ -0,0 +1,5 @@ +
Specify the Credential ID used for accessing Key Vault. +
    +
  • Must be a Azure Service Principal or Azure Managed Identity +
+
diff --git a/src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider/FolderAzureKeyVaultCredentialsProperty/help-keyVaultURL.html b/src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider/FolderAzureKeyVaultCredentialsProperty/help-keyVaultURL.html new file mode 100644 index 0000000..77d3c88 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/azurekeyvaultplugin/provider/folder/FolderAzureCredentialsProvider/FolderAzureKeyVaultCredentialsProperty/help-keyVaultURL.html @@ -0,0 +1 @@ +
The URL at which your Key Vault is located (e.g. https://YOURKEYVAULT.vault.azure.net)
diff --git a/src/test/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProviderTest.java b/src/test/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/global/AzureCredentialsProviderTest.java similarity index 82% rename from src/test/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProviderTest.java rename to src/test/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/global/AzureCredentialsProviderTest.java index 397e7a4..f2f9c51 100644 --- a/src/test/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProviderTest.java +++ b/src/test/java/org/jenkinsci/plugins/azurekeyvaultplugin/provider/global/AzureCredentialsProviderTest.java @@ -1,5 +1,7 @@ -package org.jenkinsci.plugins.azurekeyvaultplugin; +package org.jenkinsci.plugins.azurekeyvaultplugin.provider.global; +import org.jenkinsci.plugins.azurekeyvaultplugin.AzureKeyVaultException; +import org.jenkinsci.plugins.azurekeyvaultplugin.provider.global.AzureCredentialsProvider; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException;