Skip to content

Commit 01d8cda

Browse files
author
Wei Li
committed
✨ add a new credential type: Apple Developer Profile
1 parent faf92c9 commit 01d8cda

File tree

9 files changed

+216
-8
lines changed

9 files changed

+216
-8
lines changed

jenkins-client-it-docker/plugins.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ job-dsl:1.41
88
config-file-provider:2.10.0
99
testng-plugin:1.10
1010
cloudbees-folder: 5.12
11+
xcode-plugin: 2.0.0

jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedManageCredentialsIT.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private void runTest(JenkinsServer jenkinsServer) throws IOException {
7272

7373
credentialOperations(jenkinsServer, sshCredential);
7474

75-
//test credential
75+
//test certificate credential
7676
CertificateCredential certificateCredential = new CertificateCredential();
7777
certificateCredential.setId("certficateTest-" + RandomStringUtils.randomAlphanumeric(24));
7878
certificateCredential.setCertificateSourceType(CertificateCredential.CERTIFICATE_SOURCE_TYPES.FILE_ON_MASTER);
@@ -81,6 +81,13 @@ private void runTest(JenkinsServer jenkinsServer) throws IOException {
8181

8282
credentialOperations(jenkinsServer, certificateCredential);
8383

84+
//test AppleDeveloperProfileCredential
85+
AppleDeveloperProfileCredential appleDevProfile = new AppleDeveloperProfileCredential();
86+
appleDevProfile.setId("appleProfileTest-" + RandomStringUtils.randomAlphanumeric(24));
87+
appleDevProfile.setPassword(testPassword);
88+
appleDevProfile.setDeveloperProfileContent("testprofile".getBytes());
89+
90+
credentialOperations(jenkinsServer, appleDevProfile);
8491
}
8592

8693
private void credentialOperations(JenkinsServer jenkinsServer, Credential credential) throws IOException {

jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedPluginManagerIT.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ public void getPluginsShouldReturn9ForJenkins20() {
3333
}
3434

3535
@Test
36-
public void getPluginsShouldReturn27ForJenkins1651() {
36+
public void getPluginsShouldReturn28ForJenkins1651() {
3737
JenkinsVersion jv = jenkinsServer.getVersion();
3838
if (jv.isLessThan("1.651") && jv.isGreaterThan("1.651.3")) {
3939
throw new SkipException("Not Version 1.651 (" + jv.toString() + ")");
4040
}
41-
assertThat(pluginManager.getPlugins()).hasSize(27);
41+
assertThat(pluginManager.getPlugins()).hasSize(28);
4242
}
4343

4444
private Plugin createPlugin(String shortName, String version) {
@@ -101,7 +101,7 @@ public void getPluginsShouldReturnTheListOfInstalledPluginsFor1651() {
101101
// instead of maintaining at two locations.
102102
//@formatter:off
103103
Plugin[] expectedPlugins = {
104-
createPlugin("token-macro", "1.12.1"),
104+
createPlugin("token-macro", "1.12.1"),
105105
createPlugin("translation", "1.10"),
106106
createPlugin("testng-plugin", "1.10"),
107107
createPlugin("matrix-project", "1.4.1"),
@@ -127,7 +127,8 @@ public void getPluginsShouldReturnTheListOfInstalledPluginsFor1651() {
127127
createPlugin("throttle-concurrents", "1.9.0"),
128128
createPlugin("subversion", "1.54"),
129129
createPlugin("ssh-slaves", "1.9"),
130-
createPlugin("cloudbees-folder", "5.12"),
130+
createPlugin("cloudbees-folder", "5.12"),
131+
createPlugin("xcode-plugin", "2.0.0"),
131132
};
132133
//@formatter:on
133134
List<Plugin> plugins = pluginManager.getPlugins();

jenkins-client/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@
7171
<artifactId>httpclient</artifactId>
7272
</dependency>
7373

74+
<dependency>
75+
<groupId>org.apache.httpcomponents</groupId>
76+
<artifactId>httpmime</artifactId>
77+
</dependency>
78+
7479
<dependency>
7580
<groupId>jaxen</groupId>
7681
<artifactId>jaxen</artifactId>

jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.apache.http.client.methods.HttpRequestBase;
3131
import org.apache.http.entity.ContentType;
3232
import org.apache.http.entity.StringEntity;
33+
import org.apache.http.entity.mime.MultipartEntityBuilder;
3334
import org.apache.http.impl.auth.BasicScheme;
3435
import org.apache.http.impl.client.BasicCredentialsProvider;
3536
import org.apache.http.impl.client.CloseableHttpClient;
@@ -41,6 +42,7 @@
4142
import org.slf4j.Logger;
4243
import org.slf4j.LoggerFactory;
4344

45+
import java.io.File;
4446
import java.io.IOException;
4547
import java.io.InputStream;
4648
import java.net.URI;
@@ -397,6 +399,67 @@ public void post_form_json(String path, Map<String, Object> data, boolean crumbF
397399
}
398400
}
399401

402+
/**
403+
* Perform a POST request using multipart-form.
404+
*
405+
* This method was added for the purposes of creating some types of credentials, but may be
406+
* useful for other API calls as well.
407+
*
408+
* Unlike post and post_xml, the path is *not* modified by adding
409+
* "/api/json". Additionally, the params in data are provided as both
410+
* request parameters including a json parameter, *and* in the
411+
* JSON-formatted StringEntity, because this is what the folder creation
412+
* call required. It is unclear if any other jenkins APIs operate in this
413+
* fashion.
414+
*
415+
* @param path path to request, can be relative or absolute
416+
* @param data data to post
417+
* @param crumbFlag true / false.
418+
* @throws IOException in case of an error.
419+
*/
420+
public void post_multipart_form_json(String path, Map<String, Object> data, boolean crumbFlag) throws IOException {
421+
HttpPost request;
422+
if (data != null) {
423+
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
424+
for (Map.Entry<String, Object> entry : data.entrySet()) {
425+
String fieldName = entry.getKey();
426+
Object fieldValue = entry.getValue();
427+
if (fieldValue instanceof String) {
428+
builder.addTextBody(fieldName, (String) fieldValue);
429+
} else if (fieldValue instanceof byte[]) {
430+
builder.addBinaryBody(fieldName, (byte[]) fieldValue);
431+
} else if (fieldValue instanceof File) {
432+
builder.addBinaryBody(fieldName, (File) fieldValue);
433+
} else if (fieldValue instanceof InputStream) {
434+
builder.addBinaryBody(fieldName, (InputStream) fieldValue);
435+
} else {
436+
throw new IllegalArgumentException("type of field " + fieldName + " is not String, byte[], File or InputStream");
437+
}
438+
}
439+
request = new HttpPost(noapi(path));
440+
request.setEntity(builder.build());
441+
} else {
442+
request = new HttpPost(noapi(path));
443+
}
444+
445+
if (crumbFlag == true) {
446+
Crumb crumb = get("/crumbIssuer", Crumb.class);
447+
if (crumb != null) {
448+
request.addHeader(new BasicHeader(crumb.getCrumbRequestField(), crumb.getCrumb()));
449+
}
450+
}
451+
452+
HttpResponse response = client.execute(request, localContext);
453+
getJenkinsVersionFromHeader(response);
454+
455+
try {
456+
httpResponseValidator.validateResponse(response);
457+
} finally {
458+
EntityUtils.consume(response.getEntity());
459+
releaseConnection(request);
460+
}
461+
}
462+
400463

401464
/**
402465
* Perform a POST request of XML (instead of using json mapper) and return a
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.offbytwo.jenkins.model.credentials;
2+
3+
import net.sf.json.JSONObject;
4+
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
8+
/**
9+
* Apple developer profile credential type.
10+
*
11+
* NOTE: this type is only available on Jenkins after the xcode plugin (https://wiki.jenkins.io/display/JENKINS/Xcode+Plugin) is installed.
12+
*/
13+
public class AppleDeveloperProfileCredential extends Credential {
14+
public static final String TYPENAME = "Apple Developer Profile";
15+
16+
private static final String BASECLASS = "au.com.rayh.DeveloperProfile";
17+
private static final String FILE_ZERO_FIELD_NAME = "file0";
18+
private static final String FILE_ONE_FIELD_NAME = "file1";
19+
20+
private String password;
21+
private byte[] developerProfileContent;
22+
23+
public String getPassword() {
24+
return password;
25+
}
26+
27+
/**
28+
* Set the password of the developer profile
29+
* @param password
30+
*/
31+
public void setPassword(String password) {
32+
this.password = password;
33+
}
34+
35+
public byte[] getDeveloperProfileContent() {
36+
return developerProfileContent;
37+
}
38+
39+
/**
40+
* Set the content of the developer profile. A developer profile file is a zip with the following structure:
41+
*
42+
* developerprofile/
43+
* - account.keychain (can be empty. Required for validation. The plugin will create a new keychain before build)
44+
* - identities
45+
* |- <name>.p12 (A exported P12 file. Should contain both certificate and private key)
46+
* - profiles
47+
* |- <name>.mobileprovision (A mobile provisioning profile)
48+
* @param developerProfileContent
49+
*/
50+
public void setDeveloperProfileContent(byte[] developerProfileContent) {
51+
this.developerProfileContent = developerProfileContent;
52+
}
53+
54+
@Override
55+
public boolean useMultipartForm() {
56+
return true;
57+
}
58+
59+
@Override
60+
public Map<String, Object> dataForCreate() {
61+
Map<String, String> credentialMap = new HashMap<String, String>();
62+
credentialMap.put("image", FILE_ZERO_FIELD_NAME);
63+
credentialMap.put("password", this.getPassword());
64+
credentialMap.put("id", this.getId());
65+
credentialMap.put("description", this.getDescription());
66+
credentialMap.put("stapler-class", BASECLASS);
67+
credentialMap.put("$class", BASECLASS);
68+
69+
70+
Map<String, Object> jsonData = new HashMap<>();
71+
jsonData.put("", "1");
72+
jsonData.put("credentials", credentialMap);
73+
74+
Map<String, Object> formFields = new HashMap<String, Object>();
75+
formFields.put(FILE_ZERO_FIELD_NAME, this.getDeveloperProfileContent());
76+
formFields.put("_.scope", SCOPE_GLOBAL);
77+
formFields.put("_.password", this.getPassword());
78+
formFields.put("_.id", this.getId());
79+
formFields.put("_.description", this.getDescription());
80+
formFields.put("stapler-class", BASECLASS);
81+
formFields.put("$class", BASECLASS);
82+
formFields.put("json", JSONObject.fromObject(jsonData).toString());
83+
return formFields;
84+
}
85+
86+
@Override
87+
public Map<String, Object> dataForUpdate() {
88+
Map<String, Object> credentialMap = new HashMap<String, Object>();
89+
credentialMap.put("image", FILE_ONE_FIELD_NAME);
90+
credentialMap.put("password", this.getPassword());
91+
credentialMap.put("id", this.getId());
92+
credentialMap.put("description", this.getDescription());
93+
credentialMap.put("stapler-class", BASECLASS);
94+
credentialMap.put("", true);
95+
96+
97+
Map<String, Object> formFields = new HashMap<String, Object>();
98+
formFields.put(FILE_ONE_FIELD_NAME, this.getDeveloperProfileContent());
99+
formFields.put("_.", "on");
100+
formFields.put("_.password", this.getPassword());
101+
formFields.put("_.id", this.getId());
102+
formFields.put("_.description", this.getDescription());
103+
formFields.put("stapler-class", BASECLASS);
104+
formFields.put("json", JSONObject.fromObject(credentialMap).toString());
105+
return formFields;
106+
}
107+
}

jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/Credential.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
@JsonSubTypes({@JsonSubTypes.Type(value = UsernamePasswordCredential.class, name = UsernamePasswordCredential.TYPENAME),
1212
@JsonSubTypes.Type(value = SSHKeyCredential.class, name = SSHKeyCredential.TYPENAME),
1313
@JsonSubTypes.Type(value = SecretTextCredential.class, name = SecretTextCredential.TYPENAME),
14-
@JsonSubTypes.Type(value = CertificateCredential.class, name = CertificateCredential.TYPENAME)})
14+
@JsonSubTypes.Type(value = CertificateCredential.class, name = CertificateCredential.TYPENAME),
15+
@JsonSubTypes.Type(value = AppleDeveloperProfileCredential.class, name = AppleDeveloperProfileCredential.TYPENAME)})
1516
/**
1617
* Base class for credentials. Should not be instantiated directly.
1718
*/
@@ -99,4 +100,12 @@ public void setDisplayName(String displayName) {
99100
public abstract Map<String, Object> dataForCreate();
100101

101102
public abstract Map<String, Object> dataForUpdate();
103+
104+
/**
105+
* Indicate if the request should be sent as multipart/form data
106+
* @return
107+
*/
108+
public boolean useMultipartForm() {
109+
return false;
110+
}
102111
}

jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CredentialManager.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@ public Map<String, Credential> listCredentials() throws IOException {
6767
*/
6868
public void createCredential(Credential credential, Boolean crumbFlag) throws IOException {
6969
String url = String.format("%s/%s?", this.baseUrl, "createCredentials");
70-
this.jenkinsClient.post_form_json(url, credential.dataForCreate(), crumbFlag);
70+
if (credential.useMultipartForm()) {
71+
this.jenkinsClient.post_multipart_form_json(url, credential.dataForCreate(), crumbFlag);
72+
} else {
73+
this.jenkinsClient.post_form_json(url, credential.dataForCreate(), crumbFlag);
74+
}
7175
}
7276

7377
/**
@@ -80,7 +84,11 @@ public void createCredential(Credential credential, Boolean crumbFlag) throws IO
8084
public void updateCredential(String credentialId, Credential credential, Boolean crumbFlag) throws IOException {
8185
credential.setId(credentialId);
8286
String url = String.format("%s/%s/%s/%s?", this.baseUrl, "credential", credentialId, "updateSubmit");
83-
this.jenkinsClient.post_form_json(url, credential.dataForUpdate(), crumbFlag);
87+
if (credential.useMultipartForm()) {
88+
this.jenkinsClient.post_multipart_form_json(url, credential.dataForUpdate(), crumbFlag);
89+
} else {
90+
this.jenkinsClient.post_form_json(url, credential.dataForUpdate(), crumbFlag);
91+
}
8492
}
8593

8694
/**

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
<guava.version>17.0</guava.version>
6262
<json-lib.version>2.4</json-lib.version>
6363
<httpclient.version>4.3.6</httpclient.version>
64+
<httpmime.version>4.3.6</httpmime.version>
6465
<jackson-databind.version>2.3.4</jackson-databind.version>
6566
</properties>
6667

@@ -148,6 +149,12 @@
148149
<version>${httpclient.version}</version>
149150
</dependency>
150151

152+
<dependency>
153+
<groupId>org.apache.httpcomponents</groupId>
154+
<artifactId>httpmime</artifactId>
155+
<version>${httpmime.version}</version>
156+
</dependency>
157+
151158
<dependency>
152159
<groupId>jaxen</groupId>
153160
<artifactId>jaxen</artifactId>

0 commit comments

Comments
 (0)