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,