From ab23ec09f92346fc727a484ff72aa0935629a4dc Mon Sep 17 00:00:00 2001 From: Volodymyr Bobko Date: Mon, 24 Jun 2024 12:41:38 +0200 Subject: [PATCH 1/4] SIGN-6984 - allow to store Api Token in CredentialsScope.GLOBAL additionally to CredentialsScope.SYSTEM --- .../GetSignedArtifactStepExecution.java | 3 ++- .../CredentialBasedSecretRetriever.java | 23 +++++++++++++++---- .../SecretRetrieval/SecretRetriever.java | 3 +++ .../SubmitSigningRequestStepExecution.java | 3 ++- .../CredentialBasedSecretRetrieverTest.java | 20 +++++++++++++--- 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/signpath/GetSignedArtifactStepExecution.java b/src/main/java/io/jenkins/plugins/signpath/GetSignedArtifactStepExecution.java index 99b3463..cde981d 100644 --- a/src/main/java/io/jenkins/plugins/signpath/GetSignedArtifactStepExecution.java +++ b/src/main/java/io/jenkins/plugins/signpath/GetSignedArtifactStepExecution.java @@ -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; @@ -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())) { diff --git a/src/main/java/io/jenkins/plugins/signpath/SecretRetrieval/CredentialBasedSecretRetriever.java b/src/main/java/io/jenkins/plugins/signpath/SecretRetrieval/CredentialBasedSecretRetriever.java index 84f80be..bc5bea7 100644 --- a/src/main/java/io/jenkins/plugins/signpath/SecretRetrieval/CredentialBasedSecretRetriever.java +++ b/src/main/java/io/jenkins/plugins/signpath/SecretRetrieval/CredentialBasedSecretRetriever.java @@ -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 @@ -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 credentials = // authentication: null => SYSTEM, but with no warnings for using deprecated fields CredentialsProvider.lookupCredentials(StringCredentials.class, jenkins, null, Collections.emptyList()); @@ -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 ? "" : scope.getDisplayName(); + CredentialsScope credentialScope = credential.getScope(); + + if (allowedScopes.length > 0 && !Arrays.asList(allowedScopes).contains(credentialScope)) { + String scopeName = credentialScope == 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(); diff --git a/src/main/java/io/jenkins/plugins/signpath/SecretRetrieval/SecretRetriever.java b/src/main/java/io/jenkins/plugins/signpath/SecretRetrieval/SecretRetriever.java index 499fafa..ef6642f 100644 --- a/src/main/java/io/jenkins/plugins/signpath/SecretRetrieval/SecretRetriever.java +++ b/src/main/java/io/jenkins/plugins/signpath/SecretRetrieval/SecretRetriever.java @@ -2,6 +2,7 @@ import hudson.util.Secret; import io.jenkins.plugins.signpath.Exceptions.SecretNotFoundException; +import com.cloudbees.plugins.credentials.CredentialsScope; /** * Allows retrieving secrets @@ -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; } diff --git a/src/main/java/io/jenkins/plugins/signpath/SubmitSigningRequestStepExecution.java b/src/main/java/io/jenkins/plugins/signpath/SubmitSigningRequestStepExecution.java index 5be601d..3f0a84c 100644 --- a/src/main/java/io/jenkins/plugins/signpath/SubmitSigningRequestStepExecution.java +++ b/src/main/java/io/jenkins/plugins/signpath/SubmitSigningRequestStepExecution.java @@ -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; @@ -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()) { diff --git a/src/test/java/io/jenkins/plugins/signpath/SecretRetrieval/CredentialBasedSecretRetrieverTest.java b/src/test/java/io/jenkins/plugins/signpath/SecretRetrieval/CredentialBasedSecretRetrieverTest.java index ce39003..c263284 100644 --- a/src/test/java/io/jenkins/plugins/signpath/SecretRetrieval/CredentialBasedSecretRetrieverTest.java +++ b/src/test/java/io/jenkins/plugins/signpath/SecretRetrieval/CredentialBasedSecretRetrieverTest.java @@ -49,7 +49,7 @@ public void retrieveSecret() throws IOException, SecretNotFoundException { // ASSERT assertEquals(secret, result.getPlainText()); } - + @Test public void retrieveSecret_withDifferentDomain_works() throws IOException, SecretNotFoundException { String id = Some.stringNonEmpty(); @@ -90,7 +90,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 @@ -104,7 +118,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 '' but needs to be in scope 'System (Jenkins and nodes only)'.", id)); + assertEquals(ex.getMessage(), String.format("The secret '%s' was configured with scope '' but needs to be in scope(s) 'System (Jenkins and nodes only)'.", id)); } @Test From df0d4f822661b0612b56447cbcbd78761a82917b Mon Sep 17 00:00:00 2001 From: Volodymyr Bobko Date: Mon, 24 Jun 2024 12:50:34 +0200 Subject: [PATCH 2/4] updates documentation --- README.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4dfb19b..06e03e6 100644 --- a/README.md +++ b/README.md @@ -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 System 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._ @@ -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}", @@ -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}", @@ -94,7 +88,6 @@ stage('Download Signed Artifact') { } steps{ getSignedArtifact( - apiToken: "${API_TOKEN}", organizationId: "${ORGANIZATION_ID}", signingRequestId: "${signingRequestId}", outputArtifactPath: "build-output/my-artifact.exe" @@ -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 From 41d7657da94e2130cda4f9439ef6a7322a7de00a Mon Sep 17 00:00:00 2001 From: volbobvol <54706319+volbobvol@users.noreply.github.com> Date: Mon, 24 Jun 2024 19:50:49 +0200 Subject: [PATCH 3/4] Update README.md Updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06e03e6..1084dc8 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ On SignPath.io: On Jenkins: 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 System Credential so that it is available to the build pipelines of the respective projects (default id `SignPath.ApiToken`) +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._ From f89e4836838b80766accd45a11f3217edb0bf5f5 Mon Sep 17 00:00:00 2001 From: Volodymyr Bobko Date: Tue, 25 Jun 2024 15:44:16 +0200 Subject: [PATCH 4/4] SIGN-6984 extending unit tests --- .../CredentialBasedSecretRetrieverTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/io/jenkins/plugins/signpath/SecretRetrieval/CredentialBasedSecretRetrieverTest.java b/src/test/java/io/jenkins/plugins/signpath/SecretRetrieval/CredentialBasedSecretRetrieverTest.java index c263284..2a36137 100644 --- a/src/test/java/io/jenkins/plugins/signpath/SecretRetrieval/CredentialBasedSecretRetrieverTest.java +++ b/src/test/java/io/jenkins/plugins/signpath/SecretRetrieval/CredentialBasedSecretRetrieverTest.java @@ -50,6 +50,20 @@ public void retrieveSecret() throws IOException, SecretNotFoundException { 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();