-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[JENKINS-57570] - Add support for JCasC using SecretBytes #48
Changes from all commits
3b54f03
17d8275
6e4af90
d7c5901
d20b6aa
e5c75d5
5aed408
e803dbd
0138cbb
da22605
f8de1ae
4993643
c02b562
6b24344
5972c00
dd9bc3c
db6e79f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,7 @@ | |
<parent> | ||
<groupId>org.jenkins-ci.plugins</groupId> | ||
<artifactId>plugin</artifactId> | ||
<version>3.8</version> | ||
<version>3.36</version> | ||
</parent> | ||
|
||
<!-- | ||
|
@@ -101,15 +101,16 @@ | |
<artifactId>cobertura-maven-plugin</artifactId> | ||
<version>2.5.2</version> | ||
<configuration> | ||
<instrumentation> | ||
<excludes> | ||
<exclude>com/google/jenkins/plugins/**/Messages.class</exclude> | ||
</excludes> | ||
</instrumentation> | ||
<formats> | ||
<instrumentation> | ||
<excludes> | ||
<exclude>com/google/jenkins/plugins/**/Messages.class</exclude> | ||
</excludes> | ||
</instrumentation> | ||
<formats> | ||
<format>html</format> | ||
<format>xml</format> | ||
</formats> | ||
<format>xml</format> | ||
</formats> | ||
<check/> | ||
</configuration> | ||
</plugin> | ||
</plugins> | ||
|
@@ -120,27 +121,24 @@ | |
<plugin> | ||
<groupId>org.apache.maven.plugins</groupId> | ||
<artifactId>maven-javadoc-plugin</artifactId> | ||
<version>2.9</version> | ||
<version>2.10.4</version> | ||
</plugin> | ||
</plugins> | ||
</reporting> | ||
|
||
<!-- Bring some sanity to version numbering... --> | ||
<properties> | ||
<jenkins.version>1.653</jenkins.version> | ||
<jenkins.version>2.60.3</jenkins.version> | ||
<google.api.version>1.24.1</google.api.version> | ||
<java.level>7</java.level> | ||
<configuration-as-code.version>1.9</configuration-as-code.version> | ||
<credentials.version>2.1.16</credentials.version> | ||
<java.level>8</java.level> | ||
<findbugs.excludeFilterFile>findbugs-exclude.xml</findbugs.excludeFilterFile> | ||
<findbugs.effort>Max</findbugs.effort> | ||
<findbugs.threshold>Medium</findbugs.threshold> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>net.sourceforge.findbugs</groupId> | ||
<artifactId>jsr305</artifactId> | ||
<version>1.3.2</version> | ||
</dependency> | ||
<!-- com.google.guava --> | ||
<dependency> | ||
<groupId>com.google.guava</groupId> | ||
|
@@ -176,6 +174,43 @@ | |
<version>1.8.4</version> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.jenkins</groupId> | ||
<artifactId>configuration-as-code</artifactId> | ||
<version>${configuration-as-code.version}</version> | ||
<scope>test</scope> | ||
<!--Marked as optional so hpi:run does not include it. --> | ||
<optional>true</optional> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.jenkins</groupId> | ||
<artifactId>configuration-as-code</artifactId> | ||
<version>${configuration-as-code.version}</version> | ||
<classifier>tests</classifier> | ||
<scope>test</scope> | ||
<optional>true</optional> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.jenkins.configuration-as-code</groupId> | ||
<artifactId>configuration-as-code-support</artifactId> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hopefully, you can get rid of this one soon: |
||
<version>${configuration-as-code.version}</version> | ||
<scope>test</scope> | ||
<optional>true</optional> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.jenkins-ci.plugins</groupId> | ||
<artifactId>plain-credentials</artifactId> | ||
<version>1.5</version> | ||
<scope>test</scope> | ||
<optional>true</optional> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.jenkins-ci.plugins</groupId> | ||
<artifactId>ssh-credentials</artifactId> | ||
<version>1.13</version> | ||
<scope>test</scope> | ||
<optional>true</optional> | ||
</dependency> | ||
|
||
<!-- OAuth Credentials dependency --> | ||
<dependency> | ||
|
@@ -215,8 +250,7 @@ | |
<dependency> | ||
<groupId>org.jenkins-ci.plugins</groupId> | ||
<artifactId>credentials</artifactId> | ||
<version>2.1.12</version> | ||
<scope>compile</scope> | ||
<version>${credentials.version}</version> | ||
</dependency> | ||
<dependency> | ||
<!-- Required to run P12ServiceAccountConfigTestUtil-dependent tests against newer Jenkins core versions. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,18 +34,20 @@ | |
import org.kohsuke.accmod.Restricted; | ||
import org.kohsuke.accmod.restrictions.DoNotUse; | ||
import org.kohsuke.stapler.DataBoundConstructor; | ||
import org.kohsuke.stapler.DataBoundSetter; | ||
|
||
import com.cloudbees.plugins.credentials.SecretBytes; | ||
import com.google.api.client.json.jackson.JacksonFactory; | ||
import com.google.api.client.util.PemReader; | ||
import com.google.api.client.util.Strings; | ||
|
||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; | ||
import hudson.Extension; | ||
import jenkins.model.Jenkins; | ||
|
||
/** | ||
* Provides authentication mechanism for a service account by setting a .json | ||
* private key file. The .json file structure needs to be: | ||
* Provides authentication mechanism for a service account by setting a JSON | ||
* private key file. The JSON file structure needs to be: | ||
* <p> | ||
* <code> | ||
* { | ||
|
@@ -58,6 +60,11 @@ | |
* </code> | ||
*/ | ||
public class JsonServiceAccountConfig extends ServiceAccountConfig { | ||
/* | ||
* TODO(jenkinsci/google-oauth-plugin#50): Dedupe shared functionality in | ||
* google-auth-library. | ||
*/ | ||
|
||
private static final long serialVersionUID = 6818111194672325387L; | ||
private static final Logger LOGGER = | ||
Logger.getLogger(JsonServiceAccountConfig.class.getSimpleName()); | ||
|
@@ -70,41 +77,61 @@ public class JsonServiceAccountConfig extends ServiceAccountConfig { | |
private transient String jsonKeyFile; | ||
private transient JsonKey jsonKey; | ||
|
||
/** @since 0.8 */ | ||
@DataBoundConstructor | ||
public JsonServiceAccountConfig() {} | ||
|
||
/** | ||
* @param jsonKeyFile uploaded json key file | ||
* @param filename | ||
* previous json key file name. | ||
* used if jsonKeyFile is not provided. | ||
* @param secretJsonKey | ||
* previous json key file content. | ||
* used if jsonKeyFile is not provided. | ||
* @since 0.7 | ||
* For being able to load credentials created with versions < 0.8 | ||
* and backwards compatibility with external callers. | ||
* | ||
* @param jsonKeyFile The uploaded JSON key file. | ||
* @param prevJsonKeyFile The path of the previous JSON key file. | ||
* @since 0.3 | ||
*/ | ||
@DataBoundConstructor | ||
public JsonServiceAccountConfig(FileItem jsonKeyFile, | ||
String filename, SecretBytes secretJsonKey) { | ||
if (jsonKeyFile != null && jsonKeyFile.getSize() > 0) { | ||
@Deprecated | ||
public JsonServiceAccountConfig( | ||
FileItem jsonKeyFile, String prevJsonKeyFile) { | ||
this.setJsonKeyFileUpload(jsonKeyFile); | ||
if (filename == null && prevJsonKeyFile != null) { | ||
this.filename = extractFilename(prevJsonKeyFile); | ||
this.secretJsonKey = getSecretBytesFromFile(prevJsonKeyFile); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the behavior if filename and secretJsonKey are still null at this point? Is it worth logging this case and/or throwing an exception for this case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If both are still null, then they would first see a log message from the call to setUploadJsonKeyFile. With both null, the credentials created wouldn't be usable so I think at the very least adding a log message here makes sense. I've purposefully avoided using exceptions in the databound setters to avoid the demon jenkins page, but for this constructor it may make sense to throw an exception instead because it is not called directly in the UI. |
||
} | ||
|
||
/** @param jsonKeyFileUpload The uploaded JSON key file. */ | ||
@DataBoundSetter // Called on form submit, only used when key file is uploaded | ||
public void setJsonKeyFileUpload(FileItem jsonKeyFileUpload) { | ||
if (jsonKeyFileUpload != null && jsonKeyFileUpload.getSize() > 0) { | ||
try { | ||
JsonKey jsonKey = JsonKey.load(new JacksonFactory(), | ||
jsonKeyFile.getInputStream()); | ||
jsonKeyFileUpload.getInputStream()); | ||
if (jsonKey.getClientEmail() != null && | ||
jsonKey.getPrivateKey() != null) { | ||
this.filename = extractFilename(jsonKeyFile.getName()); | ||
this.secretJsonKey = SecretBytes.fromBytes(jsonKeyFile.get()); | ||
jsonKey.getPrivateKey() != null) { | ||
this.filename = extractFilename(jsonKeyFileUpload.getName()); | ||
this.secretJsonKey = SecretBytes.fromBytes(jsonKeyFileUpload.get()); | ||
} | ||
} catch (IOException e) { | ||
LOGGER.log(Level.SEVERE, "Failed to read json key from file", e); | ||
LOGGER.log(Level.SEVERE, "Failed to read JSON key from file", e); | ||
} | ||
} else { | ||
this.filename = extractFilename(filename); | ||
this.secretJsonKey = secretJsonKey; | ||
} | ||
} | ||
|
||
@Deprecated | ||
public JsonServiceAccountConfig(FileItem jsonKeyFile, | ||
String prevJsonKeyFile) { | ||
this(null, prevJsonKeyFile, getSecretBytesFromFile(prevJsonKeyFile)); | ||
/** @param filename The JSON key file name. */ | ||
@DataBoundSetter | ||
public void setFilename(String filename) { | ||
String newFilename = extractFilename(filename); | ||
if (!Strings.isNullOrEmpty(newFilename)) { | ||
this.filename = newFilename; | ||
} | ||
} | ||
|
||
/** @param secretJsonKey The JSON key file content. */ | ||
@DataBoundSetter | ||
public void setSecretJsonKey(SecretBytes secretJsonKey) { | ||
if (secretJsonKey != null && secretJsonKey.getPlainData().length > 0) { | ||
this.secretJsonKey = secretJsonKey; | ||
} | ||
} | ||
|
||
@Deprecated // used only for compatibility purpose | ||
|
@@ -139,7 +166,7 @@ private Object readResolve() { | |
if (secretJsonKey == null) { | ||
// google-oauth-plugin < 0.7 | ||
return new JsonServiceAccountConfig( | ||
null, | ||
null, | ||
getJsonKeyFile() | ||
); | ||
} | ||
|
@@ -153,15 +180,15 @@ public DescriptorImpl getDescriptor() { | |
} | ||
|
||
/** | ||
* @return Original uploaded file name | ||
* @return Original uploaded file name. | ||
* @since 0.7 | ||
*/ | ||
@CheckForNull | ||
public String getFilename() { | ||
return filename; | ||
} | ||
|
||
@Restricted(DoNotUse.class) // for UI purpose only | ||
@Restricted(DoNotUse.class) // UI: Required for stapler call of setter. | ||
@CheckForNull | ||
public SecretBytes getSecretJsonKey() { | ||
return secretJsonKey; | ||
|
@@ -172,6 +199,23 @@ public String getJsonKeyFile() { | |
return jsonKeyFile; | ||
} | ||
|
||
/** | ||
* For use in UI, do not use. | ||
* @return The uploaded JSON key file. | ||
*/ | ||
@Deprecated | ||
@Restricted(DoNotUse.class) // UI: Required for stapler call of setter. | ||
public FileItem getJsonKeyFileUpload() { | ||
return null; | ||
} | ||
|
||
/** | ||
* In this context the service account id is represented by the email address | ||
* for that service account, which should be contained in the JSON key. | ||
* | ||
* @return The service account identifier. Null if no JSON key has been | ||
* provided. | ||
*/ | ||
@Override | ||
public String getAccountId() { | ||
JsonKey jsonKey = getJsonKey(); | ||
|
@@ -181,6 +225,10 @@ public String getAccountId() { | |
return null; | ||
} | ||
|
||
/** | ||
* @return The {@link PrivateKey} that comes from the secret JSON key. Null if | ||
* this service account config contains no key or if the key is malformed. | ||
*/ | ||
@Override | ||
public PrivateKey getPrivateKey() { | ||
JsonKey jsonKey = getJsonKey(); | ||
|
@@ -190,14 +238,16 @@ public PrivateKey getPrivateKey() { | |
PemReader pemReader = new PemReader(new StringReader(privateKey)); | ||
try { | ||
PemReader.Section section = pemReader.readNextSection(); | ||
PKCS8EncodedKeySpec keySpec = | ||
new PKCS8EncodedKeySpec(section.getBase64DecodedBytes()); | ||
return KeyFactory.getInstance("RSA").generatePrivate(keySpec); | ||
} catch (IOException e) { | ||
LOGGER.log(Level.SEVERE, "Failed to read private key", e); | ||
} catch (InvalidKeySpecException e) { | ||
LOGGER.log(Level.SEVERE, "Failed to read private key", e); | ||
} catch (NoSuchAlgorithmException e) { | ||
if (section != null) { | ||
PKCS8EncodedKeySpec keySpec = | ||
new PKCS8EncodedKeySpec(section.getBase64DecodedBytes()); | ||
return KeyFactory.getInstance("RSA").generatePrivate(keySpec); | ||
} else { | ||
LOGGER.severe("The provided private key is malformed."); | ||
} | ||
} catch (IOException | ||
| InvalidKeySpecException | ||
| NoSuchAlgorithmException e) { | ||
LOGGER.log(Level.SEVERE, "Failed to read private key", e); | ||
} | ||
} | ||
|
@@ -218,7 +268,7 @@ private JsonKey getJsonKey() { | |
} | ||
|
||
/** | ||
* descriptor for .json service account authentication | ||
* Descriptor for JSON service account authentication. | ||
*/ | ||
@Extension | ||
public static final class DescriptorImpl extends Descriptor { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks an inconsistent indentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't worry too much about formatting nits in this PR. We're about to make the code formatting automatic across all GCP supported plugins.