-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathTestingBotCredentials.java
More file actions
355 lines (317 loc) · 14.1 KB
/
Copy pathTestingBotCredentials.java
File metadata and controls
355 lines (317 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
package testingbot;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.verb.POST;
import com.cloudbees.plugins.credentials.BaseCredentials;
import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.CredentialsDescriptor;
import com.cloudbees.plugins.credentials.CredentialsMatcher;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsNameProvider;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.CredentialsStore;
import com.cloudbees.plugins.credentials.NameWith;
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
import com.cloudbees.plugins.credentials.common.IdCredentials;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.domains.Domain;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
import com.testingbot.models.TestingbotUser;
import com.testingbot.testingbotrest.TestingbotApiException;
import com.testingbot.testingbotrest.TestingbotREST;
import com.testingbot.testingbotrest.TestingbotUnauthorizedException;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.Util;
import hudson.model.AbstractItem;
import hudson.model.Item;
import hudson.model.ModelObject;
import hudson.model.User;
import hudson.security.ACL;
import hudson.util.FormValidation;
import hudson.util.Secret;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import jenkins.model.Jenkins;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
@NameWith(value = TestingBotCredentials.NameProvider.class)
public class TestingBotCredentials extends BaseCredentials implements StandardCredentials {
private static final String CREDENTIAL_DISPLAY_NAME = "TestingBot";
private final String id;
private final String description;
private final String key;
private final Secret secret;
@DataBoundConstructor
public TestingBotCredentials(String id, String description, String key, String secret) {
super(CredentialsScope.GLOBAL);
this.id = IdCredentials.Helpers.fixEmptyId(id);
this.description = Util.fixNull(description);
this.key = Util.fixNull(key);
this.secret = Secret.fromString(secret);
}
@Exported
public String getKey() {
return key;
}
public Secret getSecret() {
return secret;
}
public String getDecryptedSecret() {
return secret.getPlainText();
}
@NonNull
@Exported
public String getDescription() {
return description;
}
@NonNull
@Exported
public String getId() {
return id;
}
@Override
public final boolean equals(Object o) {
return IdCredentials.Helpers.equals(this, o);
}
@Override
public final int hashCode() {
return IdCredentials.Helpers.hashCode(this);
}
private static List<String> getLegacyCredentials() {
DataInputStream in = null;
BufferedReader br = null;
try {
String apiKey = null;
String apiSecret = null;
FileInputStream fstream = new FileInputStream(Paths.get(System.getProperty("user.home"), ".testingbot").toFile());
// Get the object of DataInputStream
in = new DataInputStream(fstream);
br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
String strLine = br.readLine();
if (strLine == null) {
return null;
}
String[] data = strLine.split(":");
if (data.length < 2) {
return null;
}
apiKey = data[0];
apiSecret = data[1];
List<String> credentials = new ArrayList<>();
credentials.add(apiKey);
credentials.add(apiSecret);
return credentials;
} catch (IOException e) {
Logger.getLogger(TestingBotCredentials.class.getName()).log(Level.SEVERE, null, e);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
public static void migrate() throws IOException {
final List<TestingBotCredentials> existingCredentials = TestingBotCredentials.availableCredentials(null);
Logger.getLogger(TestingBotCredentials.class.getName()).log(Level.INFO, "existingCredentials " + existingCredentials.size());
if (existingCredentials == null || existingCredentials.isEmpty()) {
Logger.getLogger(TestingBotCredentials.class.getName()).log(Level.INFO, "No existing credentials, will try to migrate");
String createdCredentialId = UUID.randomUUID().toString();
final List<String> legacyCredentials = TestingBotCredentials.getLegacyCredentials();
if (legacyCredentials == null) {
return;
}
Logger.getLogger(TestingBotCredentials.class.getName()).log(Level.INFO, "Found existing TestingBot credentials");
final StandardCredentials credentialsToCreate;
credentialsToCreate = new TestingBotCredentials(
createdCredentialId,
"migrated from old TestingBot credentials",
legacyCredentials.get(0),
legacyCredentials.get(1)
);
final SystemCredentialsProvider credentialsProvider = SystemCredentialsProvider.getInstance();
final Map<Domain, List<Credentials>> credentialsMap = credentialsProvider.getDomainCredentialsMap();
final Domain domain = Domain.global();
if (credentialsMap.get(domain) == null) {
credentialsMap.put(domain, new ArrayList<>());
}
credentialsMap.get(domain).add(credentialsToCreate);
credentialsProvider.setDomainCredentialsMap(credentialsMap);
credentialsProvider.save();
}
}
public static TestingBotCredentials getCredentials(final AbstractItem buildItem, final String credentialsId) {
List<TestingBotCredentials> available = availableCredentials(buildItem);
if (available.isEmpty()) {
return null;
}
if (credentialsId != null) {
// A specific id was requested: return it or null — never silently fall back to a
// different account when the id does not match (renamed/mistyped/deleted credential).
return CredentialsMatchers.firstOrNull(available,
CredentialsMatchers.withId(credentialsId));
}
// No id specified: use the first available credential.
return available.get(0);
}
public static List<TestingBotCredentials> availableCredentials(final AbstractItem abstractItem) {
if (abstractItem == null) {
return CredentialsProvider.lookupCredentialsInItemGroup(
TestingBotCredentials.class, Jenkins.get(), ACL.SYSTEM2, Collections.emptyList());
}
return CredentialsProvider.lookupCredentialsInItem(
TestingBotCredentials.class, abstractItem, ACL.SYSTEM2, Collections.emptyList());
}
@Extension(ordinal = 1.0D)
@Symbol("testingbot")
public static class DescriptorImpl extends CredentialsDescriptor {
public DescriptorImpl() {
clazz.asSubclass(TestingBotCredentials.class);
}
public DescriptorImpl(Class<? extends BaseStandardCredentials> clazz) {
super(clazz);
}
@Override
public String getDisplayName() {
return CREDENTIAL_DISPLAY_NAME;
}
/**
* @return always returns false since the scope of Local credentials are
* always Global.
*/
@Override
public boolean isScopeRelevant() {
return false;
}
/**
* @return always returns false since the scope of Local credentials are
* always Global.
*/
@SuppressWarnings("unused") // used by stapler
public boolean isScopeRelevant(ModelObject object) {
return false;
}
@CheckForNull
private static FormValidation checkForDuplicates(String value, ModelObject context, ModelObject object) {
for (CredentialsStore store : CredentialsProvider.lookupStores(object)) {
if (!store.hasPermission(CredentialsProvider.VIEW)) {
continue;
}
ModelObject storeContext = store.getContext();
for (Domain domain : store.getDomains()) {
if (CredentialsMatchers.firstOrNull(store.getCredentials(domain), CredentialsMatchers.withId(value))
!= null) {
if (storeContext == context) {
return FormValidation.error("This ID is already in use");
} else {
return FormValidation.warning("The ID ‘%s’ is already in use in %s", value,
storeContext instanceof Item
? ((Item) storeContext).getFullDisplayName()
: storeContext.getDisplayName());
}
}
}
}
return null;
}
@POST
public final FormValidation doCheckId(@QueryParameter String value, @AncestorInPath ModelObject context) {
if (value.isEmpty()) {
return FormValidation.ok();
}
if (!value.matches("[a-zA-Z0-9_.-]+")) { // anything else considered kosher?
return FormValidation.error("Unacceptable characters");
}
FormValidation problem = checkForDuplicates(value, context, context);
if (problem != null) {
return problem;
}
if (!(context instanceof User)) {
User me = User.current();
if (me != null) {
problem = checkForDuplicates(value, context, me);
if (problem != null) {
return problem;
}
}
}
if (!(context instanceof Jenkins)) {
// CredentialsProvider.lookupStores(User) does not return SystemCredentialsProvider.
Jenkins j = Jenkins.getInstanceOrNull();
if (j != null) {
problem = checkForDuplicates(value, context, j);
if (problem != null) {
return problem;
}
}
}
return FormValidation.ok();
}
/**
* Verifies a key/secret against the TestingBot API and reports whether they authenticate.
* Gated so only users who can configure the surrounding item (or admins) can probe.
*/
@POST
public FormValidation doVerifyCredentials(@QueryParameter String key, @QueryParameter String secret,
@AncestorInPath ModelObject context) {
boolean allowed = Jenkins.get().hasPermission(Jenkins.ADMINISTER)
|| (context instanceof Item && ((Item) context).hasPermission(Item.CONFIGURE));
if (!allowed) {
return FormValidation.error("You do not have permission to verify credentials.");
}
if (Util.fixEmptyAndTrim(key) == null || Util.fixEmptyAndTrim(secret) == null) {
return FormValidation.error("Enter both a key and a secret first.");
}
// The password field may submit the encrypted Secret when editing an existing credential;
// fromString transparently yields the plaintext either way.
String plainSecret = Secret.fromString(secret).getPlainText();
try (TestingbotREST rest = new TestingbotREST(key.trim(), plainSecret)) {
TestingbotUser user = rest.getUserInfo();
if (user == null || Util.fixEmptyAndTrim(user.getEmail()) == null) {
return FormValidation.error("Could not verify these credentials with TestingBot.");
}
String plan = Util.fixEmptyAndTrim(user.getPlan());
return FormValidation.ok("Connection successful — signed in as %s%s",
user.getEmail(), plan != null ? " (" + plan + " plan)" : "");
} catch (TestingbotUnauthorizedException | TestingbotApiException e) {
// The REST client's typed failures: bad key/secret, and network/parse errors (it wraps
// IOException into TestingbotApiException). Unexpected runtime exceptions propagate.
return FormValidation.error("TestingBot authentication failed: " + e.getMessage());
}
}
}
public static class NameProvider extends CredentialsNameProvider<TestingBotCredentials> {
@Override
public String getName(TestingBotCredentials credentials) {
String description = Util.fixEmptyAndTrim(credentials.getDescription());
return credentials.getKey() + "/******" + (description != null ? " (" + description + ")" : "");
}
}
}