Skip to content
Permalink
Browse files
[FIXED JENKINS-40451] Respect the API endpoint for credentials domains
- Also make the API Endpoint visible when there is more that one API endpoint to select from
  • Loading branch information
stephenc committed Dec 14, 2016
1 parent d2df1ec commit b7788c606b43f29fc91d277de81a67a65abe39d4
Showing 5 changed files with 137 additions and 86 deletions.
@@ -27,6 +27,7 @@
import com.cloudbees.jenkins.GitHubWebHook;
import com.cloudbees.plugins.credentials.CredentialsMatcher;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsNameProvider;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
@@ -38,13 +39,20 @@
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.OkUrlFactory;
import hudson.Util;
import hudson.model.Item;
import hudson.model.Queue;
import hudson.model.queue.Tasks;
import hudson.security.ACL;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

@@ -59,34 +67,119 @@
import org.kohsuke.github.extras.OkHttpConnector;

import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.jenkinsci.plugins.github.config.GitHubServerConfig.GITHUB_URL;

/**
* Utilities that could perhaps be moved into {@code github-api}.
*/
public class Connector {
private static final Logger LOGGER = Logger.getLogger(Connector.class.getName());

public static @CheckForNull StandardCredentials lookupScanCredentials(@CheckForNull SCMSourceOwner context, @CheckForNull String apiUri, @CheckForNull String scanCredentialsId) {
private Connector() {
throw new IllegalAccessError("Utility class");
}

public static ListBoxModel listScanCredentials(SCMSourceOwner context, String apiUri) {
return new StandardListBoxModel()
.includeEmptyValue()
.includeMatchingAs(
context instanceof Queue.Task
? Tasks.getDefaultAuthenticationOf((Queue.Task) context)
: ACL.SYSTEM,
context,
StandardUsernameCredentials.class,
githubDomainRequirements(apiUri),
githubScanCredentialsMatcher()
);
}

public static FormValidation checkScanCredentials(SCMSourceOwner context, String apiUri, String scanCredentialsId) {
if (context == null && !Jenkins.getActiveInstance().hasPermission(Jenkins.ADMINISTER) ||
context != null && !context.hasPermission(Item.EXTENDED_READ)) {
return FormValidation.ok();
}
if (!scanCredentialsId.isEmpty()) {
ListBoxModel options = listScanCredentials(context, apiUri);
boolean found = false;
for (ListBoxModel.Option b: options) {
if (scanCredentialsId.equals(b.value)) {
found = true;
break;
}
}
if (!found) {
return FormValidation.error("Credentials not found");
}
if (!(context.hasPermission(Item.CONFIGURE)
|| context.hasPermission(Item.BUILD)
|| context.hasPermission(CredentialsProvider.USE_ITEM))) {
return FormValidation.ok("Credentials found");
}
StandardCredentials credentials = Connector.lookupScanCredentials(context, apiUri, scanCredentialsId);
if (credentials == null) {
return FormValidation.error("Credentials not found");
} else {
try {
GitHub connector = Connector.connect(apiUri, credentials);
if (connector.isCredentialValid()) {
return FormValidation.ok();
} else {
return FormValidation.error("Invalid credentials");
}
} catch (IOException e) {
// ignore, never thrown
LOGGER.log(Level.WARNING, "Exception validating credentials {0} on {1}", new Object[]{
CredentialsNameProvider.name(credentials), apiUri
});
return FormValidation.error("Exception validating credentials");
}
}
} else {
return FormValidation.warning("Credentials are recommended");
}
}

@CheckForNull
public static StandardCredentials lookupScanCredentials(@CheckForNull SCMSourceOwner context,
@CheckForNull String apiUri,
@CheckForNull String scanCredentialsId) {
if (Util.fixEmpty(scanCredentialsId) == null) {
return null;
} else {
return CredentialsMatchers.firstOrNull(
CredentialsProvider.lookupCredentials(
StandardUsernameCredentials.class,
context,
ACL.SYSTEM,
context instanceof Queue.Task
? Tasks.getDefaultAuthenticationOf((Queue.Task) context)
: ACL.SYSTEM,
githubDomainRequirements(apiUri)
),
CredentialsMatchers.allOf(CredentialsMatchers.withId(scanCredentialsId), githubScanCredentialsMatcher())
);
}
}

public static ListBoxModel listCheckoutCredentials(SCMSourceOwner context, String apiUri) {
StandardListBoxModel result = new StandardListBoxModel();
result.includeEmptyValue();
result.add("- same as scan credentials -", GitHubSCMSource.DescriptorImpl.SAME);
result.add("- anonymous -", GitHubSCMSource.DescriptorImpl.ANONYMOUS);
return result.includeMatchingAs(
context instanceof Queue.Task
? Tasks.getDefaultAuthenticationOf((Queue.Task) context)
: ACL.SYSTEM,
context,
StandardUsernameCredentials.class,
githubDomainRequirements(apiUri),
GitClient.CREDENTIALS_MATCHER
);
}

public static @Nonnull GitHub connect(@CheckForNull String apiUri, @CheckForNull StandardCredentials credentials) throws IOException {
String apiUrl = Util.fixEmptyAndTrim(apiUri);
String host;
try {
apiUrl = apiUrl != null ? apiUrl : GITHUB_URL;
apiUrl = apiUrl != null ? apiUrl : GitHubServerConfig.GITHUB_URL;
host = new URL(apiUrl).getHost();
} catch (MalformedURLException e) {
throw new IOException("Invalid GitHub API URL: " + apiUrl, e);
@@ -113,14 +206,6 @@ public class Connector {
return gb.build();
}

public static void fillScanCredentialsIdItems(StandardListBoxModel result, @CheckForNull SCMSourceOwner context, @CheckForNull String apiUri) {
result.withMatching(githubScanCredentialsMatcher(), CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, context, ACL.SYSTEM, githubDomainRequirements(apiUri)));
}

public static void fillCheckoutCredentialsIdItems(StandardListBoxModel result, @CheckForNull SCMSourceOwner context, @CheckForNull String apiUri) {
result.withMatching(GitClient.CREDENTIALS_MATCHER, CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, context, ACL.SYSTEM, githubDomainRequirements(apiUri)));
}

private static CredentialsMatcher githubScanCredentialsMatcher() {
// TODO OAuth credentials
return CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class));
@@ -159,9 +244,6 @@ private static String hashed(GitHubServerConfig config) {
.putString(trimToEmpty(config.getCredentialsId())).hash().toString();
}


private Connector() {}

/**
* Fail immediately and throw a customized exception.
*/
@@ -27,7 +27,6 @@
import com.cloudbees.jenkins.GitHubWebHook;
import com.cloudbees.plugins.credentials.CredentialsNameProvider;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.AbortException;
@@ -558,42 +557,17 @@ protected SCMSourceCategory[] createCategories() {

@Restricted(NoExternalUse.class)
public FormValidation doCheckScanCredentialsId(@AncestorInPath SCMSourceOwner context,
@QueryParameter String scanCredentialsId, @QueryParameter String apiUri) {
if (!scanCredentialsId.isEmpty()) {
StandardCredentials credentials = Connector.lookupScanCredentials(context, apiUri, scanCredentialsId);
if (credentials == null) {
return FormValidation.error("Credentials not found");
} else {
try {
GitHub connector = Connector.connect(apiUri, credentials);
if (connector.isCredentialValid()) {
return FormValidation.ok();
} else {
return FormValidation.error("Invalid credentials");
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Exception validating credentials " + CredentialsNameProvider.name(credentials) + " on " + apiUri);
return FormValidation.error("Exception validating credentials");
}
}
} else {
return FormValidation.warning("Credentials are recommended");
}
@QueryParameter String apiUri,
@QueryParameter String scanCredentialsId) {
return Connector.checkScanCredentials(context, apiUri, scanCredentialsId);
}

public ListBoxModel doFillScanCredentialsIdItems(@AncestorInPath SCMSourceOwner context/* TODO , @QueryParameter String apiUri*/) {
StandardListBoxModel result = new StandardListBoxModel();
result.withEmptySelection();
Connector.fillScanCredentialsIdItems(result, context, null);
return result;
public ListBoxModel doFillScanCredentialsIdItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String apiUri) {
return Connector.listScanCredentials(context, apiUri);
}

public ListBoxModel doFillCheckoutCredentialsIdItems(@AncestorInPath SCMSourceOwner context/* TODO , @QueryParameter String apiUri*/) {
StandardListBoxModel result = new StandardListBoxModel();
result.add("- same as scan credentials -", GitHubSCMSource.DescriptorImpl.SAME);
result.add("- anonymous -", GitHubSCMSource.DescriptorImpl.ANONYMOUS);
Connector.fillCheckoutCredentialsIdItems(result, context, null);
return result;
public ListBoxModel doFillCheckoutCredentialsIdItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String apiUri) {
return Connector.listCheckoutCredentials(context, apiUri);
}

public ListBoxModel doFillApiUriItems() {
@@ -605,6 +579,10 @@ public ListBoxModel doFillApiUriItems() {
return result;
}

public boolean isApiUriSelectable() {
return !GitHubConfiguration.get().getEndpoints().isEmpty();
}

// TODO repeating configuration blocks like this is clumsy; better to factor shared config into a Describable and use f:property

@Restricted(NoExternalUse.class)
@@ -30,7 +30,6 @@
import com.cloudbees.plugins.credentials.CredentialsNameProvider;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
@@ -1018,28 +1017,9 @@ public FormValidation doCheckIncludes(@QueryParameter String value) {

@Restricted(NoExternalUse.class)
public FormValidation doCheckScanCredentialsId(@AncestorInPath SCMSourceOwner context,
@QueryParameter String scanCredentialsId, @QueryParameter String apiUri) {
if (!scanCredentialsId.isEmpty()) {
StandardCredentials credentials = Connector.lookupScanCredentials(context, apiUri, scanCredentialsId);
if (credentials == null) {
return FormValidation.error("Credentials not found");
} else {
try {
GitHub connector = Connector.connect(apiUri, credentials);
if (connector.isCredentialValid()) {
return FormValidation.ok();
} else {
return FormValidation.error("Invalid credentials");
}
} catch (IOException e) {
// ignore, never thrown
LOGGER.log(Level.WARNING, "Exception validating credentials {0} on {1}", new Object[] {CredentialsNameProvider.name(credentials), apiUri});
return FormValidation.error("Exception validating credentials");
}
}
} else {
return FormValidation.warning("Credentials are recommended");
}
@QueryParameter String apiUri,
@QueryParameter String scanCredentialsId) {
return Connector.checkScanCredentials(context, apiUri, scanCredentialsId);
}

@Restricted(NoExternalUse.class)
@@ -1096,19 +1076,16 @@ public ListBoxModel doFillApiUriItems() {
return result;
}

public boolean isApiUriSelectable() {
return !GitHubConfiguration.get().getEndpoints().isEmpty();
}

public ListBoxModel doFillCheckoutCredentialsIdItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String apiUri) {
StandardListBoxModel result = new StandardListBoxModel();
result.add("- same as scan credentials -", SAME);
result.add("- anonymous -", ANONYMOUS);
Connector.fillCheckoutCredentialsIdItems(result, context, apiUri);
return result;
return Connector.listCheckoutCredentials(context, apiUri);
}

public ListBoxModel doFillScanCredentialsIdItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String apiUri) {
StandardListBoxModel result = new StandardListBoxModel();
result.withEmptySelection();
Connector.fillScanCredentialsIdItems(result, context, apiUri);
return result;
return Connector.listScanCredentials(context, apiUri);
}

public ListBoxModel doFillRepositoryItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String apiUri,
@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:c="/lib/credentials">
<j:if test="${descriptor.apiUriSelectable}">
<f:entry title="${%API endpoint}" field="apiUri">
<f:select/>
</f:entry>
</j:if>
<f:entry title="${%Owner}" field="repoOwner">
<f:textbox/>
</f:entry>
@@ -11,9 +16,11 @@
<f:textbox default=".*"/>
</f:entry>
<f:advanced>
<f:entry title="${%API endpoint}" field="apiUri">
<f:select/>
</f:entry>
<j:if test="${!descriptor.apiUriSelectable}">
<f:entry title="${%API endpoint}" field="apiUri">
<f:select/>
</f:entry>
</j:if>
<f:entry title="${%Checkout credentials}" field="checkoutCredentialsId">
<c:select default="${descriptor.SAME}"/>
</f:entry>
@@ -1,5 +1,10 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<j:if test="${descriptor.apiUriSelectable}">
<f:entry title="${%API endpoint}" field="apiUri">
<f:select/>
</f:entry>
</j:if>
<f:entry title="${%Owner}" field="repoOwner">
<f:textbox/>
</f:entry>
@@ -10,9 +15,11 @@
<f:select/>
</f:entry>
<f:advanced>
<f:entry title="${%API endpoint}" field="apiUri">
<f:select/>
</f:entry>
<j:if test="${!descriptor.apiUriSelectable}">
<f:entry title="${%API endpoint}" field="apiUri">
<f:select/>
</f:entry>
</j:if>
<f:entry title="${%Checkout credentials}" field="checkoutCredentialsId">
<f:select default="${descriptor.SAME}"/>
</f:entry>

0 comments on commit b7788c6

Please sign in to comment.