diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java index 1dccb8e11..2fc888c58 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java @@ -14,15 +14,15 @@ import hudson.util.ListBoxModel; import hudson.util.Secret; import java.io.IOException; -import java.io.Serializable; import java.util.List; +import jenkins.security.SlaveToMasterCallable; +import jenkins.util.JenkinsJVM; 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; @@ -139,22 +139,22 @@ static String generateAppInstallationToken(String appId, String appPrivateKey, S } + @NonNull String actualApiUri() { + return Util.fixEmpty(apiUri) == null ? "https://api.github.com" : apiUri; + } + /** * {@inheritDoc} */ @NonNull @Override public Secret getPassword() { - if (Util.fixEmpty(apiUri) == null) { - apiUri = "https://api.github.com"; - } - long now = System.currentTimeMillis(); String appInstallationToken; if (cachedToken != null && now - tokenCacheTime < JwtHelper.VALIDITY_MS /* extra buffer */ / 2) { appInstallationToken = cachedToken; } else { - appInstallationToken = generateAppInstallationToken(appID, privateKey.getPlainText(), apiUri, owner); + appInstallationToken = generateAppInstallationToken(appID, privateKey.getPlainText(), actualApiUri(), owner); cachedToken = appInstallationToken; tokenCacheTime = now; } @@ -172,12 +172,16 @@ public String getUsername() { } /** - * Ensures that the credentials state as serialized via Remoting to an agent includes fields which are {@code transient} for purposes of XStream. - * This provides a ~2× performance improvement over reconstructing the object without that state, - * in the normal case that {@link #cachedToken} is valid and will remain valid for the brief time that elapses before the agent calls {@link #getPassword}: + * Ensures that the credentials state as serialized via Remoting to an agent calls back to the controller. + * Benefits: * + * Drawbacks: + * * @see CredentialsSnapshotTaker */ @@ -185,43 +189,63 @@ private Object writeReplace() { if (/* XStream */Channel.current() == null) { return this; } - return new Replacer(this); + return new DelegatingGitHubAppCredentials(this); } - private static final class Replacer implements Serializable { - - private final CredentialsScope scope; - private final String id; - private final String description; - private final String appID; - private final Secret privateKey; - private final String apiUri; - private final String owner; - private final String cachedToken; - private final long tokenCacheTime; - - Replacer(GitHubAppCredentials onMaster) { - scope = onMaster.getScope(); - id = onMaster.getId(); - description = onMaster.getDescription(); - appID = onMaster.appID; - privateKey = onMaster.privateKey; - apiUri = onMaster.apiUri; - owner = onMaster.owner; - cachedToken = onMaster.cachedToken; - tokenCacheTime = onMaster.tokenCacheTime; - } - - private Object readResolve() { - GitHubAppCredentials clone = new GitHubAppCredentials(scope, id, description, appID, privateKey); - clone.apiUri = apiUri; - clone.owner = owner; - clone.cachedToken = cachedToken; - clone.tokenCacheTime = tokenCacheTime; - return clone; - } + private static final class DelegatingGitHubAppCredentials extends BaseStandardCredentials implements StandardUsernamePasswordCredentials { - } + static final String SEP = "%%%"; + + private final String appID; + private final String data; + private transient Channel ch; + + DelegatingGitHubAppCredentials(GitHubAppCredentials onMaster) { + super(onMaster.getScope(), onMaster.getId(), onMaster.getDescription()); + JenkinsJVM.checkJenkinsJVM(); + appID = onMaster.appID; + data = Secret.fromString(onMaster.appID + SEP + onMaster.privateKey.getPlainText() + SEP + onMaster.actualApiUri() + SEP + onMaster.owner).getEncryptedValue(); + } + + private Object readResolve() { + JenkinsJVM.checkNotJenkinsJVM(); + ch = Channel.currentOrFail(); + return this; + } + + @Override + public String getUsername() { + return appID; + } + + @Override + public Secret getPassword() { + JenkinsJVM.checkNotJenkinsJVM(); + try { + return ch.call(new GetPassword(data)); + } catch (IOException | InterruptedException x) { + throw new RuntimeException(x); + } + } + + private static final class GetPassword extends SlaveToMasterCallable { + + private final String data; + + GetPassword(String data) { + this.data = data; + } + + @Override + public Secret call() throws RuntimeException { + JenkinsJVM.checkJenkinsJVM(); + String[] fields = Secret.fromString(data).getPlainText().split(SEP); + return Secret.fromString(generateAppInstallationToken(fields[0], fields[1], fields[2], fields[3])); + } + + } + + } /** * {@inheritDoc}