Skip to content

Commit

Permalink
Merge pull request #36 from delinea-rajani/master
Browse files Browse the repository at this point in the history
  • Loading branch information
tylerezimmerman authored Nov 29, 2024
2 parents 7d90b55 + f94cad7 commit b46c3dc
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 47 deletions.
4 changes: 4 additions & 0 deletions .changes/1.1.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 1.1.1 - 2024-11-22
### 🐛 Bug Fix

- Enhanced plugin to support creating credentials in specific Jenkins folders.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</parent>
<groupId>io.jenkins.plugins</groupId>
<artifactId>thycotic-secret-server</artifactId>
<version>1.1.0</version>
<version>1.1.1</version>
<packaging>hpi</packaging>
<properties>
<!-- Baseline Jenkins version you use to build the plugin. Users must have this version or newer to run. -->
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -72,7 +76,7 @@ public String getSecretId() {
*/
@Override
public String getUsername() {
return getVaultCredential().getUsername();
return getVaultCredential(getContextItem()).getUsername();
}

/**
Expand All @@ -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.
Expand All @@ -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) {
Expand All @@ -123,37 +143,47 @@ 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();
}

/**
* 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.");
}
Expand All @@ -180,26 +210,21 @@ 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.");
}

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);

Check warning on line 227 in src/main/java/com/delinea/secrets/jenkins/global/cred/SecretServerCredentials.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 79-227 are not covered by tests
new VaultClient().fetchCredentials(vaultUrl, secretId, credential.getUsername(),
credential.getPassword().getPlainText());
return FormValidation.ok("Connection successful.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@
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;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
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;

Expand All @@ -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));

Check warning on line 47 in src/main/java/com/delinea/secrets/jenkins/wrapper/cred/UserCredentials.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 34-47 are not covered by tests
}
}

return null;
}

@DataBoundConstructor
public UserCredentials(final CredentialsScope scope, final String id, final String description,
Expand Down

0 comments on commit b46c3dc

Please sign in to comment.