package org.jenkinsci.plugins.github.config;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import hudson.security.ACL;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import hudson.util.Secret;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.github.internal.GitHubLoginFunction;
import org.jenkinsci.plugins.github.util.FluentIterableWrapper;
import org.jenkinsci.plugins.github.util.misc.NullSafeFunction;
import org.jenkinsci.plugins.github.util.misc.NullSafePredicate;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
import org.kohsuke.github.GitHub;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import static com.cloudbees.plugins.credentials.CredentialsMatchers.filter;
import static com.cloudbees.plugins.credentials.CredentialsMatchers.withId;
import static com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials;
import static com.cloudbees.plugins.credentials.domains.URIRequirementBuilder.fromUri;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
/**
* This object represents configuration of each credentials-github pair.
* If no api url explicitly defined, default url used.
* So one github server can be used with many creds and one token can be used multiply times in lot of gh servers
*
* @author lanwen (Merkushev Kirill)
* @since 1.13.0
*/
@XStreamAlias("github-server-config")
public class GitHubServerConfig extends AbstractDescribableImpl<GitHubServerConfig> {
private static final Logger LOGGER = LoggerFactory.getLogger(GitHubServerConfig.class);
/**
* Because of {@link GitHub} hide this const from external use we need to store it here
*/
public static final String GITHUB_URL = "https://api.github.com";
/**
* Used as default token value if no any creds found by given credsId.
*/
private static final String UNKNOWN_TOKEN = "UNKNOWN_TOKEN";
/**
* Default value in MB for client cache size
*
* @see #getClientCacheSize()
*/
public static final int DEFAULT_CLIENT_CACHE_SIZE_MB = 20;
private String apiUrl = GITHUB_URL;
private boolean manageHooks = true;
private final String credentialsId;
/**
* @see #getClientCacheSize()
* @see #setClientCacheSize(int)
*/
private int clientCacheSize = DEFAULT_CLIENT_CACHE_SIZE_MB;
/**
* To avoid creation of new one on every login with this config
*/
private transient GitHub cachedClient;
@DataBoundConstructor
public GitHubServerConfig(String credentialsId) {
this.credentialsId = credentialsId;
}
/**
* Set the API endpoint.
*
* @param apiUrl custom url if GH. Default value will be used in case of custom is unchecked or value is blank
*/
@DataBoundSetter
public void setApiUrl(String apiUrl) {
this.apiUrl = defaultIfBlank(apiUrl, GITHUB_URL);
}
/**
* This server config will be used to manage GH Hooks if true
*
* @param manageHooks false to ignore this config on hook auto-management
*/
@DataBoundSetter
public void setManageHooks(boolean manageHooks) {
this.manageHooks = manageHooks;
}
/**
* This method was introduced to hide custom api url under checkbox, but now UI simplified to show url all the time
* see jenkinsci/github-plugin/pull/112 for more details
*
* @param customApiUrl ignored
*
* @deprecated simply remove usage of this method, it ignored now. Should be removed after 20 sep 2016.
*/
@Deprecated
public void setCustomApiUrl(boolean customApiUrl) {
}
public String getApiUrl() {
return apiUrl;
}
public boolean isManageHooks() {
return manageHooks;
}
public String getCredentialsId() {
return credentialsId;
}
/**
* Capacity of cache for GitHub client in MB.
*
* Defaults to 20 MB
*
* @since 1.14.0
*/
public int getClientCacheSize() {
return clientCacheSize;
}
/**
* @param clientCacheSize capacity of cache for GitHub client in MB, set to <= 0 to turn off this feature
*/
@DataBoundSetter
public void setClientCacheSize(int clientCacheSize) {
this.clientCacheSize = clientCacheSize;
}
/**
* @return cached GH client or null
*/
private GitHub getCachedClient() {
return cachedClient;
}
/**
* Used by {@link org.jenkinsci.plugins.github.config.GitHubServerConfig.ClientCacheFunction}
*
* @param cachedClient updated client. Maybe null to invalidate cache
*/
private synchronized void setCachedClient(GitHub cachedClient) {
this.cachedClient = cachedClient;
}
/**
* Checks GH url for equality to default api url
*
* @param apiUrl should be not blank and not equal to default url to return true
*
* @return true if url not blank and not equal to default
*/
public static boolean isUrlCustom(String apiUrl) {
return isNotBlank(apiUrl) && !GITHUB_URL.equals(apiUrl);
}
/**
* Converts server config to authorized GH instance. If login process is not successful it returns null
*
* @return function to convert config to gh instance
* @see org.jenkinsci.plugins.github.config.GitHubServerConfig.ClientCacheFunction
*/
@CheckForNull
public static Function<GitHubServerConfig, GitHub> loginToGithub() {
return new ClientCacheFunction();
}
/**
* Extracts token from secret found by {@link #secretFor(String)}
* Returns {@link #UNKNOWN_TOKEN} if no any creds secret found with this id.
*
* @param credentialsId id to find creds
*
* @return token from creds or default non empty string
*/
@Nonnull
public static String tokenFor(String credentialsId) {
return secretFor(credentialsId).or(new Supplier<Secret>() {
@Override
public Secret get() {
return Secret.fromString(UNKNOWN_TOKEN);
}
}).getPlainText();
}
/**
* Tries to find {@link StringCredentials} by id and returns secret from it.
*
* @param credentialsId id to find creds
*
* @return secret from creds or empty optional
*/
@Nonnull
public static Optional<Secret> secretFor(String credentialsId) {
List<StringCredentials> creds = filter(
lookupCredentials(StringCredentials.class,
Jenkins.getInstance(), ACL.SYSTEM,
Collections.<DomainRequirement>emptyList()),
withId(trimToEmpty(credentialsId))
);
return FluentIterableWrapper.from(creds)
.transform(new NullSafeFunction<StringCredentials, Secret>() {
@Override
protected Secret applyNullSafe(@Nonnull StringCredentials input) {
return input.getSecret();
}
}).first();
}
/**
* Returns true if given host is part of stored (or default if blank) api url
*
* For example:
* withHost(api.github.com).apply(config for ~empty~) = true
* withHost(api.github.com).apply(config for api.github.com) = true
* withHost(api.github.com).apply(config for github.company.com) = false
*
* @param host host to find in api url
*
* @return predicate to match against {@link GitHubServerConfig}
*/
public static Predicate<GitHubServerConfig> withHost(final String host) {
return new NullSafePredicate<GitHubServerConfig>() {
@Override
protected boolean applyNullSafe(@Nonnull GitHubServerConfig github) {
return defaultIfEmpty(github.getApiUrl(), GITHUB_URL).contains(host);
}
};
}
/**
* Returns true if config can be used in hooks managing
*
* @return predicate to match against {@link GitHubServerConfig}
*/
public static Predicate<GitHubServerConfig> allowedToManageHooks() {
return new NullSafePredicate<GitHubServerConfig>() {
@Override
protected boolean applyNullSafe(@NonNull GitHubServerConfig github) {
return github.isManageHooks();
}
};
}
@Extension
public static class DescriptorImpl extends Descriptor<GitHubServerConfig> {
@Override
public String getDisplayName() {
return "GitHub Server";
}
@SuppressWarnings("unused")
public ListBoxModel doFillCredentialsIdItems(@QueryParameter String apiUrl,
@QueryParameter String credentialsId) {
if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) {
return new StandardListBoxModel().includeCurrentValue(credentialsId);
}
return new StandardListBoxModel()
.includeEmptyValue()
.includeMatchingAs(ACL.SYSTEM,
Jenkins.getInstance(),
StringCredentials.class,
fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build(),
CredentialsMatchers.always()
);
}
@SuppressWarnings("unused")
public FormValidation doVerifyCredentials(
@QueryParameter String apiUrl,
@QueryParameter String credentialsId) throws IOException {
GitHubServerConfig config = new GitHubServerConfig(credentialsId);
config.setApiUrl(apiUrl);
config.setClientCacheSize(0);
GitHub gitHub = new GitHubLoginFunction().apply(config);
try {
if (gitHub != null && gitHub.isCredentialValid()) {
return FormValidation.ok("Credentials verified for user %s, rate limit: %s",
gitHub.getMyself().getLogin(), gitHub.getRateLimit().remaining);
} else {
return FormValidation.error("Failed to validate the account");
}
} catch (IOException e) {
return FormValidation.error(e, "Failed to validate the account");
}
}
@SuppressWarnings("unused")
public FormValidation doCheckApiUrl(@QueryParameter String value) {
try {
new URL(value);
} catch (MalformedURLException e) {
return FormValidation.error("Malformed GitHub url (%s)", e.getMessage());
}
if (GITHUB_URL.equals(value)) {
return FormValidation.ok();
}
if (value.endsWith("/api/v3") || value.endsWith("/api/v3/")) {
return FormValidation.ok();
}
return FormValidation.warning("GitHub Enterprise API URL ends with \"/api/v3\"");
}
}
/**
* Function to get authorized GH client and cache it in config
* has {@link #loginToGithub()} static factory
*/
private static class ClientCacheFunction extends NullSafeFunction<GitHubServerConfig, GitHub> {
@Override
protected GitHub applyNullSafe(@Nonnull GitHubServerConfig github) {
if (github.getCachedClient() == null) {
github.setCachedClient(new GitHubLoginFunction().apply(github));
}
return github.getCachedClient();
}
}
}