From a277a71a9f75eed0a37f04cd1f35172f5696d13e Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Tue, 5 Nov 2019 16:53:44 +0000
Subject: [PATCH 01/18] Add support for GitHub app authentication
---
pom.xml | 20 +++
.../github_branch_source/Connector.java | 69 +++++++++-
.../GitHubSCMNavigator.java | 4 +-
.../InvalidPrivateKeyException.java | 8 ++
.../github_branch_source/JwtHelper.java | 82 +++++++++++
.../help-credentialsId.html | 15 +-
.../GitHubSCMSource/help-credentialsId.html | 11 ++
.../github_branch_source/JwtHelperTest.java | 128 ++++++++++++++++++
8 files changed, 324 insertions(+), 13 deletions(-)
create mode 100644 src/main/java/org/jenkinsci/plugins/github_branch_source/InvalidPrivateKeyException.java
create mode 100644 src/main/java/org/jenkinsci/plugins/github_branch_source/JwtHelper.java
create mode 100644 src/test/java/org/jenkinsci/plugins/github_branch_source/JwtHelperTest.java
diff --git a/pom.xml b/pom.xml
index dbbab0a0f..ceb9db148 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,6 +30,7 @@
2.2
true
1.35
+ 0.10.5
@@ -79,6 +80,25 @@
display-url-api
2.0
+
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jjwt.version}
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jjwt.version}
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jjwt.version}
+ runtime
+
+
org.jenkins-ci.plugins
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
index 99b681fa2..9b0be9f1f 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
@@ -25,6 +25,7 @@
package org.jenkinsci.plugins.github_branch_source;
import com.cloudbees.jenkins.GitHubWebHook;
+import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
import com.cloudbees.plugins.credentials.CredentialsMatcher;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsNameProvider;
@@ -78,13 +79,19 @@
import org.jenkinsci.plugins.github.config.GitHubServerConfig;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.github.GHApp;
+import org.kohsuke.github.GHAppInstallation;
+import org.kohsuke.github.GHAppInstallationToken;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.kohsuke.github.HttpConnector;
import org.kohsuke.github.RateLimitHandler;
import org.kohsuke.github.extras.OkHttpConnector;
+import static com.cloudbees.plugins.credentials.CredentialsMatchers.anyOf;
+import static com.cloudbees.plugins.credentials.CredentialsMatchers.instanceOf;
import static java.util.logging.Level.FINE;
+import static org.jenkinsci.plugins.github_branch_source.JwtHelper.createJWT;
/**
* Utilities that could perhaps be moved into {@code github-api}.
@@ -106,6 +113,7 @@ protected boolean removeEldestEntry(Map.Entry eldest) {
};
private static final Random ENTROPY = new Random();
private static final String SALT = Long.toHexString(ENTROPY.nextLong());
+ private static final String ERROR_AUTHENTICATING_GITHUB_APP = "Couldn't find GitHub app installation %s";
private Connector() {
throw new IllegalAccessError("Utility class");
@@ -198,13 +206,21 @@ public static FormValidation checkScanCredentials(@CheckForNull Item context, St
GitHub connector = Connector.connect(apiUri, credentials);
try {
try {
- return FormValidation.ok("User %s", connector.getMyself().getLogin());
- } catch (IOException e){
- return FormValidation.error("Invalid credentials");
+ boolean githubAppAuthentication = credentials instanceof SSHUserPrivateKey;
+ if (githubAppAuthentication) {
+ int remaining = connector.getRateLimit().getRemaining();
+ return FormValidation.ok("GHApp verified, remaining rate limit: %d", remaining);
+ }
+
+ return FormValidation.ok("User %s", connector.isCredentialValid());
+ } catch (Exception e) {
+ return FormValidation.error("Invalid credentials: %s", e.getMessage());
}
} finally {
Connector.release(connector);
}
+ } catch (IllegalArgumentException | InvalidPrivateKeyException e) {
+ return FormValidation.error(e.getMessage());
} catch (IOException e) {
// ignore, never thrown
LOGGER.log(Level.WARNING, "Exception validating credentials {0} on {1}", new Object[]{
@@ -309,6 +325,9 @@ public static void checkApiUrlValidity(@Nonnull GitHub gitHub, @CheckForNull Sta
} else if (credentials instanceof StandardUsernamePasswordCredentials) {
StandardUsernamePasswordCredentials c = (StandardUsernamePasswordCredentials) credentials;
hash = Util.getDigestOf(c.getPassword().getPlainText() + SALT);
+ } else if (credentials instanceof SSHUserPrivateKey) {
+ SSHUserPrivateKey c = (SSHUserPrivateKey) credentials;
+ hash = Util.getDigestOf(c.getPrivateKeys().get(0) + SALT);
} else {
// TODO OAuth support
throw new IOException("Unsupported credential type: " + credentials.getClass().getName());
@@ -331,6 +350,7 @@ public static void checkApiUrlValidity(@Nonnull GitHub gitHub, @CheckForNull Sta
String password;
String hash;
String authHash;
+ boolean githubApp = false;
Jenkins jenkins = Jenkins.get();
if (credentials == null) {
username = null;
@@ -343,6 +363,14 @@ public static void checkApiUrlValidity(@Nonnull GitHub gitHub, @CheckForNull Sta
password = c.getPassword().getPlainText();
hash = Util.getDigestOf(password + SALT); // want to ensure pooling by credential
authHash = Util.getDigestOf(password + "::" + jenkins.getLegacyInstanceId());
+ } else if (credentials instanceof SSHUserPrivateKey) {
+ SSHUserPrivateKey c = (SSHUserPrivateKey) credentials;
+ username = c.getUsername();
+ password = c.getPrivateKeys().get(0);
+ hash = Util.getDigestOf(password + SALT);
+ authHash = Util.getDigestOf(password + "::" + jenkins.getLegacyInstanceId());
+
+ githubApp = true;
} else {
// TODO OAuth support
throw new IOException("Unsupported credential type: " + credentials.getClass().getName());
@@ -394,8 +422,10 @@ public static void checkApiUrlValidity(@Nonnull GitHub gitHub, @CheckForNull Sta
gb.withConnector(new OkHttpConnector(new OkUrlFactory(client)));
- if (username != null) {
+ if (username != null && !githubApp) {
gb.withPassword(username, password);
+ } else if (username != null) {
+ gb.withOAuthToken(generateAppInstallationToken(username, password), "");
}
hub = gb.build();
@@ -406,6 +436,29 @@ public static void checkApiUrlValidity(@Nonnull GitHub gitHub, @CheckForNull Sta
}
}
+ @SuppressWarnings("deprecation") // preview features are required for GitHub app integration, GitHub api adds deprecated to all preview methods
+ private static String generateAppInstallationToken(String appId, String appPrivateKey) {
+ try {
+ String jwtToken = createJWT(appId, appPrivateKey);
+ GitHub gitHubApp = new GitHubBuilder().withJwtToken(jwtToken).build();
+
+ GHApp app = gitHubApp.getApp();
+
+ List appInstallations = app.listInstallations().asList();
+ if (!appInstallations.isEmpty()) {
+ GHAppInstallation appInstallation = appInstallations.get(0);
+ GHAppInstallationToken appInstallationToken = appInstallation
+ .createToken(appInstallation.getPermissions())
+ .create();
+
+ return appInstallationToken.getToken();
+ }
+ } catch (IOException e) {
+ throw new IllegalArgumentException(String.format(ERROR_AUTHENTICATING_GITHUB_APP, appId), e);
+ }
+ throw new IllegalArgumentException(String.format(ERROR_AUTHENTICATING_GITHUB_APP, appId));
+ }
+
public static void release(@CheckForNull GitHub hub) {
if (hub == null) {
return;
@@ -453,8 +506,10 @@ private static void unused(@Nonnull GitHub hub) {
}
private static CredentialsMatcher githubScanCredentialsMatcher() {
- // TODO OAuth credentials
- return CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class));
+ return anyOf(
+ instanceOf(StandardUsernamePasswordCredentials.class),
+ instanceOf(SSHUserPrivateKey.class)
+ );
}
static List githubDomainRequirements(String apiUri) {
@@ -512,7 +567,7 @@ static boolean isCredentialValid(GitHub gitHub) {
return true;
} else {
try {
- gitHub.getMyself();
+ gitHub.getRateLimit();
return true;
} catch (IOException e) {
if (LOGGER.isLoggable(FINE)) {
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java
index e4b2e1ea3..122424d13 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java
@@ -25,6 +25,7 @@
package org.jenkinsci.plugins.github_branch_source;
import com.cloudbees.jenkins.GitHubWebHook;
+import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
import com.cloudbees.plugins.credentials.CredentialsNameProvider;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
@@ -933,7 +934,8 @@ public void visitSources(SCMSourceObserver observer) throws IOException, Interru
SourceFactory sourceFactory = new SourceFactory(request);
WitnessImpl witness = new WitnessImpl(listener);
- if (!github.isAnonymous()) {
+ boolean githubAppAuthentication = credentials instanceof SSHUserPrivateKey;
+ if (!github.isAnonymous() && !githubAppAuthentication) {
GHMyself myself;
try {
// Requires an authenticated access
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/InvalidPrivateKeyException.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/InvalidPrivateKeyException.java
new file mode 100644
index 000000000..cc1f8d65f
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/InvalidPrivateKeyException.java
@@ -0,0 +1,8 @@
+package org.jenkinsci.plugins.github_branch_source;
+
+public class InvalidPrivateKeyException extends RuntimeException {
+
+ public InvalidPrivateKeyException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/JwtHelper.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/JwtHelper.java
new file mode 100644
index 000000000..6201d415a
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/JwtHelper.java
@@ -0,0 +1,82 @@
+package org.jenkinsci.plugins.github_branch_source;
+
+import io.jsonwebtoken.JwtBuilder;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Base64;
+import java.util.Date;
+import java.util.Objects;
+
+import static java.util.Objects.requireNonNull;
+
+class JwtHelper {
+
+ /**
+ * Create a JWT for authenticating to GitHub as an app installation
+ * @param githubAppId the app ID
+ * @param privateKey PKC#8 formatted private key
+ * @return JWT for authenticating to GitHub
+ */
+ static String createJWT(String githubAppId, final String privateKey) {
+ requireNonNull(githubAppId, privateKey);
+
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RS256;
+
+ long nowMillis = System.currentTimeMillis();
+ Date now = new Date(nowMillis);
+
+ Key signingKey;
+ try {
+ signingKey = getPrivateKeyFromString(privateKey);
+ } catch (GeneralSecurityException e) {
+ throw new IllegalArgumentException("Couldn't parse private key for GitHub app, make sure it's PKCS#8 format", e);
+ }
+
+ JwtBuilder builder = Jwts.builder()
+ .setIssuedAt(now)
+ .setIssuer(githubAppId)
+ .signWith(signingKey, signatureAlgorithm);
+
+ long expMillis = nowMillis + (60 * 1000 * 10);
+ Date exp = new Date(expMillis);
+ builder.setExpiration(exp);
+
+ return builder.compact();
+ }
+
+ /**
+ * Convert a PKCS#8 formatted private key in string format into a java PrivateKey
+ * @param key PCKS#8 string
+ * @return private key
+ * @throws GeneralSecurityException if we couldn't parse the string
+ */
+ private static PrivateKey getPrivateKeyFromString(final String key) throws GeneralSecurityException {
+ if (key.contains("RSA")) {
+ throw new InvalidPrivateKeyException(
+ "Private key must be a PKCS#8 formatted string, to convert it from PKCS#1 use: "
+ + "openssl pkcs8 -topk8 -inform PEM -outform PEM -in current-key.pem -out new-key.pem -nocrypt"
+ );
+ }
+
+ String privateKeyContent = key.replaceAll("\\n", "")
+ .replace("-----BEGIN PRIVATE KEY-----", "")
+ .replace("-----END PRIVATE KEY-----", "");
+
+ KeyFactory kf = KeyFactory.getInstance("RSA");
+
+ try {
+ byte[] decode = Base64.getDecoder().decode(privateKeyContent);
+ PKCS8EncodedKeySpec keySpecPKCS8 = new PKCS8EncodedKeySpec(decode);
+
+ return kf.generatePrivate(keySpecPKCS8);
+ } catch (IllegalArgumentException e) {
+ throw new InvalidPrivateKeyException("Failed to decode private key: " + e.getMessage());
+ }
+ }
+
+}
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-credentialsId.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-credentialsId.html
index a77b93fbd..322f0a622 100644
--- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-credentialsId.html
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-credentialsId.html
@@ -3,11 +3,16 @@
Credentials used to scan branches and pull requests,
check out sources and mark commit statuses.
-
- Note that only "username with password" credentials are supported.
- Existing credentials of other kinds will be filtered out. This is because Jenkins
- uses the GitHub API, which does not support other ways of authentication.
-
+ The following credential types are supported:
+
+ -
+ "Username with password"
+
+ -
+ "SSH Username with private key" - this is for authenticating as a GitHub app.
+ The username should be the 'GitHub app ID'.
+
+
If none is given, only the public repositories will be scanned, and commit status
will not be set on GitHub.
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-credentialsId.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-credentialsId.html
index a77b93fbd..df36ebcba 100644
--- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-credentialsId.html
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-credentialsId.html
@@ -3,6 +3,17 @@
Credentials used to scan branches and pull requests,
check out sources and mark commit statuses.
+ The following credential types are supported:
+
+ -
+ "Username with password"
+
+ -
+ "SSH Username with private key" - this is for authenticating as a GitHub app.
+ The username should be the 'GitHub app ID'.
+
+
+
Note that only "username with password" credentials are supported.
Existing credentials of other kinds will be filtered out. This is because Jenkins
diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/JwtHelperTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/JwtHelperTest.java
new file mode 100644
index 000000000..139ec5327
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/JwtHelperTest.java
@@ -0,0 +1,128 @@
+package org.jenkinsci.plugins.github_branch_source;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.Jwts;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.jenkinsci.plugins.github_branch_source.JwtHelper.createJWT;
+import static org.mockito.ArgumentMatchers.contains;
+
+public class JwtHelperTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ // https://stackoverflow.com/a/22176759/4951015
+ private static final String PKCS8_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n" +
+ "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD7vHsVwyDV8cj7\n" +
+ "5yR4WWl6rlgf/e5zmeBgtm0PCgnitcSbD5FU33301DPY5a7AtqVBOwEnE14L9XS7\n" +
+ "ov61U+x1m4aQmqR/dPQaA2ayh2cYPszWNQMp42ArDIfg7DhSrvsRJKHsbPXlPjqe\n" +
+ "c0udLqhSLVIO9frNLf+dAsLsgYk8O39PKGb33akGG7tWTe0J+akNQjgbS7vOi8sS\n" +
+ "NLwHIdYfz/Am+6Xmm+J4yVs6+Xt3kOeLdFBkz8H/HGsJq854MbIAK/HuId1MOPS0\n" +
+ "cDWh37tzRsM+q/HZzYRkc5bhNKw/Mj9jN9jD5GH0Lfea0QFedjppf1KvWdcXn+/W\n" +
+ "M7OmyfhvAgMBAAECggEAN96H7reExRbJRWbySCeH6mthMZB46H0hODWklK7krMUs\n" +
+ "okFdPtnvKXQjIaMwGqMuoACJa/O3bq4GP1KYdwPuOdfPkK5RjdwWBOP2We8FKXNe\n" +
+ "oLfZQOWuxT8dtQSYJ3mgTRi1OzSfikY6Wko6YOMnBj36tUlQZVMtJNqlCjphi9Uz\n" +
+ "6EyvRURlDG8sBBbC7ods5B0789qk3iGH/97ia+1QIqXAUaVFg3/BA6wkxkbNG2sN\n" +
+ "tqULgVYTw32Oj/Y/H1Y250RoocTyfsUS3I3aPIlnvcgp2bugWqDyYJ58nDIt3Pku\n" +
+ "fjImWrNz/pNiEs+efnb0QEk7m5hYwxmyXN4KRSv0OQKBgQD+I3Y3iNKSVr6wXjur\n" +
+ "OPp45fxS2sEf5FyFYOn3u760sdJOH9fGlmf9sDozJ8Y8KCaQCN5tSe3OM+XDrmiw\n" +
+ "Cu/oaqJ1+G4RG+6w1RJF+5Nfg6PkUs7eJehUgZ2Tox8Tg1mfVIV8KbMwNi5tXpug\n" +
+ "MVmA2k9xjc4uMd2jSnSj9NAqrQKBgQD9lIO1tY6YKF0Eb0Qi/iLN4UqBdJfnALBR\n" +
+ "MjxYxqqI8G4wZEoZEJJvT1Lm6Q3o577N95SihZoj69tb10vvbEz1pb3df7c1HEku\n" +
+ "LXcyVMvjR/CZ7dOSNgLGAkFfOoPhcF/OjSm4DrGPe3GiBxhwXTBjwJ5TIgEDkVIx\n" +
+ "ZVo5r7gPCwKBgQCOvsZo/Q4hql2jXNqxGuj9PVkUBNFTI4agWEYyox7ECdlxjks5\n" +
+ "vUOd5/1YvG+JXJgEcSbWRh8volDdL7qXnx0P881a6/aO35ybcKK58kvd62gEGEsf\n" +
+ "1jUAOmmTAp2y7SVK7EOp8RY370b2oZxSR0XZrUXQJ3F22wV98ZVAfoLqZQKBgDIr\n" +
+ "PdunbezAn5aPBOX/bZdZ6UmvbZYwVrHZxIKz2214U/STAu3uj2oiQX6ZwTzBDMjn\n" +
+ "IKr+z74nnaCP+eAGhztabTPzXqXNUNUn/Zshl60BwKJToTYeJXJTY+eZRhpGB05w\n" +
+ "Mz7M+Wgvvg2WZcllRnuV0j0UTysLhz1qle0vzLR9AoGBAOukkFFm2RLm9N1P3gI8\n" +
+ "mUadeAlYRZ5o0MvumOHaB5pDOCKhrqAhop2gnM0f5uSlapCtlhj0Js7ZyS3Giezg\n" +
+ "38oqAhAYxy2LMoLD7UtsHXNp0OnZ22djcDwh+Wp2YORm7h71yOM0NsYubGbp+CmT\n" +
+ "Nw9bewRvqjySBlDJ9/aNSeEY\n" +
+ "-----END PRIVATE KEY-----";
+
+ private static final String PKCS8_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n" +
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+7x7FcMg1fHI++ckeFlp\n" +
+ "eq5YH/3uc5ngYLZtDwoJ4rXEmw+RVN999NQz2OWuwLalQTsBJxNeC/V0u6L+tVPs\n" +
+ "dZuGkJqkf3T0GgNmsodnGD7M1jUDKeNgKwyH4Ow4Uq77ESSh7Gz15T46nnNLnS6o\n" +
+ "Ui1SDvX6zS3/nQLC7IGJPDt/Tyhm992pBhu7Vk3tCfmpDUI4G0u7zovLEjS8ByHW\n" +
+ "H8/wJvul5pvieMlbOvl7d5Dni3RQZM/B/xxrCavOeDGyACvx7iHdTDj0tHA1od+7\n" +
+ "c0bDPqvx2c2EZHOW4TSsPzI/YzfYw+Rh9C33mtEBXnY6aX9Sr1nXF5/v1jOzpsn4\n" +
+ "bwIDAQAB\n" +
+ "-----END PUBLIC KEY-----";
+
+
+ private static final String PKCS1_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\n" +
+ "MIIEpAIBAAKCAQEA26y2ZLYaNKHYg1FehH/WmXZ+SXG9ofLCf7+tR0j/BHbQy1Ck\n" +
+ "u6Pqxn10nKPrAZSFakNDKI1vf92+Ny8LFitBucs2JaDSm1kUHjZaoCbp2FQmbr28\n" +
+ "eO+q0oIaJ67WaIF9o1DzCiBBgqCOqZpDdZY1peRPQ7ttBfBvPOi9zEiWplrn2IlL\n" +
+ "tlndlYtV+KHlIy7odaCaSHjzawTBxLe82lpX5+YHy0doNlI5l/epJMtjcE/l2jEj\n" +
+ "xMZxWz4ZAiXd8hLYonUzxaup8IMKm4K8eh++4UcXAs0tjA0CGaieeyQZyBLPwFyf\n" +
+ "k3JStqbBgwaKLzV0D1ayokQNvc0cm4tdgk6gVwIDAQABAoIBAGlZzSdDhhHTxIhF\n" +
+ "z7RvsrVqdGo4mB9A0zJ89FcJlPPJH51CEZ7Dn+aNaA1vN1dMqScrFtwt6FlEOOMy\n" +
+ "NnjtSdoWsOMe26IQ+Gr82j2QK/nJcZ0OdYLyPdQy/OQnH0CDSYO3YLdsfL5uzbxc\n" +
+ "9RlBbn0enzz2d/SvOEnXvJ5p+YXRk3Y8Toccu66nPUKkeWDzZ3Ql/mf2Piw1VwvF\n" +
+ "/5pvZRiH5Lh5MCc7AxHlDFXRq5jQKxSdJrtHhB/GFRfHg6EOAKfCGbPHwYIMb5BW\n" +
+ "KNxRRyfpAPhUP9a+GgH4mHXkv+wSR87zE3hbCf7Fg/4mB25Cx4r/34E5W0F0XuCN\n" +
+ "HzSwXHECgYEA8pdeT6R2mlWDgD7IfhyeYoUcJ0oXvd6dKlGOETlkzkGi4QvP3BsM\n" +
+ "wg0sELPhuYCOG53SzSW9d5QkqDYJRY4/xg15QV2LYOMpP5b9cjJZRE3Uo9BVIBum\n" +
+ "EFVZvuGzZaFUO6Zx3xQiQgHuCP8Tx676vTk36ka3fVQV5FdY8tP0HyUCgYEA59ET\n" +
+ "v6eE2s10T9JeO3htK5TjwioMYpp3j+HUZX78anyqWw17OUityWi/dRnCoyfpPuIi\n" +
+ "qBGNjMk3JZYz3MmoR9pPGKgzI43EQKBay6+CjZfcQ4Vw7qzW0bUKD2xfLU+ZOeR+\n" +
+ "jJn5wdBvZHooX8e1en/aLj5h9h9FzhAy3/Sd1ssCgYB1S8tGJvdR2FclAzZeA+hx\n" +
+ "KntaY/Dm1WSYuaY/ncioEgR3XAa9Hjck/Ml5qgBSeV487CqpFr5tuyueScJh503e\n" +
+ "rVUbzec+iZfAL3mMZdvTsu5F5s3CIJxC+YHTUb40PbVEwk381vdZgyVdJDikLG8A\n" +
+ "X1Ix7M97wdRz++f+QY2gIQKBgQCeHaiHt95RU4O7EjT+AVUNPd/fxsht1QgqFpHF\n" +
+ "rMjEZUXZFyfuWZlX4F9+otR0bruUDbAvzNEsru4zb/Dt7ooegFQk8Ez5OjAbGIT1\n" +
+ "mz/EDknJsFHoKfHYVdCH1pZQlJNhvm1mv3twbBgeg4fYVKJ+7IfHtPsiYhA9ziS1\n" +
+ "RucF4wKBgQDJfd1BxBdkeSRIJ/C75iZ4vWWsM/JvMI1L68ZJEWdTqUvyyy9xLWEe\n" +
+ "8wIGZTv/mnuQhOGSaUUk0fTup7ZwTfmg+hahhCBe5kSh4bav5+knu6yQ7nhwccs8\n" +
+ "WXeajzno43UHZksae1LP1B3J1+0adxpykCMzWl19XZkxtVkYVi0Q3g==\n" +
+ "-----END RSA PRIVATE KEY-----";
+
+ @Test
+ public void createJWT_is_valid() throws Exception {
+ String jwt = createJWT("123", PKCS8_PRIVATE_KEY);
+ Jws parsedJwt = Jwts.parser()
+ .setSigningKey(getPublicKeyFromString(PKCS8_PUBLIC_KEY))
+ .parseClaimsJws(jwt);
+ assertThat(parsedJwt.getBody().getIssuer(), is("123"));
+ }
+
+ @Test
+ public void createJWT_with_pkcs1_is_invalid() {
+ expectedException.expect(InvalidPrivateKeyException.class);
+ expectedException.expectMessage(contains("openssl pkcs8 -topk8"));
+ createJWT("123", PKCS1_PRIVATE_KEY);
+ }
+
+ @Test
+ public void createJWT_with_not_base64_is_invalid() {
+ expectedException.expect(InvalidPrivateKeyException.class);
+ expectedException.expectMessage(contains("Failed to decode private key"));
+ createJWT("123", "d£!@!@£!@£");
+ }
+
+ private static PublicKey getPublicKeyFromString(final String key) throws GeneralSecurityException {
+ String publicKeyContent = key.replaceAll("\\n", "")
+ .replace("-----BEGIN PUBLIC KEY-----", "")
+ .replace("-----END PUBLIC KEY-----", "");
+
+ KeyFactory kf = KeyFactory.getInstance("RSA");
+
+ X509EncodedKeySpec keySpecPKCS8 = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyContent));
+
+ return kf.generatePublic(keySpecPKCS8);
+ }
+}
\ No newline at end of file
From 96bfe1301b32845aba36714aa85c65a62affc915 Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Sun, 2 Feb 2020 21:10:59 +0000
Subject: [PATCH 02/18] Pass api url through to fix GHE
---
.../jenkinsci/plugins/github_branch_source/Connector.java | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
index 9b0be9f1f..553ef6c84 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
@@ -425,7 +425,7 @@ public static void checkApiUrlValidity(@Nonnull GitHub gitHub, @CheckForNull Sta
if (username != null && !githubApp) {
gb.withPassword(username, password);
} else if (username != null) {
- gb.withOAuthToken(generateAppInstallationToken(username, password), "");
+ gb.withOAuthToken(generateAppInstallationToken(username, password, apiUrl), "");
}
hub = gb.build();
@@ -437,10 +437,10 @@ public static void checkApiUrlValidity(@Nonnull GitHub gitHub, @CheckForNull Sta
}
@SuppressWarnings("deprecation") // preview features are required for GitHub app integration, GitHub api adds deprecated to all preview methods
- private static String generateAppInstallationToken(String appId, String appPrivateKey) {
+ private static String generateAppInstallationToken(String appId, String appPrivateKey, String apiUrl) {
try {
String jwtToken = createJWT(appId, appPrivateKey);
- GitHub gitHubApp = new GitHubBuilder().withJwtToken(jwtToken).build();
+ GitHub gitHubApp = new GitHubBuilder().withEndpoint(apiUrl).withJwtToken(jwtToken).build();
GHApp app = gitHubApp.getApp();
From 5225a5d136baaa6aa41aee3bd5ad4dc914be87ae Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Sat, 1 Feb 2020 16:12:23 +0100
Subject: [PATCH 03/18] Add documentation
---
README.md | 10 ++++
docs/github-app.adoc | 111 +++++++++++++++++++++++++++++++++++++++
docs/implementation.adoc | 2 +-
3 files changed, 122 insertions(+), 1 deletion(-)
create mode 100644 docs/github-app.adoc
diff --git a/README.md b/README.md
index c27c8ff9a..96c3259d7 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,16 @@ The GitHub Branch Source plugin allows you to create a new project based on the
GitHub users or organizations. Complete documentation is
[hosted by CloudBees](https://docs.cloudbees.com/docs/admin-resources/latest/plugins/github-branch-source).
+### Guides
+
+* [GitHub app authentication](docs/github-app.adoc)
+* [Extension points provided by this plugin](docs/implementation.adoc)
+
+## Extension plugins
+
+* [github-scm-trait-notification-context](https://github.com/jenkinsci/github-scm-trait-notification-context-plugin) -
+allows overriding the `continuous-integration/jenkins/` commit status name.
+
## Version History
See [the changelog](CHANGELOG.md).
diff --git a/docs/github-app.adoc b/docs/github-app.adoc
new file mode 100644
index 000000000..dd5cc67bd
--- /dev/null
+++ b/docs/github-app.adoc
@@ -0,0 +1,111 @@
+= GitHub app authentication guide
+
+This guide is targeted to users who want to use a link:https://developer.github.com/v3/apps/[GitHub app]
+to authenticate to Jenkins.
+
+== Why?
+
+- the link:https://developer.github.com/apps/building-github-apps/understanding-rate-limits-for-github-apps/[rate limit]
+for a GitHub app scales with your organization size, whereas a user based token has a limit of 5000 regardless of
+how many repositories you have,
+- for organization's that have 2fa enforced - no need to manage 2fa tokens for a 'bot' user
+
+== Getting started
+
+Before you get started make sure you have the required permissions:
+
+=== GitHub
+
+You'll need the permission to create a GitHub app, if you're creating it on a personal account then you can skip this section,
+otherwise:
+
+- organization owner
+
+or
+
+- permission to manage GitHub apps has been
+link:https://help.github.com/en/github/setting-up-and-managing-organizations-and-teams/adding-github-app-managers-in-your-organization[delegated to you].
+
+=== Jenkins
+
+You'll need the permission to create a new credential and update job configuration, the specific permissions are:
+
+- Credentials/Create
+- Job/Configure
+
+== Creating the GitHub app
+
+link:https://developer.github.com/apps/building-github-apps/creating-a-github-app/[Follow the GitHub guide for creating an app]
+
+The only fields you need to fill out (currently) are:
+
+- Github App name - i.e. `Jenkins - `
+- Homepage URL - your company domain or a github repository
+- Webhook URL - your jenkins instance, i.e. `https:///github-webhook/`
+
+Permissions this plugin uses:
+
+- Commit statuses - read and write
+- Webhooks (optional) - if you want the plugin to manage webhooks for you, read and write
+
+
+Click 'Create GitHub app'
+
+You now need to generate a private key authenticating to the GitHub app
+
+Click the 'generate a private key' option.
+
+After a couple of seconds the key will be downloaded to your downloads folder.
+
+Now you need to convert the key into a different format that Jenkins can use:
+
+[source,shell]
+----
+openssl pkcs8 -topk8 -inform PEM -outform PEM -in key-in-your-downloads-folder.pem -out converted-github-app.pem -nocrypt
+----
+
+== Adding the Jenkins credential
+
+- From the Jenkins main page click 'Credentials'
+- Pick your credential store, normally `(global)`
+- Click 'Add credentials'
+
+Fill out the form:
+
+- Kind: SSH username with private key
+- ID: i.e. github-app-
+- Username: the github app ID, it can be found in the 'About' section of your GitHub app in the general tab.
+- Private key: enter directly, paste the contents of the converted private key
+- Passphrase: do not fill this field, it will be ignored
+- Click OK
+
+== Configuring the github organization folder
+
+See the link:https://docs.cloudbees.com/docs/admin-resources/latest/plugins/github-branch-source[main documentation]
+for how to create a GitHub folder.
+
+- Load the folders configuration page
+- Select the GitHub app credentials in the 'Credentials field drop down
+
+After selecting the credential you should see:
+
+[quote]
+----
+GHApp verified, remaining rate limit: 5000
+----
+
+- Click save
+- Click 'Scan organization now'
+- Click 'Scan organisation log'
+
+Verify at the bottom of the scan log it says:
+
+[quote]
+----
+Finished: SUCCESS
+----
+
+=== Help?
+
+Raise an issue on link:https://issues.jenkins-ci.org/[Jenkins jira]
+setting the 'component' to be `github-brance-source-plugin`
diff --git a/docs/implementation.adoc b/docs/implementation.adoc
index b60d9b871..f1d493080 100644
--- a/docs/implementation.adoc
+++ b/docs/implementation.adoc
@@ -45,6 +45,6 @@ explicitly apply a `DefaultGitHubNotificationStrategy` to the source context in
Duplicate (by equality) strategies are ignored when applied to the source context.
==== Implementations:
-https://github.com/steven-foster/github-scm-trait-notification-context[github-scm-trait-notification-context]
+https://github.com/jenkinsci/github-scm-trait-notification-context-plugin[github-scm-trait-notification-context]
From 42e6826d039fb0b7e2846447b3370a63392b1008 Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Sun, 2 Feb 2020 20:22:30 +0100
Subject: [PATCH 04/18] More logging on failure
---
.../org/jenkinsci/plugins/github_branch_source/Connector.java | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
index 553ef6c84..6073346e1 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
@@ -220,7 +220,9 @@ public static FormValidation checkScanCredentials(@CheckForNull Item context, St
Connector.release(connector);
}
} catch (IllegalArgumentException | InvalidPrivateKeyException e) {
- return FormValidation.error(e.getMessage());
+ String msg = "Exception validating credentials " + CredentialsNameProvider.name(credentials);
+ LOGGER.log(Level.WARNING, msg, e);
+ return FormValidation.error(e, msg);
} catch (IOException e) {
// ignore, never thrown
LOGGER.log(Level.WARNING, "Exception validating credentials {0} on {1}", new Object[]{
From 8010d4d27cd486c6cd5d23df61a4e760c6302582 Mon Sep 17 00:00:00 2001
From: Praveen Adusumilli <47391951+adusumillipraveen@users.noreply.github.com>
Date: Mon, 3 Feb 2020 15:37:57 +0000
Subject: [PATCH 05/18] Update github-app.adoc
---
docs/github-app.adoc | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/docs/github-app.adoc b/docs/github-app.adoc
index dd5cc67bd..3747736ce 100644
--- a/docs/github-app.adoc
+++ b/docs/github-app.adoc
@@ -45,8 +45,11 @@ The only fields you need to fill out (currently) are:
Permissions this plugin uses:
-- Commit statuses - read and write
-- Webhooks (optional) - if you want the plugin to manage webhooks for you, read and write
+- Commit statuses - Read and Write
+- Contents: Read-only (to read the Jenkinsfile)
+- Metadata: Read-only
+- Pull requests: Read-only
+- Webhooks (optional) - If you want the plugin to manage webhooks for you, Read and Write
Click 'Create GitHub app'
From 94157456c917e1a20e6250f3385bb85065c5e91f Mon Sep 17 00:00:00 2001
From: Praveen Adusumilli <47391951+adusumillipraveen@users.noreply.github.com>
Date: Tue, 4 Feb 2020 15:29:00 +0000
Subject: [PATCH 06/18] Update github-app.adoc
---
docs/github-app.adoc | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docs/github-app.adoc b/docs/github-app.adoc
index 3747736ce..ec34eae1b 100644
--- a/docs/github-app.adoc
+++ b/docs/github-app.adoc
@@ -67,6 +67,10 @@ Now you need to convert the key into a different format that Jenkins can use:
openssl pkcs8 -topk8 -inform PEM -outform PEM -in key-in-your-downloads-folder.pem -out converted-github-app.pem -nocrypt
----
+== Install the GitHub app
+
+- From the install app section of newly created app, install the app to your organization.
+
== Adding the Jenkins credential
- From the Jenkins main page click 'Credentials'
From df39f0c1833399d06b6f8cb4ea884ba5bd229c1e Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Tue, 4 Feb 2020 20:29:14 +0000
Subject: [PATCH 07/18] Update docs/github-app.adoc
Co-Authored-By: Olivier Jacques
---
docs/github-app.adoc | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/github-app.adoc b/docs/github-app.adoc
index ec34eae1b..d2cec6902 100644
--- a/docs/github-app.adoc
+++ b/docs/github-app.adoc
@@ -9,6 +9,7 @@ to authenticate to Jenkins.
for a GitHub app scales with your organization size, whereas a user based token has a limit of 5000 regardless of
how many repositories you have,
- for organization's that have 2fa enforced - no need to manage 2fa tokens for a 'bot' user
+- to improve and tighten security: the Jenkins GitHub app requires a minimum, controlled set of privileges compared to a service user and its personal access token which has a much wider set of privileges
== Getting started
From 262f07e8d134f9d17905f428b7e4aba812f88859 Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Tue, 4 Feb 2020 21:55:38 +0000
Subject: [PATCH 08/18] Update
src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
Co-Authored-By: Olivier Jacques
---
.../org/jenkinsci/plugins/github_branch_source/Connector.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
index 6073346e1..69f1c6b06 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
@@ -113,7 +113,7 @@ protected boolean removeEldestEntry(Map.Entry eldest) {
};
private static final Random ENTROPY = new Random();
private static final String SALT = Long.toHexString(ENTROPY.nextLong());
- private static final String ERROR_AUTHENTICATING_GITHUB_APP = "Couldn't find GitHub app installation %s";
+ private static final String ERROR_AUTHENTICATING_GITHUB_APP = "Couldn't authenticate with GitHub app ID %s";
private Connector() {
throw new IllegalAccessError("Utility class");
From cd4b7952844c2b7cf77dbcf33d1a4fed400db7ee Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Wed, 5 Feb 2020 19:17:44 +0000
Subject: [PATCH 09/18] Create GitHubAppCredential
---
pom.xml | 8 +-
.../github_branch_source/Connector.java | 49 +-----
.../GitHubAppCredential.java | 144 ++++++++++++++++++
.../GitHubSCMNavigator.java | 2 +-
.../SSHCheckoutTrait.java | 19 ++-
.../GitHubAppCredential/config.jelly | 14 ++
.../github_branch_source/Messages.properties | 2 +
7 files changed, 188 insertions(+), 50 deletions(-)
create mode 100644 src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java
create mode 100644 src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/config.jelly
diff --git a/pom.xml b/pom.xml
index ceb9db148..058a38260 100644
--- a/pom.xml
+++ b/pom.xml
@@ -68,7 +68,13 @@
org.jenkins-ci.plugins
credentials
- 2.1.18
+ 2.2.0
+
+
+
+ io.jenkins.temp.jelly
+ multiline-secrets-ui
+ 1.0
com.coravy.hudson.plugins.github
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
index 69f1c6b06..4974a3eb0 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
@@ -77,21 +77,14 @@
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.gitclient.GitClient;
import org.jenkinsci.plugins.github.config.GitHubServerConfig;
-import org.kohsuke.accmod.Restricted;
-import org.kohsuke.accmod.restrictions.NoExternalUse;
-import org.kohsuke.github.GHApp;
-import org.kohsuke.github.GHAppInstallation;
-import org.kohsuke.github.GHAppInstallationToken;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
-import org.kohsuke.github.HttpConnector;
import org.kohsuke.github.RateLimitHandler;
import org.kohsuke.github.extras.OkHttpConnector;
import static com.cloudbees.plugins.credentials.CredentialsMatchers.anyOf;
import static com.cloudbees.plugins.credentials.CredentialsMatchers.instanceOf;
import static java.util.logging.Level.FINE;
-import static org.jenkinsci.plugins.github_branch_source.JwtHelper.createJWT;
/**
* Utilities that could perhaps be moved into {@code github-api}.
@@ -113,7 +106,6 @@ protected boolean removeEldestEntry(Map.Entry eldest) {
};
private static final Random ENTROPY = new Random();
private static final String SALT = Long.toHexString(ENTROPY.nextLong());
- private static final String ERROR_AUTHENTICATING_GITHUB_APP = "Couldn't authenticate with GitHub app ID %s";
private Connector() {
throw new IllegalAccessError("Utility class");
@@ -206,7 +198,7 @@ public static FormValidation checkScanCredentials(@CheckForNull Item context, St
GitHub connector = Connector.connect(apiUri, credentials);
try {
try {
- boolean githubAppAuthentication = credentials instanceof SSHUserPrivateKey;
+ boolean githubAppAuthentication = credentials instanceof GitHubAppCredential;
if (githubAppAuthentication) {
int remaining = connector.getRateLimit().getRemaining();
return FormValidation.ok("GHApp verified, remaining rate limit: %d", remaining);
@@ -327,9 +319,6 @@ public static void checkApiUrlValidity(@Nonnull GitHub gitHub, @CheckForNull Sta
} else if (credentials instanceof StandardUsernamePasswordCredentials) {
StandardUsernamePasswordCredentials c = (StandardUsernamePasswordCredentials) credentials;
hash = Util.getDigestOf(c.getPassword().getPlainText() + SALT);
- } else if (credentials instanceof SSHUserPrivateKey) {
- SSHUserPrivateKey c = (SSHUserPrivateKey) credentials;
- hash = Util.getDigestOf(c.getPrivateKeys().get(0) + SALT);
} else {
// TODO OAuth support
throw new IOException("Unsupported credential type: " + credentials.getClass().getName());
@@ -352,7 +341,6 @@ public static void checkApiUrlValidity(@Nonnull GitHub gitHub, @CheckForNull Sta
String password;
String hash;
String authHash;
- boolean githubApp = false;
Jenkins jenkins = Jenkins.get();
if (credentials == null) {
username = null;
@@ -365,14 +353,6 @@ public static void checkApiUrlValidity(@Nonnull GitHub gitHub, @CheckForNull Sta
password = c.getPassword().getPlainText();
hash = Util.getDigestOf(password + SALT); // want to ensure pooling by credential
authHash = Util.getDigestOf(password + "::" + jenkins.getLegacyInstanceId());
- } else if (credentials instanceof SSHUserPrivateKey) {
- SSHUserPrivateKey c = (SSHUserPrivateKey) credentials;
- username = c.getUsername();
- password = c.getPrivateKeys().get(0);
- hash = Util.getDigestOf(password + SALT);
- authHash = Util.getDigestOf(password + "::" + jenkins.getLegacyInstanceId());
-
- githubApp = true;
} else {
// TODO OAuth support
throw new IOException("Unsupported credential type: " + credentials.getClass().getName());
@@ -424,10 +404,8 @@ public static void checkApiUrlValidity(@Nonnull GitHub gitHub, @CheckForNull Sta
gb.withConnector(new OkHttpConnector(new OkUrlFactory(client)));
- if (username != null && !githubApp) {
+ if (username != null) {
gb.withPassword(username, password);
- } else if (username != null) {
- gb.withOAuthToken(generateAppInstallationToken(username, password, apiUrl), "");
}
hub = gb.build();
@@ -438,29 +416,6 @@ public static void checkApiUrlValidity(@Nonnull GitHub gitHub, @CheckForNull Sta
}
}
- @SuppressWarnings("deprecation") // preview features are required for GitHub app integration, GitHub api adds deprecated to all preview methods
- private static String generateAppInstallationToken(String appId, String appPrivateKey, String apiUrl) {
- try {
- String jwtToken = createJWT(appId, appPrivateKey);
- GitHub gitHubApp = new GitHubBuilder().withEndpoint(apiUrl).withJwtToken(jwtToken).build();
-
- GHApp app = gitHubApp.getApp();
-
- List appInstallations = app.listInstallations().asList();
- if (!appInstallations.isEmpty()) {
- GHAppInstallation appInstallation = appInstallations.get(0);
- GHAppInstallationToken appInstallationToken = appInstallation
- .createToken(appInstallation.getPermissions())
- .create();
-
- return appInstallationToken.getToken();
- }
- } catch (IOException e) {
- throw new IllegalArgumentException(String.format(ERROR_AUTHENTICATING_GITHUB_APP, appId), e);
- }
- throw new IllegalArgumentException(String.format(ERROR_AUTHENTICATING_GITHUB_APP, appId));
- }
-
public static void release(@CheckForNull GitHub hub) {
if (hub == null) {
return;
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java
new file mode 100644
index 000000000..af9383c50
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java
@@ -0,0 +1,144 @@
+package org.jenkinsci.plugins.github_branch_source;
+
+import com.cloudbees.plugins.credentials.CredentialsScope;
+import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
+import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Extension;
+import hudson.Util;
+import hudson.util.Secret;
+import java.io.IOException;
+import java.util.List;
+import org.kohsuke.github.GHApp;
+import org.kohsuke.github.GHAppInstallation;
+import org.kohsuke.github.GHAppInstallationToken;
+import org.kohsuke.github.GitHub;
+import org.kohsuke.github.GitHubBuilder;
+import org.kohsuke.stapler.DataBoundConstructor;
+
+import static org.jenkinsci.plugins.github_branch_source.JwtHelper.createJWT;
+
+public class GitHubAppCredential extends BaseStandardCredentials implements StandardUsernamePasswordCredentials {
+
+ private static final String ERROR_AUTHENTICATING_GITHUB_APP = "Couldn't authenticate with GitHub app ID %s";
+
+ @NonNull
+ private final String appID;
+
+ @NonNull
+ private final Secret privateKey;
+
+ private String apiUrl;
+
+ /**
+ * Constructor.
+ *
+ * @param scope the credentials scope
+ * @param id the ID or {@code null} to generate a new one.
+ * @param description the description.
+ * @param appID the username.
+ * @param privateKey the password.
+ */
+ @DataBoundConstructor
+ @SuppressWarnings("unused") // by stapler
+ public GitHubAppCredential(
+ CredentialsScope scope,
+ String id,
+ @CheckForNull String description,
+ @NonNull String appID,
+ @NonNull Secret privateKey
+ ) {
+ super(scope, id, description);
+ this.appID = appID;
+ this.privateKey = privateKey;
+ }
+
+ public String getApiUrl() {
+ return apiUrl;
+ }
+
+ public void setApiUrl(String apiUrl) {
+ this.apiUrl = apiUrl;
+ }
+
+ @NonNull
+ public String getAppID() {
+ return appID;
+ }
+
+ @NonNull
+ public Secret getPrivateKey() {
+ return privateKey;
+ }
+
+ @SuppressWarnings("deprecation") // preview features are required for GitHub app integration, GitHub api adds deprecated to all preview methods
+ static String generateAppInstallationToken(String appId, String appPrivateKey, String apiUrl) {
+ try {
+ String jwtToken = createJWT(appId, appPrivateKey);
+ GitHub gitHubApp = new GitHubBuilder().withEndpoint(apiUrl).withJwtToken(jwtToken).build();
+
+ GHApp app = gitHubApp.getApp();
+
+ List appInstallations = app.listInstallations().asList();
+ if (!appInstallations.isEmpty()) {
+ GHAppInstallation appInstallation = appInstallations.get(0);
+ GHAppInstallationToken appInstallationToken = appInstallation
+ .createToken(appInstallation.getPermissions())
+ .create();
+
+ return appInstallationToken.getToken();
+ }
+ } catch (IOException e) {
+ throw new IllegalArgumentException(String.format(ERROR_AUTHENTICATING_GITHUB_APP, appId), e);
+ }
+ throw new IllegalArgumentException(String.format(ERROR_AUTHENTICATING_GITHUB_APP, appId));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @NonNull
+ @Override
+ public Secret getPassword() {
+ if (Util.fixEmpty(apiUrl) == null) {
+ apiUrl = "https://api.github.com";
+ }
+
+ String appInstallationToken = generateAppInstallationToken(appID, privateKey.getPlainText(), apiUrl);
+
+ return Secret.fromString(appInstallationToken);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @NonNull
+ @Override
+ public String getUsername() {
+ return appID;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Extension
+ public static class DescriptorImpl extends BaseStandardCredentialsDescriptor {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getDisplayName() {
+ return Messages.GitHubAppCredential_displayName();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getIconClassName() {
+ return "icon-github-logo";
+ }
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java
index 122424d13..eed2b8195 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java
@@ -934,7 +934,7 @@ public void visitSources(SCMSourceObserver observer) throws IOException, Interru
SourceFactory sourceFactory = new SourceFactory(request);
WitnessImpl witness = new WitnessImpl(listener);
- boolean githubAppAuthentication = credentials instanceof SSHUserPrivateKey;
+ boolean githubAppAuthentication = credentials instanceof GitHubAppCredential;
if (!github.isAnonymous() && !githubAppAuthentication) {
GHMyself myself;
try {
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/SSHCheckoutTrait.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/SSHCheckoutTrait.java
index aa583aa34..14cdc16e9 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/SSHCheckoutTrait.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/SSHCheckoutTrait.java
@@ -35,12 +35,12 @@
import hudson.Util;
import hudson.model.Item;
import hudson.model.Queue;
-import hudson.model.queue.Tasks;
import hudson.plugins.git.GitSCM;
import hudson.scm.SCM;
import hudson.security.ACL;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
+import java.util.Objects;
import jenkins.model.Jenkins;
import jenkins.plugins.git.GitSCMBuilder;
import jenkins.scm.api.SCMSource;
@@ -105,6 +105,23 @@ protected void decorateBuilder(SCMBuilder,?> builder) {
((GitHubSCMBuilder)builder).withCredentials(credentialsId, GitHubSCMBuilder.SSH);
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ SSHCheckoutTrait that = (SSHCheckoutTrait) o;
+ return Objects.equals(credentialsId, that.credentialsId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(credentialsId);
+ }
+
/**
* Our descriptor.
*/
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/config.jelly b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/config.jelly
new file mode 100644
index 000000000..953a7ec07
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/config.jelly
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/Messages.properties b/src/main/resources/org/jenkinsci/plugins/github_branch_source/Messages.properties
index 72b7e2bac..b609ef7f7 100644
--- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/Messages.properties
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/Messages.properties
@@ -66,3 +66,5 @@ ExcludeArchivedRepositoriesTrait.displayName=Exclude archived repositories
GitHubSCMNavigator.general=General
GitHubSCMNavigator.withinRepository=Within repository
+
+GitHubAppCredential.displayName=GitHub app
\ No newline at end of file
From 9e7de1c20d1aeacbc3666a17c12846e6534ef6b7 Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Wed, 5 Feb 2020 19:32:22 +0000
Subject: [PATCH 10/18] Revert unneeded changes
---
docs/github-app.adoc | 6 +++---
.../github_branch_source/Connector.java | 7 ++-----
.../GitHubAppCredential.java | 9 ---------
.../SSHCheckoutTrait.java | 19 +------------------
.../help-credentialsId.html | 15 +++++----------
.../GitHubSCMSource/help-credentialsId.html | 11 -----------
6 files changed, 11 insertions(+), 56 deletions(-)
diff --git a/docs/github-app.adoc b/docs/github-app.adoc
index d2cec6902..e62480213 100644
--- a/docs/github-app.adoc
+++ b/docs/github-app.adoc
@@ -80,10 +80,10 @@ openssl pkcs8 -topk8 -inform PEM -outform PEM -in key-in-your-downloads-folder.p
Fill out the form:
-- Kind: SSH username with private key
+- Kind: GitHub app
- ID: i.e. github-app-
-- Username: the github app ID, it can be found in the 'About' section of your GitHub app in the general tab.
-- Private key: enter directly, paste the contents of the converted private key
+- App ID: the github app ID, it can be found in the 'About' section of your GitHub app in the general tab.
+- Key: click add, paste the contents of the converted private key
- Passphrase: do not fill this field, it will be ignored
- Click OK
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
index 4974a3eb0..645b4e06b 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
@@ -25,7 +25,6 @@
package org.jenkinsci.plugins.github_branch_source;
import com.cloudbees.jenkins.GitHubWebHook;
-import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
import com.cloudbees.plugins.credentials.CredentialsMatcher;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsNameProvider;
@@ -463,10 +462,8 @@ private static void unused(@Nonnull GitHub hub) {
}
private static CredentialsMatcher githubScanCredentialsMatcher() {
- return anyOf(
- instanceOf(StandardUsernamePasswordCredentials.class),
- instanceOf(SSHUserPrivateKey.class)
- );
+ // TODO OAuth credentials
+ return CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class));
}
static List githubDomainRequirements(String apiUri) {
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java
index af9383c50..f2e624ec8 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java
@@ -31,15 +31,6 @@ public class GitHubAppCredential extends BaseStandardCredentials implements Stan
private String apiUrl;
- /**
- * Constructor.
- *
- * @param scope the credentials scope
- * @param id the ID or {@code null} to generate a new one.
- * @param description the description.
- * @param appID the username.
- * @param privateKey the password.
- */
@DataBoundConstructor
@SuppressWarnings("unused") // by stapler
public GitHubAppCredential(
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/SSHCheckoutTrait.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/SSHCheckoutTrait.java
index 14cdc16e9..aa583aa34 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/SSHCheckoutTrait.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/SSHCheckoutTrait.java
@@ -35,12 +35,12 @@
import hudson.Util;
import hudson.model.Item;
import hudson.model.Queue;
+import hudson.model.queue.Tasks;
import hudson.plugins.git.GitSCM;
import hudson.scm.SCM;
import hudson.security.ACL;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
-import java.util.Objects;
import jenkins.model.Jenkins;
import jenkins.plugins.git.GitSCMBuilder;
import jenkins.scm.api.SCMSource;
@@ -105,23 +105,6 @@ protected void decorateBuilder(SCMBuilder,?> builder) {
((GitHubSCMBuilder)builder).withCredentials(credentialsId, GitHubSCMBuilder.SSH);
}
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- SSHCheckoutTrait that = (SSHCheckoutTrait) o;
- return Objects.equals(credentialsId, that.credentialsId);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(credentialsId);
- }
-
/**
* Our descriptor.
*/
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-credentialsId.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-credentialsId.html
index 322f0a622..a77b93fbd 100644
--- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-credentialsId.html
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-credentialsId.html
@@ -3,16 +3,11 @@
Credentials used to scan branches and pull requests,
check out sources and mark commit statuses.
- The following credential types are supported:
-
- -
- "Username with password"
-
- -
- "SSH Username with private key" - this is for authenticating as a GitHub app.
- The username should be the 'GitHub app ID'.
-
-
+
+ Note that only "username with password" credentials are supported.
+ Existing credentials of other kinds will be filtered out. This is because Jenkins
+ uses the GitHub API, which does not support other ways of authentication.
+
If none is given, only the public repositories will be scanned, and commit status
will not be set on GitHub.
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-credentialsId.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-credentialsId.html
index df36ebcba..a77b93fbd 100644
--- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-credentialsId.html
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-credentialsId.html
@@ -3,17 +3,6 @@
Credentials used to scan branches and pull requests,
check out sources and mark commit statuses.
- The following credential types are supported:
-
- -
- "Username with password"
-
- -
- "SSH Username with private key" - this is for authenticating as a GitHub app.
- The username should be the 'GitHub app ID'.
-
-
-
Note that only "username with password" credentials are supported.
Existing credentials of other kinds will be filtered out. This is because Jenkins
From e2c9374023cb236808a225c9d904758a43489c3d Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Wed, 5 Feb 2020 20:15:28 +0000
Subject: [PATCH 11/18] Add GHE support
---
.../GitHubAppCredential.java | 65 ++++++++++++++++---
.../GitHubSCMNavigator.java | 4 ++
.../GitHubAppCredential/config.jelly | 16 +++++
.../GitHubAppCredential/help-apiUri.html | 3 +
4 files changed, 80 insertions(+), 8 deletions(-)
create mode 100644 src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-apiUri.html
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java
index f2e624ec8..107fbb360 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java
@@ -7,16 +7,24 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.Util;
+import hudson.util.FormValidation;
+import hudson.util.ListBoxModel;
import hudson.util.Secret;
import java.io.IOException;
import java.util.List;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.github.GHApp;
import org.kohsuke.github.GHAppInstallation;
import org.kohsuke.github.GHAppInstallationToken;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.DataBoundSetter;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.verb.POST;
+import static org.jenkinsci.plugins.github_branch_source.GitHubSCMNavigator.DescriptorImpl.getPossibleApiUriItems;
import static org.jenkinsci.plugins.github_branch_source.JwtHelper.createJWT;
public class GitHubAppCredential extends BaseStandardCredentials implements StandardUsernamePasswordCredentials {
@@ -29,7 +37,7 @@ public class GitHubAppCredential extends BaseStandardCredentials implements Stan
@NonNull
private final Secret privateKey;
- private String apiUrl;
+ private String apiUri;
@DataBoundConstructor
@SuppressWarnings("unused") // by stapler
@@ -45,12 +53,13 @@ public GitHubAppCredential(
this.privateKey = privateKey;
}
- public String getApiUrl() {
- return apiUrl;
+ public String getApiUri() {
+ return apiUri;
}
- public void setApiUrl(String apiUrl) {
- this.apiUrl = apiUrl;
+ @DataBoundSetter
+ public void setApiUri(String apiUri) {
+ this.apiUri = apiUri;
}
@NonNull
@@ -92,11 +101,11 @@ static String generateAppInstallationToken(String appId, String appPrivateKey, S
@NonNull
@Override
public Secret getPassword() {
- if (Util.fixEmpty(apiUrl) == null) {
- apiUrl = "https://api.github.com";
+ if (Util.fixEmpty(apiUri) == null) {
+ apiUri = "https://api.github.com";
}
- String appInstallationToken = generateAppInstallationToken(appID, privateKey.getPlainText(), apiUrl);
+ String appInstallationToken = generateAppInstallationToken(appID, privateKey.getPlainText(), apiUri);
return Secret.fromString(appInstallationToken);
}
@@ -131,5 +140,45 @@ public String getDisplayName() {
public String getIconClassName() {
return "icon-github-logo";
}
+
+ @SuppressWarnings("unused") // jelly
+ public boolean isApiUriSelectable() {
+ return !GitHubConfiguration.get().getEndpoints().isEmpty();
+ }
+
+ /**
+ * Returns the available GitHub endpoint items.
+ *
+ * @return the available GitHub endpoint items.
+ */
+ @SuppressWarnings("unused") // stapler
+ @Restricted(NoExternalUse.class) // stapler
+ public ListBoxModel doFillApiUriItems() {
+ return getPossibleApiUriItems();
+ }
+
+ @POST
+ @SuppressWarnings("unused") // stapler
+ @Restricted(NoExternalUse.class) // stapler
+ public FormValidation doTestConnection(
+ @QueryParameter("appID") final String appID,
+ @QueryParameter("privateKey") final String privateKey,
+ @QueryParameter("apiUri") final String apiUri
+
+ ) {
+ GitHubAppCredential gitHubAppCredential = new GitHubAppCredential(
+ CredentialsScope.GLOBAL, "test-id-not-being-saved", null,
+ appID, Secret.fromString(privateKey)
+ );
+ gitHubAppCredential.setApiUri(apiUri);
+
+ try {
+ GitHub connect = Connector.connect(apiUri, gitHubAppCredential);
+
+ return FormValidation.ok("Success, Remaining rate limit: " + connect.getRateLimit().getRemaining());
+ } catch (Exception e) {
+ return FormValidation.error(e, String.format(ERROR_AUTHENTICATING_GITHUB_APP, appID));
+ }
+ }
}
}
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java
index eed2b8195..5d58b8cea 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java
@@ -1416,6 +1416,10 @@ public ListBoxModel doFillCredentialsIdItems(@CheckForNull @AncestorInPath Item
@Restricted(NoExternalUse.class) // stapler
@SuppressWarnings("unused") // stapler
public ListBoxModel doFillApiUriItems() {
+ return getPossibleApiUriItems();
+ }
+
+ static ListBoxModel getPossibleApiUriItems() {
ListBoxModel result = new ListBoxModel();
result.add("GitHub", "");
for (Endpoint e : GitHubConfiguration.get().getEndpoints()) {
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/config.jelly b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/config.jelly
index 953a7ec07..7544d9d58 100644
--- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/config.jelly
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/config.jelly
@@ -4,6 +4,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-apiUri.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-apiUri.html
new file mode 100644
index 000000000..72fce8569
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-apiUri.html
@@ -0,0 +1,3 @@
+
+ GitHub API endpoint such as https://github.example.com/api/v3/
.
+
\ No newline at end of file
From c311b36f15001b461b56fbfca855042c99cf736c Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Wed, 5 Feb 2020 20:17:02 +0000
Subject: [PATCH 12/18] Revert change back for non gh app
---
.../org/jenkinsci/plugins/github_branch_source/Connector.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
index 645b4e06b..cf501e5e0 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
@@ -203,7 +203,7 @@ public static FormValidation checkScanCredentials(@CheckForNull Item context, St
return FormValidation.ok("GHApp verified, remaining rate limit: %d", remaining);
}
- return FormValidation.ok("User %s", connector.isCredentialValid());
+ return FormValidation.ok("User %s", connector.getMyself().getLogin());
} catch (Exception e) {
return FormValidation.error("Invalid credentials: %s", e.getMessage());
}
From ad2e0ca954e7b18bc9a99f11a31c207c4f3a6553 Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Wed, 5 Feb 2020 21:34:43 +0000
Subject: [PATCH 13/18] Add more help text
---
.../GitHubAppCredential/help-appID.html | 3 +++
.../GitHubAppCredential/help-privateKey.html | 6 ++++++
2 files changed, 9 insertions(+)
create mode 100644 src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-appID.html
create mode 100644 src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-privateKey.html
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-appID.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-appID.html
new file mode 100644
index 000000000..d85fa8314
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-appID.html
@@ -0,0 +1,3 @@
+
+ GitHub app ID, can be found on the App's settings, on the General page in the About section
+
\ No newline at end of file
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-privateKey.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-privateKey.html
new file mode 100644
index 000000000..00374b9d8
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-privateKey.html
@@ -0,0 +1,6 @@
+
+ Private key for authenticating to GitHub with, it must be in PKCS#8 format, GitHub will give it to you in PKCS#1.
+
+
+ You can convert it with openssl pkcs8 -topk8 -inform PEM -outform PEM -in current-key.pem -out new-key.pem -nocrypt
+
\ No newline at end of file
From 07dc6a6a74b033ab4a01fc64f6ff4fb72501b0f1 Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Thu, 6 Feb 2020 07:40:41 +0000
Subject: [PATCH 14/18] Rename credential class
So that JCasC symbol name is resolved to:
githubApp
and not githubAppCredential
---
.../jenkinsci/plugins/github_branch_source/Connector.java | 4 +---
...GitHubAppCredential.java => GitHubAppCredentials.java} | 8 ++++----
.../plugins/github_branch_source/GitHubSCMNavigator.java | 3 +--
.../config.jelly | 0
.../help-apiUri.html | 0
.../help-appID.html | 0
.../help-privateKey.html | 0
.../plugins/github_branch_source/Messages.properties | 2 +-
8 files changed, 7 insertions(+), 10 deletions(-)
rename src/main/java/org/jenkinsci/plugins/github_branch_source/{GitHubAppCredential.java => GitHubAppCredentials.java} (95%)
rename src/main/resources/org/jenkinsci/plugins/github_branch_source/{GitHubAppCredential => GitHubAppCredentials}/config.jelly (100%)
rename src/main/resources/org/jenkinsci/plugins/github_branch_source/{GitHubAppCredential => GitHubAppCredentials}/help-apiUri.html (100%)
rename src/main/resources/org/jenkinsci/plugins/github_branch_source/{GitHubAppCredential => GitHubAppCredentials}/help-appID.html (100%)
rename src/main/resources/org/jenkinsci/plugins/github_branch_source/{GitHubAppCredential => GitHubAppCredentials}/help-privateKey.html (100%)
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
index cf501e5e0..37e919333 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
@@ -81,8 +81,6 @@
import org.kohsuke.github.RateLimitHandler;
import org.kohsuke.github.extras.OkHttpConnector;
-import static com.cloudbees.plugins.credentials.CredentialsMatchers.anyOf;
-import static com.cloudbees.plugins.credentials.CredentialsMatchers.instanceOf;
import static java.util.logging.Level.FINE;
/**
@@ -197,7 +195,7 @@ public static FormValidation checkScanCredentials(@CheckForNull Item context, St
GitHub connector = Connector.connect(apiUri, credentials);
try {
try {
- boolean githubAppAuthentication = credentials instanceof GitHubAppCredential;
+ boolean githubAppAuthentication = credentials instanceof GitHubAppCredentials;
if (githubAppAuthentication) {
int remaining = connector.getRateLimit().getRemaining();
return FormValidation.ok("GHApp verified, remaining rate limit: %d", remaining);
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java
similarity index 95%
rename from src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java
rename to src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java
index 107fbb360..4ff804e2c 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java
@@ -27,7 +27,7 @@
import static org.jenkinsci.plugins.github_branch_source.GitHubSCMNavigator.DescriptorImpl.getPossibleApiUriItems;
import static org.jenkinsci.plugins.github_branch_source.JwtHelper.createJWT;
-public class GitHubAppCredential extends BaseStandardCredentials implements StandardUsernamePasswordCredentials {
+public class GitHubAppCredentials extends BaseStandardCredentials implements StandardUsernamePasswordCredentials {
private static final String ERROR_AUTHENTICATING_GITHUB_APP = "Couldn't authenticate with GitHub app ID %s";
@@ -41,7 +41,7 @@ public class GitHubAppCredential extends BaseStandardCredentials implements Stan
@DataBoundConstructor
@SuppressWarnings("unused") // by stapler
- public GitHubAppCredential(
+ public GitHubAppCredentials(
CredentialsScope scope,
String id,
@CheckForNull String description,
@@ -130,7 +130,7 @@ public static class DescriptorImpl extends BaseStandardCredentialsDescriptor {
*/
@Override
public String getDisplayName() {
- return Messages.GitHubAppCredential_displayName();
+ return Messages.GitHubAppCredentials_displayName();
}
/**
@@ -166,7 +166,7 @@ public FormValidation doTestConnection(
@QueryParameter("apiUri") final String apiUri
) {
- GitHubAppCredential gitHubAppCredential = new GitHubAppCredential(
+ GitHubAppCredentials gitHubAppCredential = new GitHubAppCredentials(
CredentialsScope.GLOBAL, "test-id-not-being-saved", null,
appID, Secret.fromString(privateKey)
);
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java
index 5d58b8cea..ede94cf55 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java
@@ -25,7 +25,6 @@
package org.jenkinsci.plugins.github_branch_source;
import com.cloudbees.jenkins.GitHubWebHook;
-import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
import com.cloudbees.plugins.credentials.CredentialsNameProvider;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
@@ -934,7 +933,7 @@ public void visitSources(SCMSourceObserver observer) throws IOException, Interru
SourceFactory sourceFactory = new SourceFactory(request);
WitnessImpl witness = new WitnessImpl(listener);
- boolean githubAppAuthentication = credentials instanceof GitHubAppCredential;
+ boolean githubAppAuthentication = credentials instanceof GitHubAppCredentials;
if (!github.isAnonymous() && !githubAppAuthentication) {
GHMyself myself;
try {
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/config.jelly b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/config.jelly
similarity index 100%
rename from src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/config.jelly
rename to src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/config.jelly
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-apiUri.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-apiUri.html
similarity index 100%
rename from src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-apiUri.html
rename to src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-apiUri.html
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-appID.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-appID.html
similarity index 100%
rename from src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-appID.html
rename to src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-appID.html
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-privateKey.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-privateKey.html
similarity index 100%
rename from src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredential/help-privateKey.html
rename to src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-privateKey.html
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/Messages.properties b/src/main/resources/org/jenkinsci/plugins/github_branch_source/Messages.properties
index b609ef7f7..e75daef7a 100644
--- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/Messages.properties
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/Messages.properties
@@ -67,4 +67,4 @@ ExcludeArchivedRepositoriesTrait.displayName=Exclude archived repositories
GitHubSCMNavigator.general=General
GitHubSCMNavigator.withinRepository=Within repository
-GitHubAppCredential.displayName=GitHub app
\ No newline at end of file
+GitHubAppCredentials.displayName=GitHub app
\ No newline at end of file
From 4ad286e241f0f3f5702c98600f6fac7ebb363444 Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Thu, 6 Feb 2020 09:39:07 +0000
Subject: [PATCH 15/18] Add JCasC compatibility test
---
...bAppCredentialsJCasCCompatibilityTest.java | 91 +++++++++++++++++++
...hub-app-jcasc-minimal-expected-export.yaml | 4 +
.../github-app-jcasc-minimal.yaml | 9 ++
3 files changed, 104 insertions(+)
create mode 100644 src/test/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentialsJCasCCompatibilityTest.java
create mode 100644 src/test/resources/org/jenkinsci/plugins/github_branch_source/github-app-jcasc-minimal-expected-export.yaml
create mode 100644 src/test/resources/org/jenkinsci/plugins/github_branch_source/github-app-jcasc-minimal.yaml
diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentialsJCasCCompatibilityTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentialsJCasCCompatibilityTest.java
new file mode 100644
index 000000000..008f95e5e
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentialsJCasCCompatibilityTest.java
@@ -0,0 +1,91 @@
+package org.jenkinsci.plugins.github_branch_source;
+
+import com.cloudbees.plugins.credentials.Credentials;
+import com.cloudbees.plugins.credentials.GlobalCredentialsConfiguration;
+import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
+import com.cloudbees.plugins.credentials.casc.CredentialsRootConfigurator;
+import com.cloudbees.plugins.credentials.domains.DomainCredentials;
+import hudson.ExtensionList;
+import io.jenkins.plugins.casc.ConfigurationContext;
+import io.jenkins.plugins.casc.ConfiguratorRegistry;
+import io.jenkins.plugins.casc.impl.configurators.GlobalConfigurationCategoryConfigurator;
+import io.jenkins.plugins.casc.misc.ConfiguredWithCode;
+import io.jenkins.plugins.casc.misc.EnvVarsRule;
+import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule;
+import io.jenkins.plugins.casc.model.CNode;
+import io.jenkins.plugins.casc.model.Mapping;
+import io.jenkins.plugins.casc.model.Sequence;
+import java.util.List;
+import java.util.Objects;
+import jenkins.model.Jenkins;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+
+import static io.jenkins.plugins.casc.misc.Util.toStringFromYamlFile;
+import static io.jenkins.plugins.casc.misc.Util.toYamlString;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+import static org.jvnet.hudson.test.JenkinsMatchers.hasPlainText;
+
+public class GitHubAppCredentialsJCasCCompatibilityTest {
+
+ @ConfiguredWithCode("github-app-jcasc-minimal.yaml")
+ public static JenkinsConfiguredWithCodeRule j = new JenkinsConfiguredWithCodeRule();
+
+ private static final String GITHUB_APP_KEY = "SomeString";
+
+ @ClassRule
+ public static RuleChain chain = RuleChain
+ .outerRule(new EnvVarsRule().set("GITHUB_APP_KEY", GITHUB_APP_KEY))
+ .around(j);
+
+ @Test
+ public void should_support_configuration_as_code() {
+ List domainCredentials = SystemCredentialsProvider.getInstance()
+ .getDomainCredentials();
+
+ assertThat(domainCredentials.size(), is(1));
+ List credentials = domainCredentials.get(0).getCredentials();
+ assertThat(credentials.size(), is(1));
+
+ Credentials credential = credentials.get(0);
+ assertThat(credential, instanceOf(GitHubAppCredentials.class));
+ GitHubAppCredentials gitHubAppCredentials = (GitHubAppCredentials) credential;
+
+ assertThat(gitHubAppCredentials.getAppID(), is("1111"));
+ assertThat(gitHubAppCredentials.getDescription(), is("GitHub app 1111"));
+ assertThat(gitHubAppCredentials.getId(), is("github-app"));
+ assertThat(gitHubAppCredentials.getPrivateKey(), hasPlainText(GITHUB_APP_KEY));
+ }
+
+ @Test
+ public void should_support_configuration_export() throws Exception {
+ Sequence credentials = getCredentials();
+ CNode githubApp = credentials.get(0).asMapping().get("gitHubApp");
+
+ String exported = toYamlString(githubApp)
+ // replace secret with a constant value
+ .replaceAll("privateKey: .*", "privateKey: \"some-secret-value\"");
+
+ String expected = toStringFromYamlFile(this, "github-app-jcasc-minimal-expected-export.yaml");
+
+ assertThat(exported, is(expected));
+ }
+
+ private Sequence getCredentials() throws Exception {
+ CredentialsRootConfigurator root = Jenkins.get()
+ .getExtensionList(CredentialsRootConfigurator.class).get(0);
+
+ ConfiguratorRegistry registry = ConfiguratorRegistry.get();
+ ConfigurationContext context = new ConfigurationContext(registry);
+ Mapping configNode = Objects
+ .requireNonNull(root.describe(root.getTargetComponent(context), context)).asMapping();
+ Mapping domainCredentials = configNode
+ .get("system").asMapping().get("domainCredentials")
+ .asSequence()
+ .get(0).asMapping();
+ return domainCredentials.get("credentials").asSequence();
+ }
+}
diff --git a/src/test/resources/org/jenkinsci/plugins/github_branch_source/github-app-jcasc-minimal-expected-export.yaml b/src/test/resources/org/jenkinsci/plugins/github_branch_source/github-app-jcasc-minimal-expected-export.yaml
new file mode 100644
index 000000000..ce7789c89
--- /dev/null
+++ b/src/test/resources/org/jenkinsci/plugins/github_branch_source/github-app-jcasc-minimal-expected-export.yaml
@@ -0,0 +1,4 @@
+appID: "1111"
+description: "GitHub app 1111"
+id: "github-app"
+privateKey: "some-secret-value"
diff --git a/src/test/resources/org/jenkinsci/plugins/github_branch_source/github-app-jcasc-minimal.yaml b/src/test/resources/org/jenkinsci/plugins/github_branch_source/github-app-jcasc-minimal.yaml
new file mode 100644
index 000000000..6d3bc3ed9
--- /dev/null
+++ b/src/test/resources/org/jenkinsci/plugins/github_branch_source/github-app-jcasc-minimal.yaml
@@ -0,0 +1,9 @@
+credentials:
+ system:
+ domainCredentials:
+ - credentials:
+ - gitHubApp:
+ appID: "1111"
+ description: "GitHub app 1111"
+ id: "github-app"
+ privateKey: "${GITHUB_APP_KEY}"
\ No newline at end of file
From 91e4bf4cffe55ecdebaf19338ddf726d464c05be Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Thu, 6 Feb 2020 14:48:54 +0000
Subject: [PATCH 16/18] Update the docs
---
docs/github-app.adoc | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/docs/github-app.adoc b/docs/github-app.adoc
index e62480213..fea6ac0fb 100644
--- a/docs/github-app.adoc
+++ b/docs/github-app.adoc
@@ -74,6 +74,8 @@ openssl pkcs8 -topk8 -inform PEM -outform PEM -in key-in-your-downloads-folder.p
== Adding the Jenkins credential
+=== UI
+
- From the Jenkins main page click 'Credentials'
- Pick your credential store, normally `(global)`
- Click 'Add credentials'
@@ -83,10 +85,27 @@ Fill out the form:
- Kind: GitHub app
- ID: i.e. github-app-
- App ID: the github app ID, it can be found in the 'About' section of your GitHub app in the general tab.
+- API endpoint (optional, only required for GitHub enterprise this will only show up if a GitHub enterprise server is configured).
- Key: click add, paste the contents of the converted private key
- Passphrase: do not fill this field, it will be ignored
- Click OK
+=== link:https://github.com/jenkinsci/configuration-as-code-plugin[Configuration as Code Plugin]
+
+[source,yaml]
+----
+credentials:
+ system:
+ domainCredentials:
+ - credentials:
+ - gitHubApp:
+ appID: "1111"
+ description: "GitHub app"
+ id: "github-app"
+ # apiUri: https://my-custom-github-enterprise.com/api/v3 # optional only required for GitHub enterprise
+ privateKey: "${GITHUB_APP_KEY}"
+----
+
== Configuring the github organization folder
See the link:https://docs.cloudbees.com/docs/admin-resources/latest/plugins/github-branch-source[main documentation]
@@ -94,6 +113,8 @@ for how to create a GitHub folder.
- Load the folders configuration page
- Select the GitHub app credentials in the 'Credentials field drop down
+- If you are using GitHub enterprise make sure the API url is set to your server,
+(note you currently need to set the API url on both the credential and the job).
After selecting the credential you should see:
From ff0327a40b7847230856c6de6c32bb9611016d64 Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Thu, 6 Feb 2020 16:31:43 +0000
Subject: [PATCH 17/18] Update docs/github-app.adoc
Co-Authored-By: Olivier Jacques
---
docs/github-app.adoc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/github-app.adoc b/docs/github-app.adoc
index fea6ac0fb..3ac990e9e 100644
--- a/docs/github-app.adoc
+++ b/docs/github-app.adoc
@@ -47,7 +47,7 @@ The only fields you need to fill out (currently) are:
Permissions this plugin uses:
- Commit statuses - Read and Write
-- Contents: Read-only (to read the Jenkinsfile)
+- Contents: Read-only (to read the `Jenkinsfile` and the repository content during `git fetch`). You may need "Read & write" to update the repository such as tagging releases
- Metadata: Read-only
- Pull requests: Read-only
- Webhooks (optional) - If you want the plugin to manage webhooks for you, Read and Write
From d33000aec5cf9758fba59ab8bcb3829894f85eea Mon Sep 17 00:00:00 2001
From: Tim Jacomb
Date: Mon, 17 Feb 2020 20:17:33 +0000
Subject: [PATCH 18/18] Apply suggestions from code review
Co-Authored-By: Oleg Nenashev
---
README.md | 2 +-
.../github_branch_source/GitHubAppCredentials/config.jelly | 2 +-
.../github_branch_source/GitHubAppCredentials/help-apiUri.html | 2 +-
.../github_branch_source/GitHubAppCredentials/help-appID.html | 2 +-
.../GitHubAppCredentials/help-privateKey.html | 2 +-
.../jenkinsci/plugins/github_branch_source/Messages.properties | 2 +-
.../jenkinsci/plugins/github_branch_source/JwtHelperTest.java | 2 +-
7 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 96c3259d7..6c7b3957b 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ GitHub users or organizations. Complete documentation is
### Guides
-* [GitHub app authentication](docs/github-app.adoc)
+* [GitHub App authentication](docs/github-app.adoc)
* [Extension points provided by this plugin](docs/implementation.adoc)
## Extension plugins
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/config.jelly b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/config.jelly
index 7544d9d58..5efc33569 100644
--- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/config.jelly
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/config.jelly
@@ -27,4 +27,4 @@
-
\ No newline at end of file
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-apiUri.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-apiUri.html
index 72fce8569..d2a930aa1 100644
--- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-apiUri.html
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-apiUri.html
@@ -1,3 +1,3 @@
GitHub API endpoint such as https://github.example.com/api/v3/
.
-
\ No newline at end of file
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-appID.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-appID.html
index d85fa8314..ca33b4078 100644
--- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-appID.html
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-appID.html
@@ -1,3 +1,3 @@
GitHub app ID, can be found on the App's settings, on the General page in the About section
-
\ No newline at end of file
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-privateKey.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-privateKey.html
index 00374b9d8..995e6f0fe 100644
--- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-privateKey.html
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-privateKey.html
@@ -3,4 +3,4 @@
You can convert it with openssl pkcs8 -topk8 -inform PEM -outform PEM -in current-key.pem -out new-key.pem -nocrypt
-
\ No newline at end of file
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/Messages.properties b/src/main/resources/org/jenkinsci/plugins/github_branch_source/Messages.properties
index e75daef7a..25c888f76 100644
--- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/Messages.properties
+++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/Messages.properties
@@ -67,4 +67,4 @@ ExcludeArchivedRepositoriesTrait.displayName=Exclude archived repositories
GitHubSCMNavigator.general=General
GitHubSCMNavigator.withinRepository=Within repository
-GitHubAppCredentials.displayName=GitHub app
\ No newline at end of file
+GitHubAppCredentials.displayName=GitHub App
diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/JwtHelperTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/JwtHelperTest.java
index 139ec5327..4fd37d0e8 100644
--- a/src/test/java/org/jenkinsci/plugins/github_branch_source/JwtHelperTest.java
+++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/JwtHelperTest.java
@@ -125,4 +125,4 @@ private static PublicKey getPublicKeyFromString(final String key) throws General
return kf.generatePublic(keySpecPKCS8);
}
-}
\ No newline at end of file
+}