diff --git a/.changes/1.1.1.md b/.changes/1.1.1.md new file mode 100644 index 0000000..e98be83 --- /dev/null +++ b/.changes/1.1.1.md @@ -0,0 +1,4 @@ +## 1.1.1 - 2024-11-22 +### 🐛 Bug Fix + +- Enhanced plugin to support creating credentials in specific Jenkins folders. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b3319cb..972eee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## 1.1.1 - 2024-11-22 +### 🐛 Bug Fix + +- Enhanced plugin to support creating credentials in specific Jenkins folders. ## 1.1.0 - 2024-10-07 ### 🐛 Bug Fix diff --git a/Taskfile.yml b/Taskfile.yml index 2711cea..476a61c 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -18,7 +18,7 @@ tasks: bump: desc: bump the version using changie cmds: - - changie batch 1.1.0 + - changie batch 1.1.1 - changie merge - git add .changes/* - git add CHANGELOG.md diff --git a/pom.xml b/pom.xml index 026ce30..11d2db8 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ io.jenkins.plugins thycotic-secret-server - 1.1.0 + 1.1.1 hpi diff --git a/src/main/java/com/delinea/secrets/jenkins/global/cred/SecretServerCredentials.java b/src/main/java/com/delinea/secrets/jenkins/global/cred/SecretServerCredentials.java index cd09ef3..f6b1028 100644 --- a/src/main/java/com/delinea/secrets/jenkins/global/cred/SecretServerCredentials.java +++ b/src/main/java/com/delinea/secrets/jenkins/global/cred/SecretServerCredentials.java @@ -1,15 +1,19 @@ package com.delinea.secrets.jenkins.global.cred; import java.io.IOException; +import java.util.Collections; +import javax.annotation.Nullable; import javax.servlet.ServletException; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.verb.POST; +import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; @@ -44,7 +48,7 @@ public class SecretServerCredentials extends UsernamePasswordCredentialsImpl imp * @param secretId - The ID of the secret stored in the Secret Server. */ @DataBoundConstructor - public SecretServerCredentials(CredentialsScope scope, String id, String description, String vaultUrl, + public SecretServerCredentials(final CredentialsScope scope, final String id, final String description, String vaultUrl, String credentialId, String secretId) { super(scope, id, description, null, null); this.vaultUrl = vaultUrl; @@ -72,7 +76,7 @@ public String getSecretId() { */ @Override public String getUsername() { - return getVaultCredential().getUsername(); + return getVaultCredential(getContextItem()).getUsername(); } /** @@ -83,9 +87,21 @@ public String getUsername() { */ @Override public Secret getPassword() { - return Secret.fromString(getVaultCredential().getPassword()); + return Secret.fromString(getVaultCredential(getContextItem()).getPassword()); } + @Nullable + private Item getContextItem() { + // Retrieve the nearest item in the current request context + if (Stapler.getCurrentRequest() != null) { + Item contextItem = Stapler.getCurrentRequest().findAncestorObject(Item.class); + if (contextItem != null) { + return contextItem; + } + } + return null; + } + /** * Fetches the credentials (username and password) from the Secret Server only * once and caches it. @@ -94,10 +110,14 @@ public Secret getPassword() { * @throws RuntimeException if the credentials cannot be fetched from the Secret * Server. */ - private UsernamePassword getVaultCredential() { - if (vaultCredential == null) { // Fetch only if not already cached + private UsernamePassword getVaultCredential(@Nullable Item contextItem) { + if (vaultCredential == null) { try { - UserCredentials credential = UserCredentials.get(credentialId, null); + UserCredentials credential = UserCredentials.get(credentialId, contextItem); + if (credential == null) { + throw new RuntimeException( + "UserCredentials with the specified credentialId not found in the folder context."); + } vaultCredential = new VaultClient().fetchCredentials(vaultUrl, secretId, credential.getUsername(), credential.getPassword().getPlainText()); } catch (Exception e) { @@ -123,26 +143,34 @@ public String getDisplayName() { * @return A ListBoxModel containing the available Credential IDs. */ @POST - public ListBoxModel doFillCredentialIdItems(@AncestorInPath final Item item) { - if (item == null && !Jenkins.get().hasPermission(Jenkins.ADMINISTER) - || item != null && !item.hasPermission(Item.CONFIGURE)) { - return new StandardListBoxModel(); - } - return new StandardListBoxModel().includeAs(ACL.SYSTEM, item, UserCredentials.class).includeEmptyValue(); + public ListBoxModel doFillCredentialIdItems(@AncestorInPath final Item owner) { + if ((owner == null && !Jenkins.get().hasPermission(CredentialsProvider.CREATE)) + || (owner != null && !owner.hasPermission(CredentialsProvider.CREATE))) { + return new StandardListBoxModel(); + } + return new StandardListBoxModel() + .includeEmptyValue() + .includeAs(ACL.SYSTEM, owner, UserCredentials.class); } /** * Validates the Credential ID input by the user. */ @POST - public FormValidation doCheckCredentialId(@QueryParameter final String value) + public FormValidation doCheckCredentialId(@AncestorInPath Item item, @QueryParameter final String value) throws IOException, ServletException { - if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { - return FormValidation.error("You do not have permission to perform this action"); - } + if ((item == null && !Jenkins.get().hasPermission(CredentialsProvider.CREATE)) + || (item != null && !item.hasPermission(CredentialsProvider.CREATE))) { + return FormValidation.error("You do not have permission to perform this action."); + } if (StringUtils.isBlank(value)) { return FormValidation.error("Credential ID is required."); } + // Check if the Credential ID exists within the specified item context + if (CredentialsProvider.lookupCredentials(UserCredentials.class, item, ACL.SYSTEM, Collections.emptyList()) + .stream().noneMatch(cred -> cred.getId().equals(value))) { + return FormValidation.error("Credential ID not found. Please provide a valid ID."); + } return FormValidation.ok(); } @@ -150,10 +178,12 @@ public FormValidation doCheckCredentialId(@QueryParameter final String value) * Validates the Secret ID input by the user. */ @POST - public FormValidation doCheckSecretId(@QueryParameter final String value) throws IOException, ServletException { - if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { - return FormValidation.error("You do not have permission to perform this action"); - } + public FormValidation doCheckSecretId(@AncestorInPath final Item item, @QueryParameter final String value) + throws IOException, ServletException { + if ((item == null && !Jenkins.get().hasPermission(CredentialsProvider.CREATE)) + || (item != null && !item.hasPermission(CredentialsProvider.CREATE))) { + return FormValidation.error("You do not have permission to perform this action."); + } if (StringUtils.isBlank(value)) { return FormValidation.error("Secret ID is required."); } @@ -180,15 +210,11 @@ public FormValidation doTestConnection(@AncestorInPath Item owner, @QueryParameter("vaultUrl") final String vaultUrl, @QueryParameter("credentialId") final String credentialId, @QueryParameter("secretId") final String secretId) { + if ((owner == null && !Jenkins.get().hasPermission(CredentialsProvider.CREATE)) + || (owner != null && !owner.hasPermission(CredentialsProvider.CREATE))) { + return FormValidation.error("You do not have permission to perform this action."); + } - // Check for necessary permissions - if (owner == null) { - Jenkins.get().checkPermission(Jenkins.ADMINISTER); - } else { - owner.checkPermission(Item.CONFIGURE); - } - - // Validate inputs if (StringUtils.isBlank(credentialId)) { return FormValidation.error("Credential ID is required to test the connection."); } @@ -196,10 +222,9 @@ public FormValidation doTestConnection(@AncestorInPath Item owner, if (StringUtils.isBlank(vaultUrl)) { return FormValidation.error("Vault URL cannot be blank."); } - + try { - // Attempt to fetch credentials from Secret Server - UserCredentials credential = UserCredentials.get(credentialId, null); + UserCredentials credential = UserCredentials.get(credentialId, owner); new VaultClient().fetchCredentials(vaultUrl, secretId, credential.getUsername(), credential.getPassword().getPlainText()); return FormValidation.ok("Connection successful."); diff --git a/src/main/java/com/delinea/secrets/jenkins/wrapper/cred/UserCredentials.java b/src/main/java/com/delinea/secrets/jenkins/wrapper/cred/UserCredentials.java index 24b9af7..21f867f 100644 --- a/src/main/java/com/delinea/secrets/jenkins/wrapper/cred/UserCredentials.java +++ b/src/main/java/com/delinea/secrets/jenkins/wrapper/cred/UserCredentials.java @@ -5,6 +5,8 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.kohsuke.stapler.DataBoundConstructor; + import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsScope; @@ -12,10 +14,9 @@ import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import com.cloudbees.plugins.credentials.matchers.IdMatcher; -import org.kohsuke.stapler.DataBoundConstructor; - import hudson.Extension; import hudson.model.Item; +import hudson.model.ItemGroup; import hudson.security.ACL; import jenkins.model.Jenkins; @@ -29,18 +30,26 @@ public class UserCredentials extends UsernamePasswordCredentialsImpl implements * @param item the optional item (context) * @return the credentials or {@code null} if no matching credentials exist */ - public static UserCredentials get(@Nonnull final String credentialId, @Nullable final Item item) { - if(Jenkins.get().hasPermission(CredentialsProvider.VIEW)) - { - return CredentialsMatchers.firstOrNull( - CredentialsProvider.lookupCredentials(UserCredentials.class, item, ACL.SYSTEM, Collections.emptyList()), - new IdMatcher(credentialId)); - } - else - { - return null; - } - } + public static UserCredentials get(@Nonnull final String credentialId, @Nullable final Item item) { + if (item != null) { + // If we're inside a folder (item is non-null), check for the read permission at + // the folder level. + if (item.hasPermission(Item.READ)) { + return CredentialsProvider + .lookupCredentials(UserCredentials.class, item, ACL.SYSTEM, Collections.emptyList()).stream() + .filter(cred -> cred.getId().equals(credentialId)).findFirst().orElse(null); + } + } else { + // If there's no item (global context), check for global permission to view + // credentials. + if (Jenkins.get().hasPermission(CredentialsProvider.VIEW)) { + return CredentialsMatchers.firstOrNull(CredentialsProvider.lookupCredentials(UserCredentials.class, + (ItemGroup) null, ACL.SYSTEM, Collections.emptyList()), new IdMatcher(credentialId)); + } + } + + return null; + } @DataBoundConstructor public UserCredentials(final CredentialsScope scope, final String id, final String description,