Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SIGN-6984 - allow to store Api Token in global and system scope #13

Merged
merged 5 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the allowedScope.length > 0 actually necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If allowedScope is empty there is no scope restriction

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