From 1c5d82b0129065d1800742c7c0c895119ce0aa1e Mon Sep 17 00:00:00 2001 From: Wei Li Date: Wed, 31 May 2017 18:01:28 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=20=E2=9C=A8=20Fixed=20#JENKINS-44586.=20ad?= =?UTF-8?q?d=20support=20for=20managing=20credentials?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NoExecutorStartedManageCredentialsIT.java | 116 +++++++++++++ .../com/offbytwo/jenkins/JenkinsServer.java | 147 ++++++++++++---- .../jenkins/client/JenkinsHttpClient.java | 52 ++++++ .../credentials/CertificateCredential.java | 163 ++++++++++++++++++ .../jenkins/model/credentials/Credential.java | 102 +++++++++++ .../model/credentials/CredentialManager.java | 128 ++++++++++++++ .../model/credentials/SSHKeyCredential.java | 157 +++++++++++++++++ .../credentials/SecretTextCredential.java | 53 ++++++ .../UsernamePasswordCredential.java | 77 +++++++++ .../credentials/CredentialManagerTest.java | 87 ++++++++++ 10 files changed, 1044 insertions(+), 38 deletions(-) create mode 100644 jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedManageCredentialsIT.java create mode 100644 jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CertificateCredential.java create mode 100644 jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/Credential.java create mode 100644 jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CredentialManager.java create mode 100644 jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/SSHKeyCredential.java create mode 100644 jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/SecretTextCredential.java create mode 100644 jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/UsernamePasswordCredential.java create mode 100644 jenkins-client/src/test/java/com/offbytwo/jenkins/model/credentials/CredentialManagerTest.java diff --git a/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedManageCredentialsIT.java b/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedManageCredentialsIT.java new file mode 100644 index 00000000..5e524f2d --- /dev/null +++ b/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedManageCredentialsIT.java @@ -0,0 +1,116 @@ +package com.offbytwo.jenkins.integration; + +import com.offbytwo.jenkins.JenkinsServer; +import com.offbytwo.jenkins.model.Plugin; +import com.offbytwo.jenkins.model.credentials.*; +import org.apache.commons.lang.RandomStringUtils; +import org.testng.SkipException; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.testng.Assert.*; + +@Test(groups = { Groups.NO_EXECUTOR_GROUP} ) +public class NoExecutorStartedManageCredentialsIT extends AbstractJenkinsIntegrationCase { + + @Test + public void credentialCRUDL() throws IOException { + List plugins = jenkinsServer.getPluginManager().getPlugins(); + Plugin credentialPlugin = jenkinsServer.findPluginWithName("credentials"); + if (credentialPlugin == null) { + throw new SkipException("No credentials plugin found. Skip Test"); + } + + String pluginVersion = credentialPlugin.getVersion(); + if (pluginVersion.startsWith("1.")) { + runTest(jenkinsServer); + + //test CertificateCredential with upload cert file. The 2.x version may throw exceptions. + CertificateCredential certificateCredential = new CertificateCredential(); + certificateCredential.setId("certficateTest-" + RandomStringUtils.randomAlphanumeric(24)); + certificateCredential.setCertificateSourceType(CertificateCredential.CERTIFICATE_SOURCE_TYPES.UPLOAD_CERT_FILE); + certificateCredential.setCertificateContent("testcert".getBytes()); + certificateCredential.setPassword("testpasssword"); + + credentialOperations(jenkinsServer, certificateCredential); + + } else { + runTest(jenkinsServer); + + //test SecretTextCredential, this is v2 only + SecretTextCredential secretText = new SecretTextCredential(); + secretText.setId("secrettextcredentialTest-" + RandomStringUtils.randomAlphanumeric(24)); + secretText.setSecret("testsecrettext"); + + credentialOperations(jenkinsServer, secretText); + } + } + + private void runTest(JenkinsServer jenkinsServer) throws IOException { + String testUsername = "testusername"; + String testPassword = "testpassword"; + String credentialDescription = "testDescription"; + //test UsernamePasswordCredential + UsernamePasswordCredential testUPCredential = new UsernamePasswordCredential(); + testUPCredential.setId("usernamepasswordcredentialTest-" + RandomStringUtils.randomAlphanumeric(24)); + testUPCredential.setUsername(testUsername); + testUPCredential.setPassword(testPassword); + testUPCredential.setDescription(credentialDescription); + + credentialOperations(jenkinsServer, testUPCredential); + + //test SSHKeyCredential + SSHKeyCredential sshCredential = new SSHKeyCredential(); + sshCredential.setId("sshusercredentialTest-" + RandomStringUtils.randomAlphanumeric(24)); + sshCredential.setUsername(testUsername); + sshCredential.setPassphrase(testPassword); + sshCredential.setPrivateKeyType(SSHKeyCredential.PRIVATE_KEY_TYPES.DIRECT_ENTRY); + sshCredential.setPrivateKeyValue("testPrivateKeyContent"); + + credentialOperations(jenkinsServer, sshCredential); + + //test credential + CertificateCredential certificateCredential = new CertificateCredential(); + certificateCredential.setId("certficateTest-" + RandomStringUtils.randomAlphanumeric(24)); + certificateCredential.setCertificateSourceType(CertificateCredential.CERTIFICATE_SOURCE_TYPES.FILE_ON_MASTER); + certificateCredential.setCertificatePath("/tmp/test"); + certificateCredential.setPassword("testpasssword"); + + credentialOperations(jenkinsServer, certificateCredential); + + } + + private void credentialOperations(JenkinsServer jenkinsServer, Credential credential) throws IOException { + //create the credential + String credentialId = credential.getId(); + jenkinsServer.createCredential(credential, false); + + //check if has been created by listing + Map credentials = jenkinsServer.listCredentials(); + Credential found = credentials.get(credentialId); + assertNotNull(found); + assertEquals(credential.getTypeName(), found.getTypeName()); + + //compare fields + assertEquals(credentialId, found.getId()); + assertNotNull(found.getDisplayName()); + + //update the credential + String updateDescription = "updatedDescription"; + credential.setDescription(updateDescription); + jenkinsServer.updateCredential(credentialId, credential, false); + + //verify it is updated + credentials = jenkinsServer.listCredentials(); + found = credentials.get(credentialId); + assertEquals(updateDescription, found.getDescription()); + + //delete the credential + jenkinsServer.deleteCredential(credentialId, false); + credentials = jenkinsServer.listCredentials(); + assertFalse(credentials.containsKey(credentialId)); + } +} diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java index 7b8f77a8..6029dd7a 100644 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java @@ -6,21 +6,6 @@ package com.offbytwo.jenkins; -import java.io.IOException; -import java.net.URI; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -import javax.xml.bind.JAXBException; - -import org.apache.http.HttpStatus; -import org.apache.http.client.HttpResponseException; -import org.apache.http.entity.ContentType; -import org.dom4j.DocumentException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; @@ -28,21 +13,25 @@ import com.offbytwo.jenkins.client.JenkinsHttpClient; import com.offbytwo.jenkins.client.util.EncodingUtils; import com.offbytwo.jenkins.helper.JenkinsVersion; -import com.offbytwo.jenkins.model.Build; -import com.offbytwo.jenkins.model.Computer; -import com.offbytwo.jenkins.model.ComputerSet; -import com.offbytwo.jenkins.model.FolderJob; -import com.offbytwo.jenkins.model.Job; -import com.offbytwo.jenkins.model.JobConfiguration; -import com.offbytwo.jenkins.model.JobWithDetails; -import com.offbytwo.jenkins.model.LabelWithDetails; -import com.offbytwo.jenkins.model.MainView; -import com.offbytwo.jenkins.model.MavenJobWithDetails; -import com.offbytwo.jenkins.model.PluginManager; -import com.offbytwo.jenkins.model.Queue; -import com.offbytwo.jenkins.model.QueueItem; -import com.offbytwo.jenkins.model.QueueReference; -import com.offbytwo.jenkins.model.View; +import com.offbytwo.jenkins.model.*; +import com.offbytwo.jenkins.model.credentials.Credential; +import com.offbytwo.jenkins.model.credentials.CredentialManager; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.Predicate; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpResponseException; +import org.apache.http.entity.ContentType; +import org.dom4j.DocumentException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.xml.bind.JAXBException; +import java.io.IOException; +import java.net.URI; +import java.rmi.server.ExportException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; /** * The main starting point for interacting with a Jenkins server. @@ -52,6 +41,8 @@ public class JenkinsServer { private final JenkinsHttpClient client; + private CredentialManager credentialManager; + /** * Create a new Jenkins server reference given only the server address * @@ -934,24 +925,104 @@ private String toViewBaseUrl(FolderJob folder, String name) { * @param jobName the fullName of the job. * @return the path of the job including folders if present. */ - private String parseFullName(String jobName) - { + private String parseFullName(String jobName) { if (!jobName.contains("/")) { return jobName; } - + List foldersAndJob = Arrays.asList(jobName.split("/")); - + String foldersAndJobName = ""; - + for (int i = 0; i < foldersAndJob.size(); i++) { foldersAndJobName += foldersAndJob.get(i); - - if (i != foldersAndJob.size() -1) { + + if (i != foldersAndJob.size() - 1) { foldersAndJobName += "/job/"; } } - + return foldersAndJobName; + + } + + /** + * List the credentials from the Jenkins server. + * @return a hash map of the credentials. The key is the id of each credential. + * @throws IOException + */ + public Map listCredentials() throws IOException { + return this.getCredentialManager().listCredentials(); + } + + /** + * Create the given credential + * @param credential a credential instance + * @param crumbFlag + * @throws IOException + */ + public void createCredential(Credential credential, boolean crumbFlag) throws IOException { + this.getCredentialManager().createCredential(credential, crumbFlag); + } + + /** + * Update an existing credential + * @param credentialId the id of the credential + * @param credential the updated credential instance + * @param crumbFlag + * @throws IOException + */ + public void updateCredential(String credentialId, Credential credential, boolean crumbFlag) throws IOException { + this.getCredentialManager().updateCredential(credentialId, credential, crumbFlag); + } + + /** + * Delete an existing credential + * @param credentialId the id of the credential to delete + * @param crumbFlag + * @throws IOException + */ + public void deleteCredential(String credentialId, boolean crumbFlag) throws IOException { + this.getCredentialManager().deleteCredential(credentialId, crumbFlag); + } + + /** + * Return the credentialManager instance. Will initialise it if it's never used before. + * @return the credentialManager instance + * @throws IOException + */ + private CredentialManager getCredentialManager() throws IOException { + if (this.credentialManager == null) { + Plugin credentialPlugin = findPluginWithName("credentials"); + if (credentialPlugin == null) { + throw new ExportException("credential plugin is not installed"); + } + String version = credentialPlugin.getVersion(); + this.credentialManager = new CredentialManager(version, this.client); + } + return this.credentialManager; + } + + /** + * Find a plugin that matches the given short name + * @param pluginShortName the short name of the plugin to find + * @return the pluin object that is found. Can be null if no match found. + * @throws IOException + */ + public Plugin findPluginWithName(final String pluginShortName) throws IOException { + List plugins = this.getPluginManager().getPlugins(); + Object foundPlugin = CollectionUtils.find(plugins, new Predicate() { + @Override + public boolean evaluate(Object o) { + Plugin p = (Plugin) o; + if (p.getShortName().equalsIgnoreCase(pluginShortName)) { + return true; + } else { + return false; + } + } + }); + + return foundPlugin == null ? null : (Plugin) foundPlugin; } } diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java index 8c5c467a..e77f2e56 100755 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java @@ -348,6 +348,58 @@ public HttpResponse post_form_with_result(String path, List data, return response; } + /** + * Perform a POST request using form url encoding. + * + * This method was added for the purposes of creating credentials, but may be + * useful for other API calls as well. + * + * Unlike post and post_xml, the path is *not* modified by adding + * "/api/json". Additionally, the params in data are provided as both + * request parameters including a json parameter, *and* in the + * JSON-formatted StringEntity, because this is what the folder creation + * call required. It is unclear if any other jenkins APIs operate in this + * fashion. + * + * @param path path to request, can be relative or absolute + * @param data data to post + * @param crumbFlag true / false. + * @throws IOException in case of an error. + */ + public void post_form_json(String path, Map data, boolean crumbFlag) throws IOException { + HttpPost request; + if (data != null) { + // https://gist.github.com/stuart-warren/7786892 was slightly + // helpful here + List queryParams = Lists.newArrayList(); + queryParams.add("json=" + EncodingUtils.encodeParam(JSONObject.fromObject(data).toString())); + String value = mapper.writeValueAsString(data); + StringEntity stringEntity = new StringEntity(value, ContentType.APPLICATION_FORM_URLENCODED); + request = new HttpPost(noapi(path) + StringUtils.join(queryParams, "&")); + request.setEntity(stringEntity); + } else { + request = new HttpPost(noapi(path)); + } + + if (crumbFlag == true) { + Crumb crumb = get("/crumbIssuer", Crumb.class); + if (crumb != null) { + request.addHeader(new BasicHeader(crumb.getCrumbRequestField(), crumb.getCrumb())); + } + } + + HttpResponse response = client.execute(request, localContext); + getJenkinsVersionFromHeader(response); + + try { + httpResponseValidator.validateResponse(response); + } finally { + EntityUtils.consume(response.getEntity()); + releaseConnection(request); + } + } + + /** * Perform a POST request of XML (instead of using json mapper) and return a * string rendering of the response entity. diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CertificateCredential.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CertificateCredential.java new file mode 100644 index 00000000..1d48bb2e --- /dev/null +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CertificateCredential.java @@ -0,0 +1,163 @@ +package com.offbytwo.jenkins.model.credentials; + +import org.apache.commons.codec.binary.Base64; + +import java.util.HashMap; +import java.util.Map; + +/** + * Certificate credential type. Can be used with both 1.x and 2.x versions of the credentials plugin. + * + * NOTE: there is a bug in 2.x version of the plugin that will thrown exception when uploading a certificate file. See https://issues.jenkins-ci.org/browse/JENKINS-41946. + * It is fixed in 2.1.12 and later. + */ +public class CertificateCredential extends Credential { + public static final String TYPENAME = "Certificate"; + + private static final String BASECLASS = "com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl"; + + private static final String FILE_ON_MASTER_KEYSTORE_SOURCE_CLASS = BASECLASS + "$FileOnMasterKeyStoreSource"; + + private static final String UPLOAD_KEYSTORE_SOURCE_CLASS = BASECLASS + "$UploadedKeyStoreSource"; + + private String password; + private String certificatePath; + private byte[] certificateContent; + private CERTIFICATE_SOURCE_TYPES certificateSourceType; + + /** + * The source of the certificate. + */ + public enum CERTIFICATE_SOURCE_TYPES { + /** + * The certificate is on the file system of the master node. Set the path via {@link #setCertificatePath(String)} method + */ + FILE_ON_MASTER(FILE_ON_MASTER_KEYSTORE_SOURCE_CLASS, 0), + /** + * Update the certificate content. Should set it via {@link #setCertificateContent(byte[])} method. + */ + UPLOAD_CERT_FILE(UPLOAD_KEYSTORE_SOURCE_CLASS, 1); + + private String certStoreClass; + private int certStoreType; + + CERTIFICATE_SOURCE_TYPES(String storeClass, int storeType) { + this.certStoreClass = storeClass; + this.certStoreType = storeType; + } + + public String getCertStoreClass() { + return this.certStoreClass; + } + + public int getCertStoreType() { + return this.certStoreType; + } + } + + public CertificateCredential() { + setTypeName(TYPENAME); + } + + public String getPassword() { + return password; + } + + /** + * Set the password of the certificate + * @param password + */ + public void setPassword(String password) { + this.password = password; + } + + public String getCertificatePath() { + return certificatePath; + } + + /** + * Set the path of the certificate. Required if CERTIFICATE_SOURCE_TYPES is FILE_ON_MASTER. + * @param certificatePath + */ + public void setCertificatePath(String certificatePath) { + this.certificatePath = certificatePath; + } + + public byte[] getCertificateContent() { + return certificateContent; + } + + /** + * Set the content of the certificate. Required if CERTIFICATE_SOURCE_TYPES is UPLOAD_CERT_FILE. + * @param certificateContent + */ + public void setCertificateContent(byte[] certificateContent) { + this.certificateContent = certificateContent; + } + + public CERTIFICATE_SOURCE_TYPES getCertificateSourceType() { + return certificateSourceType; + } + + /** + * Set the source of the certificate + * @param certificateSourceType + */ + public void setCertificateSourceType(CERTIFICATE_SOURCE_TYPES certificateSourceType) { + this.certificateSourceType = certificateSourceType; + } + + @Override + public Map dataForCreate() { + Map certificateSourceMap = new HashMap<>(); + certificateSourceMap.put("value", String.valueOf(this.getCertificateSourceType().getCertStoreType())); + certificateSourceMap.put("stapler-class", this.getCertificateSourceType().getCertStoreClass()); + certificateSourceMap.put("$class", this.getCertificateSourceType().getCertStoreClass()); + + if (this.getCertificateSourceType() == CERTIFICATE_SOURCE_TYPES.FILE_ON_MASTER) { + certificateSourceMap.put("keyStoreFile", this.getCertificatePath()); + } else if (this.getCertificateSourceType() == CERTIFICATE_SOURCE_TYPES.UPLOAD_CERT_FILE) { + certificateSourceMap.put("uploadedKeystore", Base64.encodeBase64String(this.getCertificateContent())); + } + + Map innerMap = new HashMap<>(); + innerMap.put("scope", SCOPE_GLOBAL); + innerMap.put("id", this.getId()); + innerMap.put("description", this.getDescription()); + innerMap.put("password", this.getPassword()); + innerMap.put("stapler-class", BASECLASS); + innerMap.put("$class", BASECLASS); + innerMap.put("keyStoreSource", certificateSourceMap); + + Map jsonData = new HashMap<>(); + jsonData.put("", "1"); + jsonData.put("credentials", innerMap); + return jsonData; + } + + @Override + public Map dataForUpdate() { + Map certificateSourceMap = new HashMap<>(); + certificateSourceMap.put("value", String.valueOf(this.getCertificateSourceType().getCertStoreType())); + certificateSourceMap.put("stapler-class", this.getCertificateSourceType().getCertStoreClass()); + certificateSourceMap.put("$class", this.getCertificateSourceType().getCertStoreClass()); + + if (this.getCertificateSourceType() == CERTIFICATE_SOURCE_TYPES.FILE_ON_MASTER) { + certificateSourceMap.put("keyStoreFile", this.getCertificatePath()); + } else if (this.getCertificateSourceType() == CERTIFICATE_SOURCE_TYPES.UPLOAD_CERT_FILE) { + certificateSourceMap.put("uploadedKeystore", Base64.encodeBase64String(this.getCertificateContent())); + } + + Map jsonData = new HashMap<>(); + jsonData.put("scope", SCOPE_GLOBAL); + jsonData.put("id", this.getId()); + jsonData.put("description", this.getDescription()); + jsonData.put("password", this.getPassword()); + jsonData.put("stapler-class", BASECLASS); + jsonData.put("$class", BASECLASS); + jsonData.put("keyStoreSource", certificateSourceMap); + + return jsonData; + } + +} diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/Credential.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/Credential.java new file mode 100644 index 00000000..790e5279 --- /dev/null +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/Credential.java @@ -0,0 +1,102 @@ +package com.offbytwo.jenkins.model.credentials; + + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.offbytwo.jenkins.model.BaseModel; + +import java.util.Map; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "typeName", defaultImpl = UsernamePasswordCredential.class) +@JsonSubTypes({@JsonSubTypes.Type(value = UsernamePasswordCredential.class, name = UsernamePasswordCredential.TYPENAME), + @JsonSubTypes.Type(value = SSHKeyCredential.class, name = SSHKeyCredential.TYPENAME), + @JsonSubTypes.Type(value = SecretTextCredential.class, name = SecretTextCredential.TYPENAME), + @JsonSubTypes.Type(value = CertificateCredential.class, name = CertificateCredential.TYPENAME)}) +/** + * Base class for credentials. Should not be instantiated directly. + */ +public abstract class Credential extends BaseModel { + + protected static final String SCOPE_GLOBAL = "GLOBAL"; + + private String id = ""; + private String scope = SCOPE_GLOBAL; + private String description = ""; + private String fullName = ""; + private String displayName = ""; + + private String typeName = ""; + + public String getScope() { + return scope; + } + + /** + * Set the scope of the credential. It is "GLOBAL" by default. Should not be changed. + * @param scope + */ + public void setScope(String scope) { + this.scope = scope; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getId() { + return this.id; + } + + /** + * Set the id of the credential. When creating a new credential, if this is not provided, a random id will be generated by Jenkins. + * Required for update and delete operations + * @param id the id of the credential. + */ + public void setId(String id) { + this.id = id; + } + + public String getFullName() { + return fullName; + } + + /** + * Should not be used. The value is set by Jenkins. + * @param fullName + */ + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getTypeName() { + return typeName; + } + + /** + * Should not be used. The value is set by Jenkins. + * @param typeName + */ + public void setTypeName(String typeName) { + this.typeName = typeName; + } + + public String getDisplayName() { + return this.displayName; + } + + /** + * Should not be used. The value is set by Jenkins + * @param displayName + */ + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public abstract Map dataForCreate(); + + public abstract Map dataForUpdate(); +} diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CredentialManager.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CredentialManager.java new file mode 100644 index 00000000..02ce3feb --- /dev/null +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CredentialManager.java @@ -0,0 +1,128 @@ +package com.offbytwo.jenkins.model.credentials; + + +import com.offbytwo.jenkins.client.JenkinsHttpClient; +import com.offbytwo.jenkins.model.BaseModel; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CredentialManager { + + public static final String V1URL = "/credential-store/domain/_"; + public static final String V2URL = "/credentials/store/system/domain/_"; + + String baseUrl = V2URL; + JenkinsHttpClient jenkinsClient; + boolean isVersion1 = false; + + public CredentialManager( String version, JenkinsHttpClient client) { + if (version.startsWith("1")) { + this.isVersion1 = true; + this.baseUrl = V1URL; + } + this.jenkinsClient = client; + } + + /** + * Return the list of exsting credentials. + * NOTE: for each credential insstance, only the following fields are set: + * - id + * - description + * - displayName + * - fullName + * - typeName + * - username (depending on the type of the credential) + * @return the existing credentials from Jenkins + * @throws IOException + */ + public Map listCredentials() throws IOException { + String url = String.format("%s?depth=2", this.baseUrl); + if (this.isVersion1) { + CredentialResponseV1 response = this.jenkinsClient.get(url, CredentialResponseV1.class); + Map credentials = response.getCredentials(); + //need to set the id on the credentials as it is not returned in the body + for (String crendentialId : credentials.keySet()) { + credentials.get(crendentialId).setId(crendentialId); + } + return credentials; + } else { + CredentialResponse response = this.jenkinsClient.get(url, CredentialResponse.class); + List credentials = response.getCredentials(); + Map credentialMap = new HashMap<>(); + for(Credential credential : credentials) { + credentialMap.put(credential.getId(), credential); + } + return credentialMap; + } + } + + /** + * Create a new credential + * @param credential the credential instance to create. + * @param crumbFlag + * @throws IOException + */ + public void createCredential(Credential credential, Boolean crumbFlag) throws IOException { + String url = String.format("%s/%s?", this.baseUrl, "createCredentials"); + this.jenkinsClient.post_form_json(url, credential.dataForCreate(), crumbFlag); + } + + /** + * Update an existing credential. + * @param credentialId the id of the credential to update + * @param credential the credential to update + * @param crumbFlag + * @throws IOException + */ + public void updateCredential(String credentialId, Credential credential, Boolean crumbFlag) throws IOException { + credential.setId(credentialId); + String url = String.format("%s/%s/%s/%s?", this.baseUrl, "credential", credentialId, "updateSubmit"); + this.jenkinsClient.post_form_json(url, credential.dataForUpdate(), crumbFlag); + } + + /** + * Delete the credential with the given id + * @param credentialId the id of the credential + * @param crumbFlag + * @throws IOException + */ + public void deleteCredential(String credentialId, Boolean crumbFlag) throws IOException { + String url = String.format("%s/%s/%s/%s?", this.baseUrl, "credential", credentialId, "doDelete"); + this.jenkinsClient.post_form(url, new HashMap(), crumbFlag); + } + + /** + * Represents the list response from Jenkins with the 2.x credentials plugin + */ + public static class CredentialResponse extends BaseModel { + private List credentials; + + public void setCredentials(List credentials) { + this.credentials = credentials; + } + + public List getCredentials() { + return credentials; + } + } + + /** + * Represents the list response from Jenkins with the 1.x credentials plugin + */ + public static class CredentialResponseV1 extends BaseModel { + + private Map credentials; + + public Map getCredentials() { + return credentials; + } + + public void setCredentials(Map credentials) { + this.credentials = credentials; + } + + } +} diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/SSHKeyCredential.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/SSHKeyCredential.java new file mode 100644 index 00000000..cacdb89a --- /dev/null +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/SSHKeyCredential.java @@ -0,0 +1,157 @@ +package com.offbytwo.jenkins.model.credentials; + +import java.util.HashMap; +import java.util.Map; + +/** + * SSH Key Credential type. Can be used with 1.x and 2.x versions of the credentials plugin. + */ +public class SSHKeyCredential extends Credential { + + public static final String TYPENAME = "SSH Username with private key"; + + private static final String BASECLASS = "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey"; + + private static final String DIRECT_ENTRY_CLASS = BASECLASS + "$DirectEntryPrivateKeySource"; + private static final String FILE_ON_MASTER_CLASS = BASECLASS + "$FileOnMasterPrivateKeySource"; + private static final String USERS_PRIVATE_KEY_CLASS = BASECLASS + "$UsersPrivateKeySource"; + + + private String username; + private String passphrase; + private String privateKeyValue; + + public SSHKeyCredential() { + setTypeName(TYPENAME); + } + + /** + * The type of the private key. + */ + public enum PRIVATE_KEY_TYPES { + /** + * Plain text + */ + DIRECT_ENTRY (DIRECT_ENTRY_CLASS, 0), + /** + * A file path on the master node + */ + FILE_ON_MASTER (FILE_ON_MASTER_CLASS, 1), + + /** + * From the Jenkins master ~/.ssh + */ + USERS_PRIVATE_KEY (USERS_PRIVATE_KEY_CLASS, 2); + + private String privateKeyTypeClass; + private int typeValue; + + PRIVATE_KEY_TYPES(String typeClass, int typeValue) { + this.privateKeyTypeClass = typeClass; + this.typeValue = typeValue; + } + + public String getTypeClass() { + return this.privateKeyTypeClass; + } + + public int getTypeValue() { + return this.typeValue; + } + } + + private PRIVATE_KEY_TYPES privateKeyType; + + + public String getUsername() { + return username; + } + + /** + * Set the username of the ssh key + * @param username + */ + public void setUsername(String username) { + this.username = username; + } + + public String getPassphrase() { + return passphrase; + } + + /** + * Set the passphrash of the ssh key + * @param passphrase + */ + public void setPassphrase(String passphrase) { + this.passphrase = passphrase; + } + + public String getPrivateKeyValue() { + return privateKeyValue; + } + + /** + * Set the value of the private key. + * Depending on the type of the private key, it should be either the content of the key, or the path of the private key file. + * @param privateKeyValue + */ + public void setPrivateKeyValue(String privateKeyValue) { + this.privateKeyValue = privateKeyValue; + } + + public PRIVATE_KEY_TYPES getPrivateKeyType() { + return privateKeyType; + } + + /** + * The source of the private key. + * @param privateKeyType + */ + public void setPrivateKeyType(PRIVATE_KEY_TYPES privateKeyType) { + this.privateKeyType = privateKeyType; + } + + @Override + public Map dataForCreate() { + Map privateKeySourceMap = new HashMap<>(); + privateKeySourceMap.put("value", String.valueOf(this.getPrivateKeyType().getTypeValue())); + privateKeySourceMap.put("privateKey", this.getPrivateKeyValue()); + privateKeySourceMap.put("stapler-class", this.getPrivateKeyType().getTypeClass()); + + Map innerMap = new HashMap<>(); + innerMap.put("scope", SCOPE_GLOBAL); + innerMap.put("id", this.getId()); + innerMap.put("username", this.getUsername()); + innerMap.put("description", this.getDescription()); + innerMap.put("passphrase", this.getPassphrase()); + innerMap.put("stapler-class", BASECLASS); + innerMap.put("$class", BASECLASS); + innerMap.put("privateKeySource", privateKeySourceMap); + + Map jsonData = new HashMap<>(); + jsonData.put("", "1"); + jsonData.put("credentials", innerMap); + return jsonData; + } + + @Override + public Map dataForUpdate() { + Map privateKeySourceMap = new HashMap<>(); + privateKeySourceMap.put("value", String.valueOf(this.getPrivateKeyType().getTypeValue())); + privateKeySourceMap.put("privateKey", this.getPrivateKeyValue()); + privateKeySourceMap.put("stapler-class", this.getPrivateKeyType().getTypeClass()); + + Map jsonData = new HashMap<>(); + jsonData.put("scope", SCOPE_GLOBAL); + jsonData.put("id", this.getId()); + jsonData.put("username", this.getUsername()); + jsonData.put("description", this.getDescription()); + jsonData.put("passphrase", this.getPassphrase()); + jsonData.put("stapler-class", BASECLASS); + jsonData.put("$class", BASECLASS); + jsonData.put("privateKeySource", privateKeySourceMap); + + return jsonData; + } +} diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/SecretTextCredential.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/SecretTextCredential.java new file mode 100644 index 00000000..44882c22 --- /dev/null +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/SecretTextCredential.java @@ -0,0 +1,53 @@ +package com.offbytwo.jenkins.model.credentials; + + +import java.util.HashMap; +import java.util.Map; + +/** + * Secret Text credential type. Can be used with 2.x version of the credentials plugins. + */ +public class SecretTextCredential extends Credential { + + public static final String TYPENAME = "Secret text"; + private static final String CLASSNAME = "org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl"; + private String secret; + + public SecretTextCredential() { + setTypeName(TYPENAME); + } + + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + @Override + public Map dataForCreate() { + Map innerMap = new HashMap<>(); + innerMap.put("scope", this.getScope()); + innerMap.put("id", this.getId()); + innerMap.put("secret", this.getSecret()); + innerMap.put("description", this.getDescription()); + innerMap.put("$class", CLASSNAME); + innerMap.put("stapler-class", CLASSNAME); + Map data = new HashMap<>(); + data.put("", "1"); + data.put("credentials", innerMap); + return data; + } + + @Override + public Map dataForUpdate() { + Map data = new HashMap<>(); + data.put("scope", this.getScope()); + data.put("id", this.getId()); + data.put("secret", this.getSecret()); + data.put("description", this.getDescription()); + data.put("stapler-class", CLASSNAME); + return data; + } +} diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/UsernamePasswordCredential.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/UsernamePasswordCredential.java new file mode 100644 index 00000000..9cbdb308 --- /dev/null +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/UsernamePasswordCredential.java @@ -0,0 +1,77 @@ +package com.offbytwo.jenkins.model.credentials; + +import java.util.HashMap; +import java.util.Map; + +/** + * Username and password credential type. Can be used with 1.x and 2.x versions of the credentials plugin. + */ +public class UsernamePasswordCredential extends Credential { + + public static final String TYPENAME = "Username with password"; + private static final String CLASSNAME = "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl"; + + private String username; + private String password; + + public UsernamePasswordCredential() { + this.setTypeName(TYPENAME); + } + + public String getUsername() { + if (this.username != null) { + return this.username; + } + if (this.getDisplayName() != null) { + return this.getDisplayName().split("/") [0]; + } + return null; + } + + /** + * Set the username of the credential + * @param username + */ + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + /** + * Set the password of the credential + * @param password + */ + public void setPassword(String password) { + this.password = password; + } + + @Override + public Map dataForCreate() { + Map innerMap = new HashMap<>(); + innerMap.put("scope", this.getScope()); + innerMap.put("id", this.getId()); + innerMap.put("username", this.getUsername()); + innerMap.put("password", this.getPassword()); + innerMap.put("description", this.getDescription()); + innerMap.put("$class", CLASSNAME); + Map data = new HashMap<>(); + data.put("", "0"); + data.put("credentials", innerMap); + return data; + } + + @Override + public Map dataForUpdate() { + Map data = new HashMap<>(); + data.put("scope", this.getScope()); + data.put("id", this.getId()); + data.put("username", this.getUsername()); + data.put("password", this.getPassword()); + data.put("description", this.getDescription()); + data.put("stapler-class", CLASSNAME); + return data; + } +} diff --git a/jenkins-client/src/test/java/com/offbytwo/jenkins/model/credentials/CredentialManagerTest.java b/jenkins-client/src/test/java/com/offbytwo/jenkins/model/credentials/CredentialManagerTest.java new file mode 100644 index 00000000..45a81f9b --- /dev/null +++ b/jenkins-client/src/test/java/com/offbytwo/jenkins/model/credentials/CredentialManagerTest.java @@ -0,0 +1,87 @@ +package com.offbytwo.jenkins.model.credentials; + + +import com.offbytwo.jenkins.client.JenkinsHttpClient; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertTrue; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class CredentialManagerTest { + + private JenkinsHttpClient client = mock(JenkinsHttpClient.class); + + private CredentialManager credentialManager = new CredentialManager("2.0.0", client); + + private UsernamePasswordCredential credential1 = new UsernamePasswordCredential(); + private UsernamePasswordCredential credential2 = new UsernamePasswordCredential(); + + @Before + public void setup() { + credential1.setId("credential1"); + credential1.setUsername("test1"); + credential1.setPassword("test1"); + + credential2.setId("credential2"); + credential2.setUsername("test2"); + credential2.setPassword("test2"); + } + + @Test + public void testListCredentials() throws IOException { + List credentialList = new ArrayList<>(); + credentialList.add(credential1); + credentialList.add(credential2); + + CredentialManager.CredentialResponse response = new CredentialManager.CredentialResponse(); + response.setCredentials(credentialList); + + given(client.get(anyString(), eq(CredentialManager.CredentialResponse.class))).willReturn(response); + + Map credentials = credentialManager.listCredentials(); + assertTrue(credentials.containsKey(credential1.getId())); + assertTrue(credentials.containsKey(credential2.getId())); + + verify(client).get(CredentialManager.V2URL + "?depth=2", CredentialManager.CredentialResponse.class); + } + + @Test + public void testCreateCredential() throws IOException { + UsernamePasswordCredential credentialToCreate = new UsernamePasswordCredential(); + credentialToCreate.setId("testCreation"); + credentialToCreate.setUsername("testuser"); + credentialToCreate.setPassword("password"); + + credentialManager.createCredential(credentialToCreate, false); + verify(client).post_form_json(eq (CredentialManager.V2URL + "/createCredentials?"), anyMap(), eq(false)); + } + + @Test + public void testUpdateCredential() throws IOException { + String credentialId = "testUpdate"; + UsernamePasswordCredential credentialToUpdate = new UsernamePasswordCredential(); + credentialToUpdate.setId(credentialId); + credentialToUpdate.setUsername("testuser"); + credentialToUpdate.setPassword("password"); + + credentialManager.updateCredential(credentialId, credentialToUpdate, false); + verify(client).post_form_json(eq(CredentialManager.V2URL + "/credential/" + credentialId + "/updateSubmit?"), anyMap(), eq(false)); + } + + @Test + public void testDeleteCredential() throws IOException { + String credentialId = "testDelete"; + credentialManager.deleteCredential(credentialId, false); + + verify(client).post_form(eq(CredentialManager.V2URL + "/credential/" + credentialId + "/doDelete?"), anyMap(), eq(false)); + } +} From 4f90635eaa110ec81b23f35fb68bac76b64754c0 Mon Sep 17 00:00:00 2001 From: Wei Li Date: Tue, 27 Jun 2017 15:55:44 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=20=E2=9C=A8=20add=20a=20new=20credential?= =?UTF-8?q?=20type:=20Apple=20Developer=20Profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins-client-it-docker/plugins.txt | 1 + .../NoExecutorStartedManageCredentialsIT.java | 9 +- .../NoExecutorStartedPluginManagerIT.java | 9 +- jenkins-client/pom.xml | 5 + .../jenkins/client/JenkinsHttpClient.java | 63 +++++++++++ .../AppleDeveloperProfileCredential.java | 107 ++++++++++++++++++ .../jenkins/model/credentials/Credential.java | 11 +- .../model/credentials/CredentialManager.java | 12 +- pom.xml | 7 ++ 9 files changed, 216 insertions(+), 8 deletions(-) create mode 100644 jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/AppleDeveloperProfileCredential.java diff --git a/jenkins-client-it-docker/plugins.txt b/jenkins-client-it-docker/plugins.txt index e802c9d0..6c1b1f79 100644 --- a/jenkins-client-it-docker/plugins.txt +++ b/jenkins-client-it-docker/plugins.txt @@ -8,3 +8,4 @@ job-dsl:1.41 config-file-provider:2.10.0 testng-plugin:1.10 cloudbees-folder: 5.12 +xcode-plugin: 2.0.0 \ No newline at end of file diff --git a/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedManageCredentialsIT.java b/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedManageCredentialsIT.java index 5e524f2d..d215309c 100644 --- a/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedManageCredentialsIT.java +++ b/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedManageCredentialsIT.java @@ -72,7 +72,7 @@ private void runTest(JenkinsServer jenkinsServer) throws IOException { credentialOperations(jenkinsServer, sshCredential); - //test credential + //test certificate credential CertificateCredential certificateCredential = new CertificateCredential(); certificateCredential.setId("certficateTest-" + RandomStringUtils.randomAlphanumeric(24)); certificateCredential.setCertificateSourceType(CertificateCredential.CERTIFICATE_SOURCE_TYPES.FILE_ON_MASTER); @@ -81,6 +81,13 @@ private void runTest(JenkinsServer jenkinsServer) throws IOException { credentialOperations(jenkinsServer, certificateCredential); + //test AppleDeveloperProfileCredential + AppleDeveloperProfileCredential appleDevProfile = new AppleDeveloperProfileCredential(); + appleDevProfile.setId("appleProfileTest-" + RandomStringUtils.randomAlphanumeric(24)); + appleDevProfile.setPassword(testPassword); + appleDevProfile.setDeveloperProfileContent("testprofile".getBytes()); + + credentialOperations(jenkinsServer, appleDevProfile); } private void credentialOperations(JenkinsServer jenkinsServer, Credential credential) throws IOException { diff --git a/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedPluginManagerIT.java b/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedPluginManagerIT.java index bf39e06e..650e7d7d 100644 --- a/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedPluginManagerIT.java +++ b/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedPluginManagerIT.java @@ -33,12 +33,12 @@ public void getPluginsShouldReturn9ForJenkins20() { } @Test - public void getPluginsShouldReturn27ForJenkins1651() { + public void getPluginsShouldReturn28ForJenkins1651() { JenkinsVersion jv = jenkinsServer.getVersion(); if (jv.isLessThan("1.651") && jv.isGreaterThan("1.651.3")) { throw new SkipException("Not Version 1.651 (" + jv.toString() + ")"); } - assertThat(pluginManager.getPlugins()).hasSize(27); + assertThat(pluginManager.getPlugins()).hasSize(28); } private Plugin createPlugin(String shortName, String version) { @@ -101,7 +101,7 @@ public void getPluginsShouldReturnTheListOfInstalledPluginsFor1651() { // instead of maintaining at two locations. //@formatter:off Plugin[] expectedPlugins = { - createPlugin("token-macro", "1.12.1"), + createPlugin("token-macro", "1.12.1"), createPlugin("translation", "1.10"), createPlugin("testng-plugin", "1.10"), createPlugin("matrix-project", "1.4.1"), @@ -127,7 +127,8 @@ public void getPluginsShouldReturnTheListOfInstalledPluginsFor1651() { createPlugin("throttle-concurrents", "1.9.0"), createPlugin("subversion", "1.54"), createPlugin("ssh-slaves", "1.9"), - createPlugin("cloudbees-folder", "5.12"), + createPlugin("cloudbees-folder", "5.12"), + createPlugin("xcode-plugin", "2.0.0"), }; //@formatter:on List plugins = pluginManager.getPlugins(); diff --git a/jenkins-client/pom.xml b/jenkins-client/pom.xml index baf14987..6050f92d 100644 --- a/jenkins-client/pom.xml +++ b/jenkins-client/pom.xml @@ -71,6 +71,11 @@ httpclient + + org.apache.httpcomponents + httpmime + + jaxen jaxen diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java index e77f2e56..975c2316 100755 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java @@ -30,6 +30,7 @@ import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.auth.BasicScheme; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; @@ -41,6 +42,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -399,6 +401,67 @@ public void post_form_json(String path, Map data, boolean crumbF } } + /** + * Perform a POST request using multipart-form. + * + * This method was added for the purposes of creating some types of credentials, but may be + * useful for other API calls as well. + * + * Unlike post and post_xml, the path is *not* modified by adding + * "/api/json". Additionally, the params in data are provided as both + * request parameters including a json parameter, *and* in the + * JSON-formatted StringEntity, because this is what the folder creation + * call required. It is unclear if any other jenkins APIs operate in this + * fashion. + * + * @param path path to request, can be relative or absolute + * @param data data to post + * @param crumbFlag true / false. + * @throws IOException in case of an error. + */ + public void post_multipart_form_json(String path, Map data, boolean crumbFlag) throws IOException { + HttpPost request; + if (data != null) { + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + for (Map.Entry entry : data.entrySet()) { + String fieldName = entry.getKey(); + Object fieldValue = entry.getValue(); + if (fieldValue instanceof String) { + builder.addTextBody(fieldName, (String) fieldValue); + } else if (fieldValue instanceof byte[]) { + builder.addBinaryBody(fieldName, (byte[]) fieldValue); + } else if (fieldValue instanceof File) { + builder.addBinaryBody(fieldName, (File) fieldValue); + } else if (fieldValue instanceof InputStream) { + builder.addBinaryBody(fieldName, (InputStream) fieldValue); + } else { + throw new IllegalArgumentException("type of field " + fieldName + " is not String, byte[], File or InputStream"); + } + } + request = new HttpPost(noapi(path)); + request.setEntity(builder.build()); + } else { + request = new HttpPost(noapi(path)); + } + + if (crumbFlag == true) { + Crumb crumb = get("/crumbIssuer", Crumb.class); + if (crumb != null) { + request.addHeader(new BasicHeader(crumb.getCrumbRequestField(), crumb.getCrumb())); + } + } + + HttpResponse response = client.execute(request, localContext); + getJenkinsVersionFromHeader(response); + + try { + httpResponseValidator.validateResponse(response); + } finally { + EntityUtils.consume(response.getEntity()); + releaseConnection(request); + } + } + /** * Perform a POST request of XML (instead of using json mapper) and return a diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/AppleDeveloperProfileCredential.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/AppleDeveloperProfileCredential.java new file mode 100644 index 00000000..8b84ab8a --- /dev/null +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/AppleDeveloperProfileCredential.java @@ -0,0 +1,107 @@ +package com.offbytwo.jenkins.model.credentials; + +import net.sf.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +/** + * Apple developer profile credential type. + * + * NOTE: this type is only available on Jenkins after the xcode plugin (https://wiki.jenkins.io/display/JENKINS/Xcode+Plugin) is installed. + */ +public class AppleDeveloperProfileCredential extends Credential { + public static final String TYPENAME = "Apple Developer Profile"; + + private static final String BASECLASS = "au.com.rayh.DeveloperProfile"; + private static final String FILE_ZERO_FIELD_NAME = "file0"; + private static final String FILE_ONE_FIELD_NAME = "file1"; + + private String password; + private byte[] developerProfileContent; + + public String getPassword() { + return password; + } + + /** + * Set the password of the developer profile + * @param password + */ + public void setPassword(String password) { + this.password = password; + } + + public byte[] getDeveloperProfileContent() { + return developerProfileContent; + } + + /** + * Set the content of the developer profile. A developer profile file is a zip with the following structure: + * + * developerprofile/ + * - account.keychain (can be empty. Required for validation. The plugin will create a new keychain before build) + * - identities + * |- .p12 (A exported P12 file. Should contain both certificate and private key) + * - profiles + * |- .mobileprovision (A mobile provisioning profile) + * @param developerProfileContent + */ + public void setDeveloperProfileContent(byte[] developerProfileContent) { + this.developerProfileContent = developerProfileContent; + } + + @Override + public boolean useMultipartForm() { + return true; + } + + @Override + public Map dataForCreate() { + Map credentialMap = new HashMap(); + credentialMap.put("image", FILE_ZERO_FIELD_NAME); + credentialMap.put("password", this.getPassword()); + credentialMap.put("id", this.getId()); + credentialMap.put("description", this.getDescription()); + credentialMap.put("stapler-class", BASECLASS); + credentialMap.put("$class", BASECLASS); + + + Map jsonData = new HashMap<>(); + jsonData.put("", "1"); + jsonData.put("credentials", credentialMap); + + Map formFields = new HashMap(); + formFields.put(FILE_ZERO_FIELD_NAME, this.getDeveloperProfileContent()); + formFields.put("_.scope", SCOPE_GLOBAL); + formFields.put("_.password", this.getPassword()); + formFields.put("_.id", this.getId()); + formFields.put("_.description", this.getDescription()); + formFields.put("stapler-class", BASECLASS); + formFields.put("$class", BASECLASS); + formFields.put("json", JSONObject.fromObject(jsonData).toString()); + return formFields; + } + + @Override + public Map dataForUpdate() { + Map credentialMap = new HashMap(); + credentialMap.put("image", FILE_ONE_FIELD_NAME); + credentialMap.put("password", this.getPassword()); + credentialMap.put("id", this.getId()); + credentialMap.put("description", this.getDescription()); + credentialMap.put("stapler-class", BASECLASS); + credentialMap.put("", true); + + + Map formFields = new HashMap(); + formFields.put(FILE_ONE_FIELD_NAME, this.getDeveloperProfileContent()); + formFields.put("_.", "on"); + formFields.put("_.password", this.getPassword()); + formFields.put("_.id", this.getId()); + formFields.put("_.description", this.getDescription()); + formFields.put("stapler-class", BASECLASS); + formFields.put("json", JSONObject.fromObject(credentialMap).toString()); + return formFields; + } +} diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/Credential.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/Credential.java index 790e5279..e0a39268 100644 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/Credential.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/Credential.java @@ -11,7 +11,8 @@ @JsonSubTypes({@JsonSubTypes.Type(value = UsernamePasswordCredential.class, name = UsernamePasswordCredential.TYPENAME), @JsonSubTypes.Type(value = SSHKeyCredential.class, name = SSHKeyCredential.TYPENAME), @JsonSubTypes.Type(value = SecretTextCredential.class, name = SecretTextCredential.TYPENAME), - @JsonSubTypes.Type(value = CertificateCredential.class, name = CertificateCredential.TYPENAME)}) + @JsonSubTypes.Type(value = CertificateCredential.class, name = CertificateCredential.TYPENAME), + @JsonSubTypes.Type(value = AppleDeveloperProfileCredential.class, name = AppleDeveloperProfileCredential.TYPENAME)}) /** * Base class for credentials. Should not be instantiated directly. */ @@ -99,4 +100,12 @@ public void setDisplayName(String displayName) { public abstract Map dataForCreate(); public abstract Map dataForUpdate(); + + /** + * Indicate if the request should be sent as multipart/form data + * @return + */ + public boolean useMultipartForm() { + return false; + } } diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CredentialManager.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CredentialManager.java index 02ce3feb..501d6747 100644 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CredentialManager.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CredentialManager.java @@ -67,7 +67,11 @@ public Map listCredentials() throws IOException { */ public void createCredential(Credential credential, Boolean crumbFlag) throws IOException { String url = String.format("%s/%s?", this.baseUrl, "createCredentials"); - this.jenkinsClient.post_form_json(url, credential.dataForCreate(), crumbFlag); + if (credential.useMultipartForm()) { + this.jenkinsClient.post_multipart_form_json(url, credential.dataForCreate(), crumbFlag); + } else { + this.jenkinsClient.post_form_json(url, credential.dataForCreate(), crumbFlag); + } } /** @@ -80,7 +84,11 @@ public void createCredential(Credential credential, Boolean crumbFlag) throws IO public void updateCredential(String credentialId, Credential credential, Boolean crumbFlag) throws IOException { credential.setId(credentialId); String url = String.format("%s/%s/%s/%s?", this.baseUrl, "credential", credentialId, "updateSubmit"); - this.jenkinsClient.post_form_json(url, credential.dataForUpdate(), crumbFlag); + if (credential.useMultipartForm()) { + this.jenkinsClient.post_multipart_form_json(url, credential.dataForUpdate(), crumbFlag); + } else { + this.jenkinsClient.post_form_json(url, credential.dataForUpdate(), crumbFlag); + } } /** diff --git a/pom.xml b/pom.xml index cb1291fb..bb77ee5a 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,7 @@ 17.0 2.4 4.3.6 + 4.3.6 2.3.4 @@ -148,6 +149,12 @@ ${httpclient.version} + + org.apache.httpcomponents + httpmime + ${httpmime.version} + + jaxen jaxen From e7fa341486feb7671dc91bfd8d802b3441dd980b Mon Sep 17 00:00:00 2001 From: Wei Li Date: Tue, 27 Jun 2017 16:20:17 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=20=F0=9F=90=9B=20fix=20travis=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 40d3995c..4f107b0c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,9 @@ --- +sudo: required +# Various issues have been logged aginst Travis-ci about Java 7 Support. Looks like it's broken on the latest Linux image. +# For now we use the old version of the image. In the longer term, we should consider moving to Java 8 +group: deprecated-2017Q2 + services: - docker From b32038070cc0a0ec193275cf14564d6bf3508e51 Mon Sep 17 00:00:00 2001 From: Wei Li Date: Fri, 7 Jul 2017 13:14:43 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=20=F0=9F=90=9B=20make=20sure=20the=20right?= =?UTF-8?q?=20content=20type=20are=20set=20when=20creating=20developer=20?= =?UTF-8?q?=20profile=20type=20credentials?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jenkins/client/FormBinaryField.java | 25 +++++++++++++++++++ .../jenkins/client/JenkinsHttpClient.java | 7 +++++- .../AppleDeveloperProfileCredential.java | 10 +++++--- 3 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 jenkins-client/src/main/java/com/offbytwo/jenkins/client/FormBinaryField.java diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/client/FormBinaryField.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/FormBinaryField.java new file mode 100644 index 00000000..72d25b7e --- /dev/null +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/FormBinaryField.java @@ -0,0 +1,25 @@ +package com.offbytwo.jenkins.client; + +public class FormBinaryField { + private String fileName; + private String contentType; + private byte[] content; + + public FormBinaryField(String fileName, String contentType, byte[] content) { + this.fileName = fileName; + this.contentType = contentType; + this.content = content; + } + + public String getFileName() { + return fileName; + } + + public String getContentType() { + return contentType; + } + + public byte[] getContent() { + return content; + } +} diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java index 975c2316..e4a7648f 100755 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java @@ -30,6 +30,7 @@ import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.HttpMultipartMode; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.auth.BasicScheme; import org.apache.http.impl.client.BasicCredentialsProvider; @@ -423,6 +424,7 @@ public void post_multipart_form_json(String path, Map data, bool HttpPost request; if (data != null) { MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); for (Map.Entry entry : data.entrySet()) { String fieldName = entry.getKey(); Object fieldValue = entry.getValue(); @@ -430,12 +432,15 @@ public void post_multipart_form_json(String path, Map data, bool builder.addTextBody(fieldName, (String) fieldValue); } else if (fieldValue instanceof byte[]) { builder.addBinaryBody(fieldName, (byte[]) fieldValue); + } else if (fieldValue instanceof FormBinaryField) { + FormBinaryField binaryField = (FormBinaryField) fieldValue; + builder.addBinaryBody(fieldName, binaryField.getContent(), ContentType.create(binaryField.getContentType()), binaryField.getFileName()); } else if (fieldValue instanceof File) { builder.addBinaryBody(fieldName, (File) fieldValue); } else if (fieldValue instanceof InputStream) { builder.addBinaryBody(fieldName, (InputStream) fieldValue); } else { - throw new IllegalArgumentException("type of field " + fieldName + " is not String, byte[], File or InputStream"); + builder.addTextBody(fieldName, JSONObject.fromObject(fieldValue).toString()); } } request = new HttpPost(noapi(path)); diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/AppleDeveloperProfileCredential.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/AppleDeveloperProfileCredential.java index 8b84ab8a..a1f30dd5 100644 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/AppleDeveloperProfileCredential.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/AppleDeveloperProfileCredential.java @@ -1,5 +1,6 @@ package com.offbytwo.jenkins.model.credentials; +import com.offbytwo.jenkins.client.FormBinaryField; import net.sf.json.JSONObject; import java.util.HashMap; @@ -17,6 +18,9 @@ public class AppleDeveloperProfileCredential extends Credential { private static final String FILE_ZERO_FIELD_NAME = "file0"; private static final String FILE_ONE_FIELD_NAME = "file1"; + private static final String DEFAULT_DEV_PROFILE_NAME = "developerProfile.zip"; + private static final String DEFAULT_DEV_PROFULE_CONTENT_TYPE = "application/zip"; + private String password; private byte[] developerProfileContent; @@ -72,14 +76,14 @@ public Map dataForCreate() { jsonData.put("credentials", credentialMap); Map formFields = new HashMap(); - formFields.put(FILE_ZERO_FIELD_NAME, this.getDeveloperProfileContent()); + formFields.put(FILE_ZERO_FIELD_NAME, new FormBinaryField(DEFAULT_DEV_PROFILE_NAME, DEFAULT_DEV_PROFULE_CONTENT_TYPE, this.getDeveloperProfileContent())); formFields.put("_.scope", SCOPE_GLOBAL); formFields.put("_.password", this.getPassword()); formFields.put("_.id", this.getId()); formFields.put("_.description", this.getDescription()); formFields.put("stapler-class", BASECLASS); formFields.put("$class", BASECLASS); - formFields.put("json", JSONObject.fromObject(jsonData).toString()); + formFields.put("json", jsonData); return formFields; } @@ -95,7 +99,7 @@ public Map dataForUpdate() { Map formFields = new HashMap(); - formFields.put(FILE_ONE_FIELD_NAME, this.getDeveloperProfileContent()); + formFields.put(FILE_ONE_FIELD_NAME, new FormBinaryField(DEFAULT_DEV_PROFILE_NAME, DEFAULT_DEV_PROFULE_CONTENT_TYPE, this.getDeveloperProfileContent())); formFields.put("_.", "on"); formFields.put("_.password", this.getPassword()); formFields.put("_.id", this.getId());