diff --git a/docs/README.md b/docs/README.md
index f9e42f4c..c96a9ea6 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -169,7 +169,7 @@ node {
### SSH User Private Key
-An SSH *private key*, with a *username*.
+An SSH *Private Key*, with a *Username*.
- Value: *private key*
- Tags:
@@ -293,6 +293,80 @@ node {
}
```
+#### Github App Credentials (Optional)
+
+Requires *git-source-branch* plugin to create this credential type
+
+A github *private key*, with a *github app id*.
+
+- Value: *content*
+- Tags:
+ - `jenkins:credentials:type` = `githubApp`
+ - `jenkins:credentials:appid` = *Github App Id*
+
+The private key format used is PKCS#8 (starts with `-----BEGIN PRIVATE KEY-----`).
+
+##### Example
+
+AWS CLI:
+
+```bash
+openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in pkcs1.key -out pkcs8.key
+aws secretsmanager create-secret --name 'githubapp' --secret-string 'file://pkcs8.key' --tags 'Key=jenkins:credentials:type,Value=githubApp' 'Key=jenkins:credentials:appid,Value=11111' --description 'Github App Credentials'
+```
+
+Declarative Pipeline:
+
+```groovy
+pipeline {
+ agent any
+ environment {
+ GITHUB_APP = credentials('githubapp')
+ }
+ stages {
+ stage('Example') {
+ steps {
+ echo 'Hello world'
+ }
+ }
+ }
+}
+```
+
+Scripted Pipeline:
+
+```groovy
+node {
+ withCredentials([usernamePassword(credentialsId: 'githubapp', usernameVariable: 'GITHUBAPP_USR', passwordVariable: 'GITHUBAPP_PSW')]) {
+ echo 'Hello world'
+ }
+}
+```
+
+### SecretSource
+
+The plugin allows JCasC to interpolate string secrets from Secrets Manager.
+
+#### Example
+
+AWS CLI:
+
+```bash
+aws secretsmanager create-secret --name 'my-password' --secret-string 'abc123' --description 'Jenkins user password'
+```
+
+JCasC:
+
+```yaml
+jenkins:
+ securityRealm:
+ local:
+ allowsSignup: false
+ users:
+ - id: "foo"
+ password: "${my-password}"
+```
+
## Advanced Usage
You may need to deal with multi-field credentials or vendor-specific credential types that the plugin does not (yet) support.
diff --git a/pom.xml b/pom.xml
index 7d67027e..be8dd77b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -130,6 +130,18 @@
1.21
test
+
+ org.jmockit
+ jmockit
+ 1.41
+ test
+
+
+ org.jenkins-ci.plugins
+ github-branch-source
+ 2.9.3
+ true
+
@@ -146,6 +158,11 @@
+
+
+ -javaagent:"${settings.localRepository}"/org/jmockit/jmockit/1.41/jmockit-1.41.jar
+
+
org.apache.maven.plugins
diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/CredentialsFactory.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/CredentialsFactory.java
index 7dd01198..df6488a5 100644
--- a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/CredentialsFactory.java
+++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/CredentialsFactory.java
@@ -12,6 +12,7 @@
import io.jenkins.plugins.credentials.secretsmanager.Messages;
import io.jenkins.plugins.credentials.secretsmanager.factory.certificate.AwsCertificateCredentials;
import io.jenkins.plugins.credentials.secretsmanager.factory.file.AwsFileCredentials;
+import io.jenkins.plugins.credentials.secretsmanager.factory.git_app.GitCredentialFactory;
import io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key.AwsSshUserPrivateKey;
import io.jenkins.plugins.credentials.secretsmanager.factory.string.AwsStringCredentials;
import io.jenkins.plugins.credentials.secretsmanager.factory.username_password.AwsUsernamePasswordCredentials;
@@ -40,6 +41,7 @@ public static Optional create(String arn, String name, Stri
final String type = tags.getOrDefault(Tags.type, "");
final String username = tags.getOrDefault(Tags.username, "");
final String filename = tags.getOrDefault(Tags.filename, name);
+ final String appId = tags.getOrDefault(Tags.appid, "");
switch (type) {
case Type.string:
@@ -52,6 +54,8 @@ public static Optional create(String arn, String name, Stri
return Optional.of(new AwsCertificateCredentials(name, description, new SecretBytesSupplier(client, arn)));
case Type.file:
return Optional.of(new AwsFileCredentials(name, description, filename, new SecretBytesSupplier(client, arn)));
+ case Type.githubApp:
+ return GitCredentialFactory.createCredential(name, description, appId, new StringSupplier(client, name));
default:
return Optional.empty();
}
diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Tags.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Tags.java
index 202e739a..bae8fac2 100644
--- a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Tags.java
+++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Tags.java
@@ -9,6 +9,7 @@ public abstract class Tags {
public static final String filename = namespace + "filename";
public static final String type = namespace + "type";
public static final String username = namespace + "username";
+ public static final String appid = namespace + "appid";
private Tags() {
diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Type.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Type.java
index 75322c32..e461fc39 100644
--- a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Type.java
+++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Type.java
@@ -9,6 +9,7 @@ public abstract class Type {
public static final String usernamePassword = "usernamePassword";
public static final String sshUserPrivateKey = "sshUserPrivateKey";
public static final String string = "string";
+ public static final String githubApp = "githubApp";
private Type() {
diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/git_app/GitCredentialFactory.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/git_app/GitCredentialFactory.java
new file mode 100644
index 00000000..c24b6df2
--- /dev/null
+++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/git_app/GitCredentialFactory.java
@@ -0,0 +1,29 @@
+package io.jenkins.plugins.credentials.secretsmanager.factory.git_app;
+
+import com.cloudbees.plugins.credentials.CredentialsScope;
+import com.cloudbees.plugins.credentials.common.StandardCredentials;
+import hudson.Extension;
+import hudson.util.Secret;
+import io.jenkins.plugins.credentials.secretsmanager.factory.Type;
+import jenkins.model.Jenkins;
+import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials;
+
+import java.util.function.Supplier;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+@Extension(optional = true)
+public class GitCredentialFactory {
+ private static final Logger LOG = Logger.getLogger(GitCredentialFactory.class.getName());
+
+ public static Optional createCredential(String name, String description, String appId, Supplier privateKey) {
+ if (Jenkins.get().getPlugin("github-branch-source") == null) {
+ LOG.warning("Plugin not installed: github-branch-source. Cannot create type: " + Type.githubApp);
+ return Optional.empty();
+ }
+
+ Secret secret = Secret.fromString(privateKey.get());
+
+ return Optional.of(new GitHubAppCredentials(CredentialsScope.GLOBAL, name, description, appId, secret));
+ }
+}
diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/GithubAppCredentialsIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/GithubAppCredentialsIT.java
new file mode 100644
index 00000000..1af6ca5b
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/GithubAppCredentialsIT.java
@@ -0,0 +1,227 @@
+package io.jenkins.plugins.credentials.secretsmanager;
+
+import com.amazonaws.services.secretsmanager.model.*;
+import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
+import com.cloudbees.plugins.credentials.CredentialsScope;
+import com.cloudbees.plugins.credentials.common.StandardCredentials;
+import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
+import hudson.util.ListBoxModel;
+import hudson.util.Secret;
+import io.jenkins.plugins.casc.misc.ConfiguredWithCode;
+import io.jenkins.plugins.credentials.secretsmanager.factory.Type;
+import io.jenkins.plugins.credentials.secretsmanager.util.*;
+import mockit.Mock;
+import mockit.MockUp;
+import mockit.integration.junit4.JMockit;
+import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials;
+import org.jenkinsci.plugins.workflow.job.WorkflowRun;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+import java.util.*;
+
+import static io.jenkins.plugins.credentials.secretsmanager.util.assertions.CustomAssertions.assertThat;
+import static org.junit.Assume.assumeTrue;
+
+@RunWith(JMockit.class)
+public class GithubAppCredentialsIT implements CredentialsTests {
+
+ private static final String APP_ID = "11111";
+ private static final String PRIVATE_KEY = Crypto.newPrivateKey()
+ .replace("-----BEGIN RSA PRIVATE KEY-----", "")
+ .replace("-----END RSA PRIVATE KEY-----", "");;
+
+ public final MyJenkinsConfiguredWithCodeRule jenkins = new MyJenkinsConfiguredWithCodeRule();
+ public final AWSSecretsManagerRule secretsManager = new AWSSecretsManagerRule();
+
+ @Rule
+ public final TestRule chain = Rules.jenkinsWithSecretsManager(jenkins, secretsManager);
+
+ @BeforeClass
+ public static void checkClassExists() {
+ Optional clazz = getGithubAppCredentialClass();
+ assumeTrue(clazz.isPresent());
+ }
+
+ @Test
+ @ConfiguredWithCode(value = "/integration.yml")
+ public void shouldSupportListView() {
+ // Given
+ final CreateSecretResult foo = createGitHubAppCredentialSecret(APP_ID, PRIVATE_KEY);
+
+ // When
+ final ListBoxModel list = jenkins.getCredentials().list(StandardCredentials.class);
+
+ // Then
+ assertThat(list)
+ .containsOption(foo.getName(), foo.getName());
+ }
+
+ @Test
+ @ConfiguredWithCode(value = "/integration.yml")
+ public void shouldHaveDescriptorIcon() {
+ final CreateSecretResult foo = createGitHubAppCredentialSecret(APP_ID, PRIVATE_KEY);
+ final GitHubAppCredentials ours = lookup(GitHubAppCredentials.class, foo.getName());
+
+ final GitHubAppCredentials theirs = new GitHubAppCredentials(CredentialsScope.GLOBAL, "name", "description", "11111", Secret.fromString("secret"));
+
+ assertThat(ours)
+ .hasSameDescriptorIconAs(theirs);
+ }
+
+ @Test
+ @ConfiguredWithCode(value = "/integration.yml")
+ public void shouldSupportWithCredentialsBinding() {
+ // Given
+ final CreateSecretResult foo = createGitHubAppCredentialSecret(APP_ID, PRIVATE_KEY);
+
+ new MockUp() {
+ @Mock
+ public Secret getPassword() {
+ return Secret.fromString("test-token");
+ }
+ };
+
+ // When
+ final WorkflowRun run = runPipeline("",
+ "withCredentials([usernamePassword(credentialsId: '" + foo.getName() + "', usernameVariable: 'USR', passwordVariable: 'PSW')]) {",
+ " echo \"Credential: {username: $USR, password: $PSW}\"",
+ "}");
+
+ // Then
+ assertThat(run)
+ .hasResult(hudson.model.Result.SUCCESS)
+ .hasLogContaining("Credential: {username: ****, password: ****}");
+ }
+
+ @Test
+ @ConfiguredWithCode(value = "/integration.yml")
+ public void shouldSupportEnvironmentBinding(){
+ // Given
+ final CreateSecretResult foo = createGitHubAppCredentialSecret(APP_ID, PRIVATE_KEY);
+
+ new MockUp() {
+ @Mock
+ public Secret getPassword() {
+ return Secret.fromString("test-token");
+ }
+ };
+
+ // When
+ final WorkflowRun run = runPipeline("",
+ "pipeline {",
+ " agent none",
+ " stages {",
+ " stage('Example') {",
+ " environment {",
+ " FOO = credentials('" + foo.getName() + "')",
+ " }",
+ " steps {",
+ " echo \"{variable: $FOO, username: $FOO_USR, password: $FOO_PSW}\"",
+ " }",
+ " }",
+ " }",
+ "}");
+
+ // Then
+ assertThat(run)
+ .hasResult(hudson.model.Result.SUCCESS)
+ .hasLogContaining("{variable: ****, username: ****, password: ****}");
+ }
+
+ @Test
+ @ConfiguredWithCode(value = "/integration.yml")
+ public void shouldSupportSnapshots() {
+
+ // Given
+ final CreateSecretResult foo = createGitHubAppCredentialSecret(APP_ID, PRIVATE_KEY);
+ final GitHubAppCredentials before = jenkins.getCredentials().lookup(GitHubAppCredentials.class, foo.getName());
+
+ // When
+ final GitHubAppCredentials after = CredentialSnapshots.snapshot(before);
+
+ // Then
+ assertThat(after)
+ .hasUsername(before.getUsername())
+ .hasPrivateKey(before.getPrivateKey())
+ .hasId(before.getId());
+ }
+
+ @Test
+ @ConfiguredWithCode(value = "/integration.yml")
+ public void shouldHaveId() {
+ // Given
+ final CreateSecretResult foo = createGitHubAppCredentialSecret(APP_ID, PRIVATE_KEY);
+
+ // When
+ final GitHubAppCredentials credential =
+ jenkins.getCredentials().lookup(GitHubAppCredentials.class, foo.getName());
+
+ // Then
+ assertThat(credential)
+ .hasId(foo.getName());
+ }
+
+ @Test
+ @ConfiguredWithCode(value = "/integration.yml")
+ public void shouldHaveUsername() {
+ // Given
+ final CreateSecretResult foo = createGitHubAppCredentialSecret(APP_ID, PRIVATE_KEY);
+
+ // When
+ final StandardUsernamePasswordCredentials credential =
+ jenkins.getCredentials().lookup(GitHubAppCredentials.class, foo.getName());
+
+ // Then
+ assertThat(credential)
+ .hasUsername(APP_ID);
+ }
+
+ @Test
+ @ConfiguredWithCode(value = "/integration.yml")
+ public void shouldHavePrivateKey() {
+ // Given
+ final CreateSecretResult foo = createGitHubAppCredentialSecret(APP_ID, PRIVATE_KEY);
+
+ // When
+ final GitHubAppCredentials credential = lookup(GitHubAppCredentials.class, foo.getName());
+
+ // Then
+ assertThat(credential)
+ .hasPrivateKey(PRIVATE_KEY);
+ }
+
+ private CreateSecretResult createGitHubAppCredentialSecret(String appId, String privateKey) {
+ final List tags = Lists.of(
+ AwsTags.type(Type.githubApp),
+ AwsTags.appid(appId));
+
+ final CreateSecretRequest request = new CreateSecretRequest()
+ .withName(CredentialNames.random())
+ .withSecretString(privateKey)
+ .withTags(tags);
+
+ return secretsManager.getClient().createSecret(request);
+ }
+
+ private WorkflowRun runPipeline(String... pipeline) {
+ return jenkins.getPipelines().run(Strings.m(pipeline));
+ }
+
+ private C lookup(Class type, String id) {
+ return jenkins.getCredentials().lookup(type, id);
+ }
+
+ private static Optional getGithubAppCredentialClass() {
+ Class githubCredentials;
+ try {
+ githubCredentials = Class.forName("org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials");
+ } catch (Throwable ex) {
+ return Optional.empty();
+ }
+ return Optional.of(githubCredentials);
+ }
+}
diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AwsTags.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AwsTags.java
index 7f5e1db7..7cf1b960 100644
--- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AwsTags.java
+++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AwsTags.java
@@ -19,6 +19,10 @@ public static Tag username(String username) {
return AwsTags.tag(Tags.username, username);
}
+ public static Tag appid(String id) {
+ return AwsTags.tag(Tags.appid, id);
+ }
+
public static Tag type(String type) {
return tag(Tags.type, type);
}
diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/PagedIterableImpl.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/PagedIterableImpl.java
new file mode 100644
index 00000000..ad8dcbae
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/PagedIterableImpl.java
@@ -0,0 +1,27 @@
+package io.jenkins.plugins.credentials.secretsmanager.util;
+
+import org.kohsuke.github.*;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.function.Consumer;
+
+public class PagedIterableImpl extends PagedIterable {
+
+ private final Class receiverType;
+ private final Consumer itemInitializer;
+ private final Iterator iterator;
+
+ public PagedIterableImpl(Class receiverType, Consumer itemInitializer, Iterator iterator) {
+ this.receiverType = receiverType;
+ this.itemInitializer = itemInitializer;
+ this.iterator = iterator;
+ }
+
+ @Nonnull
+ public PagedIterator _iterator(int pageSize) {
+ return null;
+ }
+
+}
diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/CustomAssertions.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/CustomAssertions.java
index 63a92396..83ff4bbd 100644
--- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/CustomAssertions.java
+++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/CustomAssertions.java
@@ -5,6 +5,7 @@
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import hudson.util.ListBoxModel;
+import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials;
import org.jenkinsci.plugins.plaincredentials.FileCredentials;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
@@ -42,6 +43,10 @@ public static SSHUserPrivateKeyAssert assertThat(SSHUserPrivateKey actual) {
return new SSHUserPrivateKeyAssert(actual);
}
+ public static GithubAppCredentialsAssert assertThat(GitHubAppCredentials actual) {
+ return new GithubAppCredentialsAssert(actual);
+ }
+
public static WorkflowRunAssert assertThat(WorkflowRun actual) {
return new WorkflowRunAssert(actual);
}
diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/GithubAppCredentialsAssert.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/GithubAppCredentialsAssert.java
new file mode 100644
index 00000000..3a71e04a
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/GithubAppCredentialsAssert.java
@@ -0,0 +1,62 @@
+package io.jenkins.plugins.credentials.secretsmanager.util.assertions;
+
+import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
+import hudson.util.Secret;
+import org.assertj.core.api.AbstractAssert;
+import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials;
+
+import java.util.Objects;
+
+public class GithubAppCredentialsAssert extends AbstractAssert {
+
+ public GithubAppCredentialsAssert(GitHubAppCredentials actual) {
+ super(actual, GithubAppCredentialsAssert.class);
+ }
+
+ public GithubAppCredentialsAssert hasUsername(String username) {
+ isNotNull();
+
+ if (!Objects.equals(actual.getUsername(), username)) {
+ failWithMessage("Expected username to be <%s> but was <%s>", username, actual.getUsername());
+ }
+
+ return this;
+ }
+
+ public GithubAppCredentialsAssert hasPrivateKey(String privateKey) {
+ return hasPrivateKey(Secret.fromString(privateKey));
+ }
+
+ public GithubAppCredentialsAssert hasPrivateKey(Secret privateKey) {
+ isNotNull();
+
+ if (!Objects.equals(actual.getPrivateKey(), privateKey)) {
+ failWithMessage("Expected private keys to be <%s> but was <%s>", privateKey, actual.getPrivateKey());
+ }
+
+ return this;
+ }
+
+ public GithubAppCredentialsAssert hasAppId(String appId) {
+ isNotNull();
+
+ if (!Objects.equals(actual.getAppID(), appId)) {
+ failWithMessage("Expected App Id to be <%s> but was <%s>", appId, actual.getAppID());
+ }
+
+ return this;
+ }
+
+ public GithubAppCredentialsAssert hasSameDescriptorIconAs(StandardUsernamePasswordCredentials theirs) {
+ new StandardCredentialsAssert(actual).hasSameDescriptorIconAs(theirs);
+
+ return this;
+ }
+
+ public GithubAppCredentialsAssert hasId(String id) {
+ new StandardCredentialsAssert(actual).hasId(id);
+
+ return this;
+ }
+
+}