Skip to content

Commit

Permalink
Merge pull request #13 from jenkinsci/feature/SIGN-6984
Browse files Browse the repository at this point in the history
SIGN-6984 - allow to store Api Token in global and system scope
  • Loading branch information
volbobvol authored Jun 27, 2024
2 parents fac1f88 + a78daac commit ea5a2c9
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 20 deletions.
16 changes: 5 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ On SignPath.io:

On Jenkins:

1. Store the **Trusted Build System Token** in a System Credential (Under Manage Jenkins / Manage Credentials)
2. Store the API Token(s) in a System Credential so that it is available to the build pipelines of the respective projects
1. Store the **Trusted Build System Token** in a System Credential (Under Manage Jenkins / Manage Credentials) with the id `SignPath.TrustedBuildSystemToken`
2. Store the API Token(s) in a Credential so that it is available to the build pipelines of the respective projects (default id `SignPath.ApiToken`)

_Note: Currently, the SignPath plugin requires you to use **git** as your source control system. The git repository origin information is extracted and included in the signing request._

Expand All @@ -53,9 +53,6 @@ Include the `submitSigningRequest` and optionally, the `getSignedArtifact` steps
stage('Sign with SignPath') {
steps {
submitSigningRequest(
apiUrl: "https://app.signpath.io/Api",
apiTokenCredentialId: "${API_TOKEN_CREDENTIAL_ID}",
trustedBuildSystemTokenCredentialId: "${TRUSTED_BUILD_SYSTEM_TOKEN_CREDENTIAL_ID}",
organizationId: "${ORGANIZATION_ID}",
projectSlug: "${PROJECT_SLUG}",
signingPolicySlug: "${SIGNING_POLICY_SLUG}",
Expand All @@ -74,9 +71,6 @@ stage('Sign with SignPath') {
steps {
script {
signingRequestId = submitSigningRequest(
apiUrl: "https://app.signpath.io/Api",
apiTokenCredentialId: "${API_TOKEN_CREDENTIAL_ID}",
trustedBuildSystemTokenCredentialId: "${TRUSTED_BUILD_SYSTEM_TOKEN_CREDENTIAL_ID}",
organizationId: "${ORGANIZATION_ID}",
projectSlug: "${PROJECT_SLUG}",
signingPolicySlug: "${SIGNING_POLICY_SLUG}",
Expand All @@ -94,7 +88,6 @@ stage('Download Signed Artifact') {
}
steps{
getSignedArtifact(
apiToken: "${API_TOKEN}",
organizationId: "${ORGANIZATION_ID}",
signingRequestId: "${signingRequestId}",
outputArtifactPath: "build-output/my-artifact.exe"
Expand All @@ -109,9 +102,10 @@ stage('Download Signed Artifact') {
| Parameter | |
| ----------------------------------------------------- | ---- |
| `apiUrl` | (optional) The API endpoint of SignPath. Defaults to `https://app.signpath.io/api`
| `apiTokenCredentialId` | The ID of the credential containing the **API Token**
| `trustedBuildSytemTokenCredentialId` | The ID of the credential containing the **Trusted Build System Token**
| `apiTokenCredentialId` | The ID of the credential containing the **API Token**. Defaults to `SignPath.ApiToken`. Recommended in scope "Global".
| `trustedBuildSytemTokenCredentialId` | The ID of the credential containing the **Trusted Build System Token**. Needs to be in scope "System".
| `organizationId`, `projectSlug`, `signingPolicySlug` | Specify which organization, project and signing policy to use for signing. See the [official documentation](https://about.signpath.io/documentation/build-system-integration)
| `artifactConfigurationSlug` | (optional). Specify which artifact configuration to use. See the [official documentation](https://about.signpath.io/documentation/build-system-integration)
| `inputArtifactPath` | The relative path of the artifact to be signed
| `outputArtifactPath` | The relative path where the signed artifact is stored after signing
| `waitForCompletion` | Set to `true` for synchronous and `false` for asynchronous signing requests
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.jenkins.plugins.signpath;

import com.cloudbees.plugins.credentials.CredentialsScope;
import hudson.model.TaskListener;
import hudson.util.Secret;
import io.jenkins.plugins.signpath.ApiIntegration.SignPathCredentials;
Expand Down Expand Up @@ -54,7 +55,7 @@ protected Void run() throws SignPathStepFailedException {

try {
Secret trustedBuildSystemToken = secretRetriever.retrieveSecret(input.getTrustedBuildSystemTokenCredentialId());
Secret apiToken = secretRetriever.retrieveSecret(input.getApiTokenCredentialId());
Secret apiToken = secretRetriever.retrieveSecret(input.getApiTokenCredentialId(), new CredentialsScope[] { CredentialsScope.SYSTEM, CredentialsScope.GLOBAL });
SignPathCredentials credentials = new SignPathCredentials(apiToken, trustedBuildSystemToken);
SignPathFacade signPathFacade = signPathFacadeFactory.create(credentials);
try (TemporaryFile signedArtifact = signPathFacade.getSignedArtifact(input.getOrganizationId(), input.getSigningRequestId())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
import com.cloudbees.plugins.credentials.CredentialsScope;
import hudson.util.Secret;
import io.jenkins.plugins.signpath.Exceptions.SecretNotFoundException;
import java.util.Arrays;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
* Implementation of the
Expand All @@ -30,6 +32,12 @@ public CredentialBasedSecretRetriever(Jenkins jenkins) {

@Override
public Secret retrieveSecret(String id) throws SecretNotFoundException {
CredentialsScope[] allowedScopes = { CredentialsScope.SYSTEM };
return retrieveSecret(id, allowedScopes);
}

@Override
public Secret retrieveSecret(String id, CredentialsScope[] allowedScopes) throws SecretNotFoundException {
List<StringCredentials> credentials =
// authentication: null => SYSTEM, but with no warnings for using deprecated fields
CredentialsProvider.lookupCredentials(StringCredentials.class, jenkins, null, Collections.emptyList());
Expand All @@ -40,12 +48,17 @@ public Secret retrieveSecret(String id) throws SecretNotFoundException {
throw new SecretNotFoundException(String.format("The secret '%s' could not be found in the credential store.", id));
}

if (credential.getScope() != CredentialsScope.SYSTEM) {
CredentialsScope scope = credential.getScope();
String scopeName = scope == null ? "<null>" : scope.getDisplayName();
CredentialsScope credentialScope = credential.getScope();

if (allowedScopes.length > 0 && !Arrays.asList(allowedScopes).contains(credentialScope)) {
String scopeName = credentialScope == null ? "<null>" : credentialScope.getDisplayName();
String allowedScopesStr = Arrays.stream(allowedScopes)
.map(CredentialsScope::getDisplayName)
.collect(Collectors.joining("' or '"));

throw new SecretNotFoundException(
String.format("The secret '%s' was configured with scope '%s' but needs to be in scope '%s'.",
id, scopeName, CredentialsScope.SYSTEM.getDisplayName()));
String.format("The secret '%s' was configured with scope '%s' but needs to be in scope(s) '%s'.",
id, scopeName, allowedScopesStr));
}

return credential.getSecret();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import hudson.util.Secret;
import io.jenkins.plugins.signpath.Exceptions.SecretNotFoundException;
import com.cloudbees.plugins.credentials.CredentialsScope;

/**
* Allows retrieving secrets
Expand All @@ -15,4 +16,6 @@ public interface SecretRetriever {
* @throws SecretNotFoundException occurs if the secret is not found
*/
Secret retrieveSecret(String id) throws SecretNotFoundException;

Secret retrieveSecret(String id, CredentialsScope[] allowedScopes) throws SecretNotFoundException;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.jenkins.plugins.signpath;

import com.cloudbees.plugins.credentials.CredentialsScope;
import hudson.model.TaskListener;
import hudson.util.Secret;
import io.jenkins.plugins.signpath.ApiIntegration.Model.SigningRequestModel;
Expand Down Expand Up @@ -68,7 +69,7 @@ protected String run() throws SignPathStepFailedException {

try {
Secret trustedBuildSystemToken = secretRetriever.retrieveSecret(input.getTrustedBuildSystemTokenCredentialId());
Secret apiToken = secretRetriever.retrieveSecret(input.getApiTokenCredentialId());
Secret apiToken = secretRetriever.retrieveSecret(input.getApiTokenCredentialId(), new CredentialsScope[] { CredentialsScope.SYSTEM, CredentialsScope.GLOBAL });
SignPathCredentials credentials = new SignPathCredentials(apiToken, trustedBuildSystemToken);
SignPathFacade signPathFacade = signPathFacadeFactory.create(credentials);
try(SigningRequestOriginModel originModel = originRetriever.retrieveOrigin()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,21 @@ public void retrieveSecret() throws IOException, SecretNotFoundException {
// ASSERT
assertEquals(secret, result.getPlainText());
}

@Test
public void retrieveSecretNoScopeRestriction() throws IOException, SecretNotFoundException {
String id = Some.stringNonEmpty();
String secret = Some.stringNonEmpty();
CredentialStoreUtils.addCredentials(credentialStore, CredentialsScope.USER, id, secret);

// ACT
CredentialsScope[] emptyScopesSet = { };
Secret result = sut.retrieveSecret(id, emptyScopesSet);

// ASSERT
assertEquals(secret, result.getPlainText());
}

@Test
public void retrieveSecret_withDifferentDomain_works() throws IOException, SecretNotFoundException {
String id = Some.stringNonEmpty();
Expand Down Expand Up @@ -90,7 +104,21 @@ public void retrieveSecret_wrongScope_throws() throws IOException {

// ASSERT
Throwable ex = assertThrows(SecretNotFoundException.class, act);
assertEquals(ex.getMessage(), String.format("The secret '%s' was configured with scope 'Global (Jenkins, nodes, items, all child items, etc)' but needs to be in scope 'System (Jenkins and nodes only)'.", id));
assertEquals(ex.getMessage(), String.format("The secret '%s' was configured with scope 'Global (Jenkins, nodes, items, all child items, etc)' but needs to be in scope(s) 'System (Jenkins and nodes only)'.", id));
}

@Test
public void retrieveSecret_wrongScope_multiple_scopes_throws() throws IOException {
String id = Some.stringNonEmpty();
String secret = Some.stringNonEmpty();
CredentialStoreUtils.addCredentials(credentialStore, CredentialsScope.GLOBAL, id, secret);

// ACT
ThrowingRunnable act = () -> sut.retrieveSecret(id, new CredentialsScope[] { CredentialsScope.SYSTEM, CredentialsScope.USER, });

// ASSERT
Throwable ex = assertThrows(SecretNotFoundException.class, act);
assertEquals(ex.getMessage(), String.format("The secret '%s' was configured with scope 'Global (Jenkins, nodes, items, all child items, etc)' but needs to be in scope(s) 'System (Jenkins and nodes only)' or 'User'.", id));
}

@Test
Expand All @@ -104,7 +132,7 @@ public void retrieveSecret_nullScope_throws() throws IOException {

// ASSERT
Throwable ex = assertThrows(SecretNotFoundException.class, act);
assertEquals(ex.getMessage(), String.format("The secret '%s' was configured with scope '<null>' but needs to be in scope 'System (Jenkins and nodes only)'.", id));
assertEquals(ex.getMessage(), String.format("The secret '%s' was configured with scope '<null>' but needs to be in scope(s) 'System (Jenkins and nodes only)'.", id));
}

@Test
Expand Down

0 comments on commit ea5a2c9

Please sign in to comment.