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 4b8cce3b..8708f030 100644 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java @@ -6,20 +6,6 @@ package com.offbytwo.jenkins; -import java.io.IOException; -import java.net.URI; -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; @@ -27,21 +13,24 @@ 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.List; +import java.util.Map; /** * The main starting point for interacting with a Jenkins server. @@ -51,6 +40,8 @@ public class JenkinsServer { private final JenkinsHttpClient client; + private CredentialManager credentialManager; + /** * Create a new Jenkins server reference given only the server address * @@ -917,4 +908,83 @@ private String toViewBaseUrl(FolderJob folder, String name) { return toBaseUrl(folder) + "view/" + EncodingUtils.encode(name); } + /** + * 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 ee4342bd..838d33f3 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 @@ -312,6 +312,58 @@ public void post_form(String path, Map data, boolean crumbFlag) } } + /** + * 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)); + } +}