package org.jenkinsci.plugins.github.config; import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.SystemCredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.common.StandardUsernameListBoxModel; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.domains.DomainSpecification; import com.cloudbees.plugins.credentials.domains.HostnameSpecification; import com.cloudbees.plugins.credentials.domains.SchemeSpecification; import com.google.common.collect.ImmutableList; import hudson.Extension; import hudson.model.Describable; 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.plaincredentials.impl.StringCredentialsImpl; import org.kohsuke.github.GHAuthorization; import org.kohsuke.github.GitHub; import org.kohsuke.github.GitHubBuilder; import org.kohsuke.stapler.QueryParameter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; import java.net.URI; import java.util.List; import java.util.UUID; import static com.cloudbees.plugins.credentials.CredentialsMatchers.firstOrNull; 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 java.lang.String.format; import static java.util.Arrays.asList; import static org.apache.commons.lang3.StringUtils.defaultIfBlank; import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.apache.commons.lang3.Validate.notNull; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.GITHUB_URL; import static org.kohsuke.github.GHAuthorization.AMIN_HOOK; import static org.kohsuke.github.GHAuthorization.REPO; import static org.kohsuke.github.GHAuthorization.REPO_STATUS; /** * Helper class to convert username+password credentials or directly login+password to GH token * and save it as token credentials with help of plain-credentials plugin * * @author lanwen (Merkushev Kirill) * @since 1.13.0 */ @Extension public class GitHubTokenCredentialsCreator extends Descriptor<GitHubTokenCredentialsCreator> implements Describable<GitHubTokenCredentialsCreator> { private static final Logger LOGGER = LoggerFactory.getLogger(GitHubTokenCredentialsCreator.class); /** * Default scope required for this plugin. * * - admin:repo_hook - for managing hooks (read, write and delete old ones) * - repo - to see private repos * - repo:status - to manipulate commit statuses */ public static final List<String> GH_PLUGIN_REQUIRED_SCOPE = ImmutableList.of( AMIN_HOOK, REPO, REPO_STATUS ); public GitHubTokenCredentialsCreator() { super(GitHubTokenCredentialsCreator.class); } @Override public GitHubTokenCredentialsCreator getDescriptor() { return this; } @Override public String getDisplayName() { return "Convert login and password to token"; } @SuppressWarnings("unused") public ListBoxModel doFillCredentialsIdItems(@QueryParameter String apiUrl, @QueryParameter String credentialsId) { if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) { return new StandardUsernameListBoxModel().includeCurrentValue(credentialsId); } return new StandardUsernameListBoxModel() .includeEmptyValue() .includeMatchingAs( ACL.SYSTEM, Jenkins.getInstance(), StandardUsernamePasswordCredentials.class, fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build(), CredentialsMatchers.always() ) .includeMatchingAs( Jenkins.getAuthentication(), Jenkins.getInstance(), StandardUsernamePasswordCredentials.class, fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build(), CredentialsMatchers.always() ); } @SuppressWarnings("unused") public FormValidation doCreateTokenByCredentials( @QueryParameter String apiUrl, @QueryParameter String credentialsId) { if (isEmpty(credentialsId)) { return FormValidation.error("Please specify credentials to create token"); } StandardUsernamePasswordCredentials creds = firstOrNull(lookupCredentials( StandardUsernamePasswordCredentials.class, Jenkins.getInstance(), ACL.SYSTEM, fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build()), withId(credentialsId)); if (creds == null) { // perhaps they selected a personal credential for convertion creds = firstOrNull(lookupCredentials( StandardUsernamePasswordCredentials.class, Jenkins.getInstance(), Jenkins.getAuthentication(), fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build()), withId(credentialsId)); } GHAuthorization token; try { token = createToken( notNull(creds, "Why selected creds is null?").getUsername(), creds.getPassword().getPlainText(), defaultIfBlank(apiUrl, GITHUB_URL) ); } catch (IOException e) { return FormValidation.error(e, "Can't create GH token - %s", e.getMessage()); } StandardCredentials credentials = createCredentials(apiUrl, token.getToken(), creds.getUsername()); return FormValidation.ok("Created credentials with id %s (can use it for GitHub Server Config)", credentials.getId()); } @SuppressWarnings("unused") public FormValidation doCreateTokenByPassword( @QueryParameter String apiUrl, @QueryParameter String login, @QueryParameter String password) { try { GHAuthorization token = createToken(login, password, defaultIfBlank(apiUrl, GITHUB_URL)); StandardCredentials credentials = createCredentials(apiUrl, token.getToken(), login); return FormValidation.ok( "Created credentials with id %s (can use it for GitHub Server Config)", credentials.getId()); } catch (IOException e) { return FormValidation.error(e, "Can't create GH token for %s - %s", login, e.getMessage()); } } /** * Can be used to convert given login and password to GH personal token as more secured way to interact with api * * @param username gh login * @param password gh password * @param apiUrl gh api url. Can be null or empty to default * * @return personal token with requested scope * @throws IOException when can't create token with given creds */ public GHAuthorization createToken(@Nonnull String username, @Nonnull String password, @Nullable String apiUrl) throws IOException { GitHub gitHub = new GitHubBuilder() .withEndpoint(defaultIfBlank(apiUrl, GITHUB_URL)) .withPassword(username, password) .build(); return gitHub.createToken( GH_PLUGIN_REQUIRED_SCOPE, format("Jenkins GitHub Plugin token (%s)", Jenkins.getInstance().getRootUrl()), Jenkins.getInstance().getRootUrl() ); } /** * Creates {@link org.jenkinsci.plugins.plaincredentials.StringCredentials} with previously created GH token. * Adds them to domain extracted from server url (will be generated if no any exists before). * Domain will have domain requirements consists of scheme and host from serverAPIUrl arg * * @param serverAPIUrl to add to domain with host and scheme requirement from this url * @param token GH Personal token * @param username used to add to description of newly created creds * * @return credentials object * @see #createCredentials(String, StandardCredentials) */ public StandardCredentials createCredentials(@Nullable String serverAPIUrl, String token, String username) { String url = defaultIfBlank(serverAPIUrl, GITHUB_URL); String description = format("GitHub (%s) auto generated token credentials for %s", url, username); StringCredentialsImpl creds = new StringCredentialsImpl( CredentialsScope.GLOBAL, UUID.randomUUID().toString(), description, Secret.fromString(token)); return createCredentials(url, creds); } /** * Saves given creds in jenkins for domain extracted from server api url * * @param serverAPIUrl to extract (and create if no any) domain * @param credentials creds to save * * @return saved creds */ private StandardCredentials createCredentials(@Nonnull String serverAPIUrl, final StandardCredentials credentials) { URI serverUri = URI.create(defaultIfBlank(serverAPIUrl, GITHUB_URL)); List<DomainSpecification> specifications = asList( new SchemeSpecification(serverUri.getScheme()), new HostnameSpecification(serverUri.getHost(), null) ); final Domain domain = new Domain(serverUri.getHost(), "GitHub domain (autogenerated)", specifications); ACL.impersonate(ACL.SYSTEM, new Runnable() { // do it with system rights @Override public void run() { try { new SystemCredentialsProvider.StoreImpl().addDomain(domain, credentials); } catch (IOException e) { LOGGER.error("Can't add creds for domain", e); } } }); return credentials; } }