package hudson.plugins.tfs; import com.cloudbees.plugins.credentials.CredentialsMatcher; import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsProvider; 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.HostnameRequirement; import com.microsoft.tfs.core.exceptions.TFSUnauthorizedException; import hudson.Extension; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; import hudson.plugins.tfs.model.ListOfGitRepositories; import hudson.plugins.tfs.model.MockableVersionControlClient; import hudson.plugins.tfs.model.Server; import hudson.plugins.tfs.util.StringHelper; import hudson.plugins.tfs.util.TeamRestClient; import hudson.plugins.tfs.util.UriHelper; import hudson.security.ACL; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import jenkins.model.Jenkins; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; import java.util.List; import java.util.logging.Logger; public class TeamCollectionConfiguration extends AbstractDescribableImpl<TeamCollectionConfiguration> { private static final Logger LOGGER = Logger.getLogger(TeamCollectionConfiguration.class.getName()); private final String collectionUrl; private final String credentialsId; @DataBoundConstructor public TeamCollectionConfiguration(final String collectionUrl, final String credentialsId) { this.collectionUrl = collectionUrl; this.credentialsId = credentialsId; } public String getCollectionUrl() { return collectionUrl; } public String getCredentialsId() { return credentialsId; } @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } @Extension public static class DescriptorImpl extends Descriptor<TeamCollectionConfiguration> { @Override public String getDisplayName() { return "Team Project Collection"; } @SuppressWarnings("unused") public FormValidation doCheckCollectionUrl( @QueryParameter final String value) { if (StringUtils.isBlank(value)) { return FormValidation.warning("Please provide a value"); } final URI uri; try { uri = new URI(value); } catch (final URISyntaxException e) { return FormValidation.error("Malformed TFS/Team Services collection URL (%s)", e.getMessage()); } final String hostName = uri.getHost(); if (StringUtils.isBlank(hostName)) { return FormValidation.error("Please provide a host name"); } if (isTeamServices(hostName)) { return checkTeamServices(uri); } // TODO: check that it's not a deep URL to a repository, work item, API endpoint, etc. return FormValidation.ok(); } @SuppressWarnings("unused") public FormValidation doTestCredentials( @QueryParameter final String collectionUrl, @QueryParameter final String credentialsId) { final String errorTemplate = "Error: %s"; String hostName = null; try { final URI uri = new URI(collectionUrl); hostName = uri.getHost(); } catch (final URISyntaxException e) { return FormValidation.error(errorTemplate, e.getMessage()); } try { final StandardUsernamePasswordCredentials credential = findCredentialsById(credentialsId); if (isTeamServices(hostName)) { if (credential == null) { return FormValidation.error(errorTemplate, "Team Services accounts need credentials, preferably a Personal Access Token"); } } return testConnection(collectionUrl, credential); } catch (final IOException e) { return FormValidation.error(e, errorTemplate, e.getMessage()); } } @SuppressWarnings("unused") public ListBoxModel doFillCredentialsIdItems( @QueryParameter final String collectionUrl) { final Jenkins jenkins = Jenkins.getInstance(); String hostName = null; try { final URI uri = new URI(collectionUrl); hostName = uri.getHost(); } catch (final URISyntaxException ignored) { } if (hostName == null || !jenkins.hasPermission(Jenkins.ADMINISTER)) { return new ListBoxModel(); } final List<StandardUsernamePasswordCredentials> matches = findCredentials(hostName); return new StandardListBoxModel() .withEmptySelection() .withAll(matches); } } static FormValidation checkTeamServices(final URI uri) { if (UriHelper.hasPath(uri)) { return FormValidation.error("A Team Services collection URL must have an empty path."); } return FormValidation.ok(); } static boolean areSameCollectionUri(final URI a, final URI b) { if (a == null) { throw new IllegalArgumentException("Parameter 'a' is null"); } if (b == null) { throw new IllegalArgumentException("Parameter 'b' is null"); } final String aHost = a.getHost(); final String bHost = b.getHost(); if (isTeamServices(aHost) && isTeamServices(bHost)) { return StringHelper.equalIgnoringCase(aHost, bHost); } return UriHelper.areSame(a, b); } public static boolean isTeamServices(final String hostName) { return StringHelper.endsWithIgnoreCase(hostName, ".visualstudio.com"); } static FormValidation testConnection(final String collectionUri, final StandardUsernamePasswordCredentials credentials) throws IOException { final Server server = Server.create(null, null, collectionUri, credentials, null, null); try { final MockableVersionControlClient vcc = server.getVersionControlClient(); return FormValidation.ok("Success via SOAP API."); } catch (final TFSUnauthorizedException e) { // performing TFVC requires All Scopes and someone might be setting up for Git only; ignore } final TeamRestClient client = new TeamRestClient(collectionUri, credentials); try { final ListOfGitRepositories repositories = client.getRepositories(); if (repositories.count < 1) { return FormValidation.warning("There does not seem to be any Git repositories"); } return FormValidation.ok("Success via REST API."); } catch (final IOException e) { return FormValidation.error("Error: " + e.getMessage()); } } static StandardUsernamePasswordCredentials findCredential(final String hostName, final String credentialsId) { final List<StandardUsernamePasswordCredentials> matches = findCredentials(hostName); final CredentialsMatcher matcher = CredentialsMatchers.withId(credentialsId); final StandardUsernamePasswordCredentials result = CredentialsMatchers.firstOrNull(matches, matcher); return result; } static List<StandardUsernamePasswordCredentials> findCredentials(final String hostName) { final Jenkins jenkins = Jenkins.getInstance(); final HostnameRequirement requirement = new HostnameRequirement(hostName); final List<StandardUsernamePasswordCredentials> matches = CredentialsProvider.lookupCredentials( StandardUsernamePasswordCredentials.class, jenkins, ACL.SYSTEM, requirement ); return matches; } static StandardUsernamePasswordCredentials findCredentialsById(final String credentialsId) { final Jenkins jenkins = Jenkins.getInstance(); final List<StandardUsernamePasswordCredentials> matches = CredentialsProvider.lookupCredentials( StandardUsernamePasswordCredentials.class, jenkins, ACL.SYSTEM, Collections.<DomainRequirement>emptyList() ); final CredentialsMatcher matcher = CredentialsMatchers.withId(credentialsId); final StandardUsernamePasswordCredentials result = CredentialsMatchers.firstOrNull(matches, matcher); return result; } // TODO: we'll probably also want findCredentialsForGitRepo, where we match part of the URL path public static StandardUsernamePasswordCredentials findCredentialsForCollection(final URI collectionUri) { final TeamPluginGlobalConfig config = TeamPluginGlobalConfig.get(); // TODO: consider using a different data structure to speed up this look-up final List<TeamCollectionConfiguration> pairs = config.getCollectionConfigurations(); for (final TeamCollectionConfiguration pair : pairs) { final String candidateCollectionUrlString = pair.getCollectionUrl(); final URI candidateCollectionUri = URI.create(candidateCollectionUrlString); if (areSameCollectionUri(candidateCollectionUri, collectionUri)) { final String credentialsId = pair.credentialsId; if (credentialsId != null) { return findCredentialsById(credentialsId); } return null; } } final String template = "There is no team project collection configured for the URL '%1$s'.\n" + "Please go to Jenkins > Manage Jenkins > Configure System and then " + "add a Team Project Collection with a Collection URL of '%1$s'."; final String message = String.format(template, collectionUri); throw new IllegalArgumentException(message); } }