package org.jenkinsci.plugins.ghprb;
import static hudson.Util.fixEmpty;
import static hudson.Util.fixEmptyAndTrim;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.kohsuke.github.GHAuthorization;
import org.kohsuke.github.GHCommitState;
import org.kohsuke.github.GHIssue;
import org.kohsuke.github.GHMyself;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.export.Exported;
import com.cloudbees.plugins.credentials.CredentialsMatcher;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.IdCredentials;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;
import com.google.common.base.Joiner;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
import hudson.Extension;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import hudson.model.Item;
import hudson.security.ACL;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import hudson.util.Secret;
public class GhprbGitHubAuth extends AbstractDescribableImpl<GhprbGitHubAuth> {
private static final Logger logger = Logger.getLogger(GhprbGitHubAuth.class.getName());
@Extension
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
private final String serverAPIUrl;
private final String jenkinsUrl;
private final String credentialsId;
private final String id;
private final String description;
private final Secret secret;
private transient GitHub gh;
@DataBoundConstructor
public GhprbGitHubAuth(
String serverAPIUrl,
String jenkinsUrl,
String credentialsId,
String description,
String id,
Secret secret
) {
if (StringUtils.isEmpty(serverAPIUrl)) {
serverAPIUrl = "https://api.github.com";
}
this.serverAPIUrl = fixEmptyAndTrim(serverAPIUrl);
this.jenkinsUrl = fixEmptyAndTrim(jenkinsUrl);
this.credentialsId = fixEmpty(credentialsId);
if (StringUtils.isEmpty(id)) {
id = UUID.randomUUID().toString();
}
this.id = IdCredentials.Helpers.fixEmptyId(id);
this.description = description;
this.secret = secret;
}
@Exported
public String getServerAPIUrl() {
return serverAPIUrl;
}
@Exported
public String getJenkinsUrl() {
return jenkinsUrl;
}
@Exported
public String getCredentialsId() {
return credentialsId;
}
@Exported
public String getDescription() {
return description;
}
@Exported
public String getId() {
return id;
}
@Exported
public Secret getSecret() {
return secret;
}
public boolean checkSignature(String body, String signature) {
if (secret == null || StringUtils.isEmpty(secret.getPlainText())) {
return true;
}
if (signature != null && signature.startsWith("sha1=")) {
String expected = signature.substring(5);
String algorithm = "HmacSHA1";
try {
SecretKeySpec keySpec = new SecretKeySpec(secret.getPlainText().getBytes(Charset.forName("UTF-8")), algorithm);
Mac mac = Mac.getInstance(algorithm);
mac.init(keySpec);
byte[] localSignatureBytes = mac.doFinal(body.getBytes("UTF-8"));
String localSignature = Hex.encodeHexString(localSignatureBytes);
if (! localSignature.equals(expected)) {
logger.log(Level.SEVERE, "Local signature {0} does not match external signature {1}",
new Object[] {localSignature, expected});
return false;
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Couldn't match both signatures");
return false;
}
} else {
logger.log(Level.SEVERE, "Request doesn't contain a signature. Check that github has a secret that should be attached to the hook");
return false;
}
logger.log(Level.INFO, "Signatures checking OK");
return true;
}
private static GitHubBuilder getBuilder(Item context, String serverAPIUrl, String credentialsId) {
GitHubBuilder builder = new GitHubBuilder()
.withEndpoint(serverAPIUrl)
.withConnector(new HttpConnectorWithJenkinsProxy());
String contextName = context == null ? "(Jenkins.instance)" : context.getFullDisplayName();
if (StringUtils.isEmpty(credentialsId)) {
logger.log(Level.WARNING, "credentialsId not set for context {0}, using anonymous connection", contextName);
return builder;
}
StandardCredentials credentials = Ghprb.lookupCredentials(context, credentialsId, serverAPIUrl);
if (credentials == null) {
logger.log(Level.SEVERE, "Failed to look up credentials for context {0} using id: {1}",
new Object[] { contextName, credentialsId });
} else if (credentials instanceof StandardUsernamePasswordCredentials) {
logger.log(Level.FINEST, "Using username/password for context {0}", contextName);
StandardUsernamePasswordCredentials upCredentials = (StandardUsernamePasswordCredentials) credentials;
builder.withPassword(upCredentials.getUsername(), upCredentials.getPassword().getPlainText());
} else if (credentials instanceof StringCredentials) {
logger.log(Level.FINEST, "Using OAuth token for context {0}", contextName);
StringCredentials tokenCredentials = (StringCredentials) credentials;
builder.withOAuthToken(tokenCredentials.getSecret().getPlainText());
} else {
logger.log(Level.SEVERE, "Unknown credential type for context {0} using id: {1}: {2}",
new Object[] { contextName, credentialsId, credentials.getClass().getName() });
return null;
}
return builder;
}
private void buildConnection(Item context) {
GitHubBuilder builder = getBuilder(context, serverAPIUrl, credentialsId);
if (builder == null) {
logger.log(Level.SEVERE, "Unable to get builder using credentials: {0}", credentialsId);
return;
}
try {
gh = builder.build();
} catch (IOException e) {
logger.log(Level.SEVERE, "Unable to connect using credentials: " + credentialsId, e);
}
}
public GitHub getConnection(Item context) throws IOException {
synchronized (this) {
if (gh == null) {
buildConnection(context);
}
return gh;
}
}
@Override
public DescriptorImpl getDescriptor() {
return DESCRIPTOR;
}
public static final class DescriptorImpl extends Descriptor<GhprbGitHubAuth> {
@Override
public String getDisplayName() {
return "GitHub Auth";
}
/**
* Stapler helper method.
*
* @param context the context.
* @param serverAPIUrl the github api server url.
* @param credentialsId the credentialsId from the credentials plugin
* @return list box model.
* @throws URISyntaxException If the url is bad
*/
public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item context, @QueryParameter String serverAPIUrl, @QueryParameter String credentialsId) throws URISyntaxException {
List<DomainRequirement> domainRequirements = URIRequirementBuilder.fromUri(serverAPIUrl).build();
List<CredentialsMatcher> matchers = new ArrayList<CredentialsMatcher>(3);
if (!StringUtils.isEmpty(credentialsId)) {
matchers.add(0, CredentialsMatchers.withId(credentialsId));
}
matchers.add(CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class));
matchers.add(CredentialsMatchers.instanceOf(StringCredentials.class));
List<StandardCredentials> credentials = CredentialsProvider.lookupCredentials(
StandardCredentials.class,
context,
ACL.SYSTEM,
domainRequirements
);
return new StandardListBoxModel()
.withMatching(
CredentialsMatchers.anyOf(
matchers.toArray(new CredentialsMatcher[0])),
credentials
);
}
public FormValidation doCreateApiToken(
@QueryParameter("serverAPIUrl") final String serverAPIUrl,
@QueryParameter("credentialsId") final String credentialsId,
@QueryParameter("username") final String username,
@QueryParameter("password") final String password) {
try {
GitHubBuilder builder = new GitHubBuilder()
.withEndpoint(serverAPIUrl)
.withConnector(new HttpConnectorWithJenkinsProxy());
if (StringUtils.isEmpty(credentialsId)) {
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
return FormValidation.error("Username and Password required");
}
builder.withPassword(username, password);
} else {
StandardCredentials credentials = Ghprb.lookupCredentials(null, credentialsId, serverAPIUrl);
if (credentials instanceof StandardUsernamePasswordCredentials) {
StandardUsernamePasswordCredentials upCredentials = (StandardUsernamePasswordCredentials) credentials;
builder.withPassword(upCredentials.getUsername(), upCredentials.getPassword().getPlainText());
} else {
return FormValidation.error("No username/password credentials provided");
}
}
GitHub gh = builder.build();
GHAuthorization token = gh.createToken(Arrays.asList(GHAuthorization.REPO_STATUS,
GHAuthorization.REPO), "Jenkins GitHub Pull Request Builder", null);
String tokenId;
try {
tokenId = Ghprb.createCredentials(serverAPIUrl, token.getToken());
} catch (Exception e) {
tokenId = "Unable to create credentials: " + e.getMessage();
}
return FormValidation.ok("Access token created: " + token.getToken() + " token CredentialsID: " + tokenId);
} catch (IOException ex) {
return FormValidation.error("GitHub API token couldn't be created: " + ex.getMessage());
}
}
public FormValidation doCheckServerAPIUrl(@QueryParameter String value) {
if ("https://api.github.com".equals(value)) {
return FormValidation.ok();
}
if (value.endsWith("/api/v3") || value.endsWith("/api/v3/")) {
return FormValidation.ok();
}
return FormValidation.warning("GitHub API URI is \"https://api.github.com\". GitHub Enterprise API URL ends with \"/api/v3\"");
}
public FormValidation doCheckRepoAccess(
@QueryParameter("serverAPIUrl") final String serverAPIUrl,
@QueryParameter("credentialsId") final String credentialsId,
@QueryParameter("repo") final String repo) {
try {
GitHubBuilder builder = getBuilder(null, serverAPIUrl, credentialsId);
if (builder == null) {
return FormValidation.error("Unable to look up GitHub credentials using ID: " + credentialsId + "!!");
}
GitHub gh = builder.build();
GHRepository repository = gh.getRepository(repo);
StringBuilder sb = new StringBuilder();
sb.append("User has access to: ");
List<String> permissions = new ArrayList<String>(3);
if (repository.hasAdminAccess()) {
permissions.add("Admin");
}
if (repository.hasPushAccess()) {
permissions.add("Push");
}
if (repository.hasPullAccess()) {
permissions.add("Pull");
}
sb.append(Joiner.on(", ").join(permissions));
return FormValidation.ok(sb.toString());
} catch (Exception ex) {
return FormValidation.error("Unable to connect to GitHub API: " + ex);
}
}
public FormValidation doTestGithubAccess(
@QueryParameter("serverAPIUrl") final String serverAPIUrl,
@QueryParameter("credentialsId") final String credentialsId) {
try {
GitHubBuilder builder = getBuilder(null, serverAPIUrl, credentialsId);
if (builder == null) {
return FormValidation.error("Unable to look up GitHub credentials using ID: " + credentialsId + "!!");
}
GitHub gh = builder.build();
GHMyself me = gh.getMyself();
String name = me.getName();
String email = me.getEmail();
String login = me.getLogin();
String comment = String.format("Connected to %s as %s (%s) login: %s", serverAPIUrl, name, email, login);
return FormValidation.ok(comment);
} catch (Exception ex) {
return FormValidation.error("Unable to connect to GitHub API: " + ex);
}
}
public FormValidation doTestComment(
@QueryParameter("serverAPIUrl") final String serverAPIUrl,
@QueryParameter("credentialsId") final String credentialsId,
@QueryParameter("repo") final String repoName,
@QueryParameter("issueId") final int issueId,
@QueryParameter("message1") final String comment) {
try {
GitHubBuilder builder = getBuilder(null, serverAPIUrl, credentialsId);
if (builder == null) {
return FormValidation.error("Unable to look up GitHub credentials using ID: " + credentialsId + "!!");
}
GitHub gh = builder.build();
GHRepository repo = gh.getRepository(repoName);
GHIssue issue = repo.getIssue(issueId);
issue.comment(comment);
return FormValidation.ok("Issued comment to issue: " + issue.getHtmlUrl());
} catch (Exception ex) {
return FormValidation.error("Unable to issue comment: " + ex);
}
}
public FormValidation doTestUpdateStatus(
@QueryParameter("serverAPIUrl") final String serverAPIUrl,
@QueryParameter("credentialsId") final String credentialsId,
@QueryParameter("repo") final String repoName,
@QueryParameter("sha1") final String sha1,
@QueryParameter("state") final GHCommitState state,
@QueryParameter("url") final String url,
@QueryParameter("message2") final String message,
@QueryParameter("context") final String context) {
try {
GitHubBuilder builder = getBuilder(null, serverAPIUrl, credentialsId);
if (builder == null) {
return FormValidation.error("Unable to look up GitHub credentials using ID: " + credentialsId + "!!");
}
GitHub gh = builder.build();
GHRepository repo = gh.getRepository(repoName);
repo.createCommitStatus(sha1, state, url, message, context);
return FormValidation.ok("Updated status of: " + sha1);
} catch (Exception ex) {
return FormValidation.error("Unable to update status: " + ex);
}
}
public ListBoxModel doFillStateItems(@QueryParameter("state") String state) {
ListBoxModel items = new ListBoxModel();
for (GHCommitState commitState : GHCommitState.values()) {
items.add(commitState.toString(), commitState.toString());
if (state.equals(commitState.toString())) {
items.get(items.size() - 1).selected = true;
}
}
return items;
}
}
}