diff --git a/pom.xml b/pom.xml index cbb2f1971..3447508ad 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ org.jenkins-ci.plugins plugin - 4.85 + 4.88 @@ -67,7 +67,8 @@ 999999-SNAPSHOT jenkinsci/${project.artifactId}-plugin - 2.426.3 + 2.462.3 + 1372 @@ -87,9 +88,8 @@ io.jenkins.tools.bom - bom-2.426.x - - 2961.v1f472390972e + bom-2.452.x + 3208.vb_21177d4b_cd9 import pom @@ -109,7 +109,6 @@ org.jenkins-ci.plugins bouncycastle-api - 2.30.1.78.1-246.ve1089fe22055 @@ -182,7 +181,7 @@ org.antlr antlr4-maven-plugin - 4.13.1 + 4.13.2 true true diff --git a/src/main/java/com/cloudbees/plugins/credentials/SecretBytesRedaction.java b/src/main/java/com/cloudbees/plugins/credentials/SecretBytesRedaction.java new file mode 100644 index 000000000..2f3a0a89a --- /dev/null +++ b/src/main/java/com/cloudbees/plugins/credentials/SecretBytesRedaction.java @@ -0,0 +1,27 @@ +package com.cloudbees.plugins.credentials; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import jenkins.security.ExtendedReadRedaction; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Restricted(NoExternalUse.class) +// @Extension +// See SecretBytesReactionExtension +public class SecretBytesRedaction implements ExtendedReadRedaction { + private static final Pattern SECRET_BYTES_PATTERN = Pattern.compile(">(" + SecretBytes.ENCRYPTED_VALUE_PATTERN + ")<"); + + @Override + public String apply(String configDotXml) { + Matcher matcher = SECRET_BYTES_PATTERN.matcher(configDotXml); + StringBuilder cleanXml = new StringBuilder(); + while (matcher.find()) { + if (SecretBytes.isSecretBytes(matcher.group(1))) { + matcher.appendReplacement(cleanXml, ">********<"); + } + } + matcher.appendTail(cleanXml); + return cleanXml.toString(); + } +} diff --git a/src/main/java/com/cloudbees/plugins/credentials/SecretBytesRedactionExtension.java b/src/main/java/com/cloudbees/plugins/credentials/SecretBytesRedactionExtension.java new file mode 100644 index 000000000..c2663ee7f --- /dev/null +++ b/src/main/java/com/cloudbees/plugins/credentials/SecretBytesRedactionExtension.java @@ -0,0 +1,25 @@ +package com.cloudbees.plugins.credentials; + +import hudson.ExtensionList; +import hudson.init.Initializer; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.security.ExtendedReadRedaction; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Restricted(NoExternalUse.class) +public class SecretBytesRedactionExtension { + + public static final Logger LOGGER = Logger.getLogger(SecretBytesRedactionExtension.class.getName()); + + // TODO Delete this and annotate `SecretBytesRedaction` with `@Extension` once the core dependency is >= 2.479 + @Initializer + public static void create() { + try { + ExtensionList.lookup(ExtendedReadRedaction.class).add(new SecretBytesRedaction()); + } catch (NoClassDefFoundError unused) { + LOGGER.log(Level.WARNING, "Failed to register SecretBytesRedaction. Update Jenkins to add support for redacting credentials in config.xml files from users with ExtendedRead permission. Learn more: https://www.jenkins.io/redirect/plugin/credentials/SecretBytesRedaction/"); + } + } +} diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java index 00121cf79..34e7ccb02 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java @@ -575,11 +575,11 @@ public String getDisplayName() { @Restricted(NoExternalUse.class) @RequirePOST public FormValidation doCheckUploadedKeystore(@QueryParameter String value, - @QueryParameter String uploadedCertFile, + @QueryParameter String certificateBase64, @QueryParameter String password) { // Priority for the file, to cover the (re-)upload cases - if (StringUtils.isNotEmpty(uploadedCertFile)) { - byte[] uploadedCertFileBytes = Base64.getDecoder().decode(uploadedCertFile.getBytes(StandardCharsets.UTF_8)); + if (StringUtils.isNotEmpty(certificateBase64)) { + byte[] uploadedCertFileBytes = Base64.getDecoder().decode(certificateBase64.getBytes(StandardCharsets.UTF_8)); return validateCertificateKeystore(uploadedCertFileBytes, password); } diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImpl.java index bb947482c..34321f8fa 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImpl.java @@ -30,11 +30,19 @@ import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; import hudson.Util; +import hudson.model.Descriptor; +import hudson.util.FormValidation; import hudson.util.Secret; +import jenkins.security.FIPS140; +import org.apache.commons.lang.StringUtils; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; + +import java.util.Objects; /** * Concrete implementation of {@link StandardUsernamePasswordCredentials}. @@ -73,9 +81,13 @@ public class UsernamePasswordCredentialsImpl extends BaseStandardCredentials imp @SuppressWarnings("unused") // by stapler public UsernamePasswordCredentialsImpl(@CheckForNull CredentialsScope scope, @CheckForNull String id, @CheckForNull String description, - @CheckForNull String username, @CheckForNull String password) { + @CheckForNull String username, @CheckForNull String password) + throws Descriptor.FormException { super(scope, id, description); this.username = Util.fixNull(username); + if(FIPS140.useCompliantAlgorithms() && StringUtils.length(password) < 14) { + throw new Descriptor.FormException(Messages.passwordTooShortFIPS(), "password"); + } this.password = Secret.fromString(password); } @@ -128,5 +140,13 @@ public String getDisplayName() { public String getIconClassName() { return "symbol-id-card"; } + + @RequirePOST + public FormValidation doCheckPassword(@QueryParameter String password) { + if(FIPS140.useCompliantAlgorithms() && StringUtils.length(password) < 14) { + return FormValidation.error(Messages.passwordTooShortFIPS()); + } + return FormValidation.ok(); + } } } diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsSnapshotTaker.java b/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsSnapshotTaker.java index d429ff916..85dddb6f2 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsSnapshotTaker.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsSnapshotTaker.java @@ -4,6 +4,7 @@ import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import hudson.Extension; +import hudson.model.Descriptor; import hudson.util.Secret; @Extension @@ -25,8 +26,13 @@ public StandardUsernamePasswordCredentials snapshot(StandardUsernamePasswordCred if (credentials instanceof UsernamePasswordCredentialsImpl) { return credentials; } - UsernamePasswordCredentialsImpl snapshot = new UsernamePasswordCredentialsImpl(credentials.getScope(), credentials.getId(), credentials.getDescription(), credentials.getUsername(), Secret.toString(credentials.getPassword())); - snapshot.setUsernameSecret(credentials.isUsernameSecret()); - return snapshot; + try { + UsernamePasswordCredentialsImpl snapshot = + new UsernamePasswordCredentialsImpl(credentials.getScope(), credentials.getId(), credentials.getDescription(), credentials.getUsername(), Secret.toString(credentials.getPassword())); + snapshot.setUsernameSecret(credentials.isUsernameSecret()); + return snapshot; + } catch (Descriptor.FormException e) { + throw new RuntimeException(e); + } } } \ No newline at end of file diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/CredentialsWrapper/update.jelly b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/CredentialsWrapper/update.jelly index 20d8405b4..e02b4cc8a 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/CredentialsWrapper/update.jelly +++ b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/CredentialsWrapper/update.jelly @@ -49,10 +49,6 @@ - diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/configure.jelly b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/configure.jelly index ee939e262..36e491d41 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/configure.jelly +++ b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/configure.jelly @@ -44,7 +44,7 @@ - + @@ -54,28 +54,14 @@ items="${instance.specifications}"/> - + - + diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials.jelly b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials.jelly index 9dce2c2f2..254c7dbee 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials.jelly +++ b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials.jelly @@ -72,10 +72,6 @@ - diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/newDomain.jelly b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/newDomain.jelly index a19a2cfd5..a15eebd0c 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/newDomain.jelly +++ b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/newDomain.jelly @@ -32,8 +32,7 @@ - - + @@ -43,22 +42,12 @@ items="${null}"/> - + - + diff --git a/src/main/resources/com/cloudbees/plugins/credentials/GlobalCredentialsConfiguration/index.jelly b/src/main/resources/com/cloudbees/plugins/credentials/GlobalCredentialsConfiguration/index.jelly index e9412e42f..3dc57020c 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/GlobalCredentialsConfiguration/index.jelly +++ b/src/main/resources/com/cloudbees/plugins/credentials/GlobalCredentialsConfiguration/index.jelly @@ -45,10 +45,6 @@ - diff --git a/src/main/resources/com/cloudbees/plugins/credentials/common/formBehaviour.js b/src/main/resources/com/cloudbees/plugins/credentials/common/formBehaviour.js new file mode 100644 index 000000000..cbc0d5bba --- /dev/null +++ b/src/main/resources/com/cloudbees/plugins/credentials/common/formBehaviour.js @@ -0,0 +1,13 @@ +Behaviour.specify(".required-for-submit", 'required-for-submit', -99, function(requiredField) { + const saveButton = requiredField.closest("form").querySelector('[name="Submit"]'); + function updateSave() { + const state = requiredField.value.length === 0; + saveButton.disabled = state; + } + requiredField.addEventListener('input', updateSave); + updateSave(saveButton); +}); + +Behaviour.specify(".autofocus", "autofocus", 0, function(el) { + el.focus(); +}); \ No newline at end of file diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/UploadedKeyStoreSource/config.jelly b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/UploadedKeyStoreSource/config.jelly index f0f25d667..9277636bd 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/UploadedKeyStoreSource/config.jelly +++ b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/UploadedKeyStoreSource/config.jelly @@ -24,8 +24,7 @@ ~ THE SOFTWARE. --> - - + - - + - + diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/UploadedKeyStoreSource/configUpload.js b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/UploadedKeyStoreSource/configUpload.js new file mode 100644 index 000000000..5b230fcf2 --- /dev/null +++ b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/UploadedKeyStoreSource/configUpload.js @@ -0,0 +1,61 @@ +// multiple objects named "password" in the form => +// extend findNearBy to allow selecting by id +if (!findNearBy.patched) { + var oldNearBy = findNearBy; + findNearBy = function(el, name) { + return name.charAt(0) == '#' ? document.querySelector(name.split('/')[0]) : oldNearBy(el, name); + } + findNearBy.patched = true; +} + +Behaviour.specify(".certificate-file-upload", 'certificate-file-upload', -99, function(uploadedCertFileInput) { + // Adding a onChange method on the file input to retrieve the value of the file content in a variable + var fileId = uploadedCertFileInput.id; + var _onchange = uploadedCertFileInput.onchange; + if (typeof _onchange === "function") { + uploadedCertFileInput.onchange = function() { fileOnChange(this); _onchange.call(this); } + } else { + uploadedCertFileInput.onchange = fileOnChange.bind(uploadedCertFileInput); + } + const base64field = uploadedCertFileInput.closest('.radioBlock-container').querySelector('[name="certificateBase64"]'); + function fileOnChange() { + // only trigger validation if the PKCS12 upload is selected + if (uploadedCertFileInput.closest(".form-container").className.indexOf("-hidden") != -1) { + return + } + try { // inspired by https://stackoverflow.com/a/754398 + var uploadedCertFileInputFile = uploadedCertFileInput.files[0]; + var reader = new FileReader(); + reader.onload = function (evt) { + base64field.value = btoa(evt.target.result); + var uploadedKeystore = document.getElementById(fileId + "-textbox"); + uploadedKeystore.onchange(uploadedKeystore); + } + reader.onerror = function (evt) { + if (window.console !== null) { + console.warn("Error during loading uploadedCertFile content", evt); + } + uploadedCertFile[fileId] = ''; + } + + reader.readAsBinaryString(uploadedCertFileInputFile); + } + catch(e){ + if (window.console !== null) { + console.warn("Unable to retrieve uploadedCertFile content"); + } + } + } + + // workaround for JENKINS-65616 + // tweaks `checkDependsOn` to reference password by id instead of RelativePath + var r = window.document.getElementById(fileId + "-textbox"); + var p = findNextFormItem(r, 'password'); + if (p) { + const dependsOn = r.getAttribute('checkDependsOn'); + if (!dependsOn.includes(p.id)) { + r.setAttribute('checkDependsOn', dependsOn + ' #' + p.id + "/password"); + } + } +}); + diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/credentials.jelly b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/credentials.jelly index 28a479022..e9d0ce9f0 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/credentials.jelly +++ b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/credentials.jelly @@ -37,7 +37,7 @@ - + diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties b/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties index 608714530..90ab3277c 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties +++ b/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties @@ -44,3 +44,4 @@ CertificateCredentialsImpl.PEMMultipleKeys=More than 1 key provided CertificateCredentialsImpl.PEMNonKeys=PEM contains non key entries CertificateCredentialsImpl.PEMKeyInfo={0} {1} private key CertificateCredentialsImpl.PEMKeyParseError=Could not parse key: {0} +passwordTooShortFIPS=Password is too short (< 14 characters) diff --git a/src/main/resources/lib/credentials/select.jelly b/src/main/resources/lib/credentials/select.jelly index f0650adef..5236ea278 100644 --- a/src/main/resources/lib/credentials/select.jelly +++ b/src/main/resources/lib/credentials/select.jelly @@ -24,7 +24,7 @@ --> + xmlns:f="/lib/form" xmlns:t="/lib/hudson" xmlns:dd="/lib/layout/dropdowns"> A select control that supports the data binding and AJAX updates with support for adding credentials. Your descriptor should have the 'doFillXyzItems' method, which returns a StandardListBoxModel @@ -92,10 +92,10 @@ - - + diff --git a/src/main/resources/lib/credentials/select/select.css b/src/main/resources/lib/credentials/select/select.css index 776d18587..ed6481d97 100644 --- a/src/main/resources/lib/credentials/select/select.css +++ b/src/main/resources/lib/credentials/select/select.css @@ -1,91 +1,6 @@ -select.credentials-select { width:auto; vertical-align: top; } -div.credentials-select-content-inactive { display:none; } -body.masked {overflow-y: hidden;} -div.include-user-credentials { padding: 0.2em; } -/* adapted from Jenkins' style.css */ -.help-content { - position: relative; - padding: 1rem; - margin: 1rem 0; - word-break: break-word; - border-radius: 6px; - z-index: 0; +div.credentials-select-content-inactive { + display:none; } - -.help-content::before { - content: ""; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - background: var(--text-color); - opacity: 0.05; - z-index: -1; - border-radius: inherit; -} - -.help-content p:first-of-type { - margin-top: 0; -} - -.help-content p:last-of-type { - margin-bottom: 0; -} - -.help-content .from-plugin { - text-align: right; - color: #666; -} - -.help-content a { - text-decoration: underline; -} - -@media (min-width: 1600px) { - select.credentials-select { - max-width: 90%; - } -} - -@media (max-width: 1600px) { - select.credentials-select { - max-width: 85%; - } -} - -@media (max-width: 1400px) { - select.credentials-select { - max-width: 80%; - } -} - -@media (max-width: 1200px) { - select.credentials-select { - max-width: 70%; - } -} - -@media (max-width: 800px) { - select.credentials-select { - max-width: 60%; - } -} - -@media (max-width: 767px) { - select.credentials-select { - max-width: 75%; - } -} - -@media (max-width: 700px) { - select.credentials-select { - max-width: 70%; - } -} - -@media (max-width: 600px) { - select.credentials-select { - max-width: 100%; /* force new line */ - } +.help-btn a { + margin-left: .5rem; } diff --git a/src/main/resources/lib/credentials/select/select.js b/src/main/resources/lib/credentials/select/select.js index ba46a696e..1114fb974 100644 --- a/src/main/resources/lib/credentials/select/select.js +++ b/src/main/resources/lib/credentials/select/select.js @@ -127,49 +127,14 @@ window.credentials.addSubmit = function (_) { } }; -Behaviour.specify("BUTTON.credentials-add-menu", 'credentials-select', -99, function(e) { - var btn = e; - var menu = btn.nextElementSibling; - while (menu && !menu.matches('DIV.credentials-add-menu-items')) { - menu = menu.nextElementSibling; - } - if (menu) { - var menuAlign = (btn.getAttribute("menualign") || "tl-bl"); - - var menuButton = new YAHOO.widget.Button(btn, { - type: "menu", - menu: menu, - menualignment: menuAlign.split("-"), - menuminscrollheight: 250 - }); - // copy class names - for (var i = 0; i < btn.classList.length; i++) { - menuButton._button.classList.add(btn.classList.item(i)); - } - menuButton._button.setAttribute("suffix", btn.getAttribute("suffix")); - menuButton.getMenu().clickEvent.subscribe(function (type, args, value) { - var item = args[1]; - if (item.cfg.getProperty("disabled")) { - return; - } - window.credentials.add(item.srcElement.getAttribute('data-url')); - }); - // YUI menu will not parse disabled when using DIV-LI only when using SELECT-OPTION - // but SELECT-OPTION doesn't support images, so we need to catch the rendering and roll our - // own disabled attribute support - menuButton.getMenu().beforeShowEvent.subscribe(function(type,args,value){ - var items = this.getItems(); - for (var i = 0; i < items.length; i++) { - if (items[i].srcElement.getAttribute('disabled')) { - items[i].cfg.setProperty('disabled', true); - } - } - }); - } - e=null; +Behaviour.specify("[data-type='credentials-add-store-item']", 'credentials-add-store-item', -99, function(e) { + e.addEventListener("click", function (event) { + window.credentials.add(event.target.dataset.url); + }); + e = null; }); Behaviour.specify("BUTTON.credentials-add", 'credentials-select', 0, function (e) { - makeButton(e, e.disabled ? null : window.credentials.add); + e.addEventListener("click", window.credentials.add); e = null; // avoid memory leak }); Behaviour.specify("DIV.credentials-select-control", 'credentials-select', 100, function (d) { @@ -220,10 +185,14 @@ Behaviour.specify("DIV.include-user-credentials", 'include-user-credentials', 0, }; // simpler version of f:helpLink using inline help text e.querySelector('span.help-btn').onclick = function (evt) { - var help = e.querySelector('.help-content'); - help.hidden = !help.hidden; + var help = e.querySelector('.help'); + help.style.display = help.style.display == "block" ? "" : "block"; + return false; }; }); +// prevent accidental removal of CSS by moving it to the head (https://issues.jenkins.io/browse/JENKINS-26578) +var style = document.querySelector("body link[href$='credentials/select/select.css']"); +style && document.head.appendChild(style); window.setTimeout(function() { // HACK: can be removed once base version of Jenkins has fix of https://issues.jenkins-ci.org/browse/JENKINS-26578 // need to apply the new behaviours to existing objects diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsSelectHelperTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsSelectHelperTest.java index d01bd87cb..5703712c3 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsSelectHelperTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsSelectHelperTest.java @@ -10,9 +10,7 @@ import org.htmlunit.html.HtmlButton; import org.htmlunit.html.HtmlForm; import org.htmlunit.html.HtmlInput; -import org.htmlunit.html.HtmlListItem; import org.htmlunit.html.HtmlPage; -import org.htmlunit.html.HtmlSpan; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -26,10 +24,15 @@ public class CredentialsSelectHelperTest { public void doAddCredentialsFromPopupWorksAsExpected() throws Exception { try (JenkinsRule.WebClient wc = j.createWebClient()) { HtmlPage htmlPage = wc.goTo("credentials-selection"); + HtmlButton addCredentialsButton = htmlPage.querySelector(".credentials-add-menu"); + // The 'click' event doesn't fire a 'mouseenter' event causing the menu not to show, so let's fire one + addCredentialsButton.fireEvent("mouseenter"); addCredentialsButton.click(); - HtmlListItem li = htmlPage.querySelector(".credentials-add-menu-items li"); - li.click(); + + HtmlButton jenkinsCredentialsOption = htmlPage.querySelector(".jenkins-dropdown__item"); + jenkinsCredentialsOption.click(); + wc.waitForBackgroundJavaScript(4000); HtmlForm form = htmlPage.querySelector("#credentials-dialog-form"); diff --git a/src/test/java/com/cloudbees/plugins/credentials/SecretBytesRedactionTest.java b/src/test/java/com/cloudbees/plugins/credentials/SecretBytesRedactionTest.java new file mode 100644 index 000000000..f11c2b180 --- /dev/null +++ b/src/test/java/com/cloudbees/plugins/credentials/SecretBytesRedactionTest.java @@ -0,0 +1,78 @@ +package com.cloudbees.plugins.credentials; + +import com.cloudbees.hudson.plugins.folder.Folder; +import com.cloudbees.plugins.credentials.domains.Domain; +import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; +import hudson.model.Item; +import hudson.model.ModelObject; +import java.util.Base64; +import java.util.Iterator; +import jenkins.model.Jenkins; +import org.htmlunit.Page; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class SecretBytesRedactionTest { + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + public void testRedaction() throws Exception { + final String usernamePasswordPassword = "thisisthe_theuserpassword"; + final SecretBytes secretBytes = SecretBytes.fromString("thisis_theTestData"); + + Item.EXTENDED_READ.setEnabled(true); + + final Folder folder = j.jenkins.createProject(Folder.class, "F"); + final CredentialsStore store = lookupStore(folder); + final UsernamePasswordCredentialsImpl usernamePasswordCredentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "passwordid", null, "theusername", usernamePasswordPassword); + store.addCredentials(Domain.global(), usernamePasswordCredentials); + store.addCredentials(Domain.global(), new SecretBytesCredential(CredentialsScope.GLOBAL, "certid", "thedesc", secretBytes)); + + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("alice").grant(Item.READ, Item.EXTENDED_READ, Jenkins.READ).everywhere().to("bob")); + + try (JenkinsRule.WebClient webClient = j.createWebClient().login("alice")) { + final Page page = webClient.goTo("job/F/config.xml", "application/xml"); + final String content = page.getWebResponse().getContentAsString(); + assertThat(content, containsString(usernamePasswordCredentials.getPassword().getEncryptedValue())); + assertThat(content, containsString(Base64.getEncoder().encodeToString(secretBytes.getEncryptedData()))); + } + try (JenkinsRule.WebClient webClient = j.createWebClient().login("bob")) { + final Page page = webClient.goTo("job/F/config.xml", "application/xml"); + final String content = page.getWebResponse().getContentAsString(); + assertThat(content, not(containsString(usernamePasswordCredentials.getPassword().getEncryptedValue()))); + assertThat(content, not(containsString(Base64.getEncoder().encodeToString(secretBytes.getEncryptedData())))); + assertThat(content, containsString("********")); + assertThat(content, containsString("********")); + } + } + + // Stolen from BaseStandardCredentialsTest + private static CredentialsStore lookupStore(ModelObject object) { + Iterator stores = CredentialsProvider.lookupStores(object).iterator(); + assertTrue(stores.hasNext()); + CredentialsStore store = stores.next(); + assertEquals("we got the expected store", object, store.getContext()); + return store; + } + + // This would be nicer with a real credential like `FileCredentialsImpl` but another test falls over if we add `plain-credentials` to the test scope + public static class SecretBytesCredential extends BaseStandardCredentials { + private final SecretBytes mySecretBytes; + + public SecretBytesCredential(CredentialsScope scope, String id, String description, SecretBytes bytes) { + super(scope, id, description); + this.mySecretBytes = bytes; + } + } +} diff --git a/src/test/java/com/cloudbees/plugins/credentials/builds/CredentialsParameterBinderTest.java b/src/test/java/com/cloudbees/plugins/credentials/builds/CredentialsParameterBinderTest.java index edcd10ce1..c0bfaba53 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/builds/CredentialsParameterBinderTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/builds/CredentialsParameterBinderTest.java @@ -61,7 +61,7 @@ public class CredentialsParameterBinderTest { private static final String PARAMETER_NAME = "cred"; @BeforeClass - public static void setUpClass() throws IOException { + public static void setUpClass() throws Exception { j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); CredentialsProvider.lookupStores(j.jenkins).iterator().next() .addCredentials(Domain.global(), new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, GLOBAL_CREDENTIALS_ID, "global credential", "root", "correct horse battery staple")); diff --git a/src/test/java/com/cloudbees/plugins/credentials/cli/CLICommandsTest.java b/src/test/java/com/cloudbees/plugins/credentials/cli/CLICommandsTest.java index e142a1f59..1f1cf8880 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/cli/CLICommandsTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/cli/CLICommandsTest.java @@ -307,7 +307,7 @@ public void deleteSmokes() throws Exception { } @Test - public void listCredentialsAsXML() throws IOException { + public void listCredentialsAsXML() throws Exception { Domain smokes = new Domain("smokes", "smoke test domain", Collections.singletonList(new HostnameSpecification("smokes.example.com", null))); UsernamePasswordCredentialsImpl smokey = diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/BaseStandardCredentialsTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/BaseStandardCredentialsTest.java index ec1a1e17c..bc8201adb 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/impl/BaseStandardCredentialsTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/BaseStandardCredentialsTest.java @@ -35,13 +35,11 @@ import hudson.security.ACL; import hudson.security.ACLContext; import hudson.util.FormValidation; -import java.io.IOException; import java.util.Iterator; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.MockFolder; -import org.xml.sax.SAXException; import static hudson.util.FormValidation.Kind.ERROR; import static hudson.util.FormValidation.Kind.OK; @@ -136,7 +134,7 @@ public void doCheckIdDuplication() throws Exception { } @Test - public void noIDValidationMessageOnCredentialsUpdate() throws IOException, SAXException { + public void noIDValidationMessageOnCredentialsUpdate() throws Exception { // create credentials with ID test CredentialsStore store = lookupStore(r.jenkins); addCreds(store, CredentialsScope.GLOBAL, "test"); @@ -154,7 +152,7 @@ private static CredentialsStore lookupStore(ModelObject object) { return store; } - private static void addCreds(CredentialsStore store, CredentialsScope scope, String id) throws IOException { + private static void addCreds(CredentialsStore store, CredentialsScope scope, String id) throws Exception { // For purposes of this test we do not care about domains. store.addCredentials(Domain.global(), new UsernamePasswordCredentialsImpl(scope, id, null, "x", "y")); } diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java index 36782545b..96ff209a0 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java @@ -342,7 +342,7 @@ private String getContentFrom_doCheckUploadedKeystore(String value, String uploa request.setEncodingType(FormEncodingType.URL_ENCODED); request.setRequestBody( "value="+URLEncoder.encode(value, StandardCharsets.UTF_8.name())+ - "&uploadedCertFile="+URLEncoder.encode(uploadedCertFile, StandardCharsets.UTF_8.name())+ + "&certificateBase64="+URLEncoder.encode(uploadedCertFile, StandardCharsets.UTF_8.name())+ "&password="+URLEncoder.encode(password, StandardCharsets.UTF_8.name()) ); diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImplFIPSTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImplFIPSTest.java new file mode 100644 index 000000000..c8ef9a51c --- /dev/null +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImplFIPSTest.java @@ -0,0 +1,91 @@ +package com.cloudbees.plugins.credentials.impl; + +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.CredentialsStore; +import com.cloudbees.plugins.credentials.domains.Domain; +import hudson.ExtensionList; +import hudson.model.Descriptor; +import hudson.security.ACL; +import hudson.util.FormValidation; +import jenkins.model.Jenkins; +import org.apache.commons.text.StringEscapeUtils; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.RealJenkinsRule; + +import java.io.IOException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThrows; + +public class UsernamePasswordCredentialsImplFIPSTest { + + @Rule public RealJenkinsRule rule = new RealJenkinsRule().javaOptions("-Djenkins.security.FIPS140.COMPLIANCE=true", "-Xmx512M") + .withDebugPort(8000).withDebugServer(true).withDebugSuspend(true); + + @Test + public void nonCompliantLaunchExceptionTest() throws Throwable { + rule.then(r -> { + new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "all-good-beer", "Best captain and player in the world", + "Pat Cummins", "theaustraliancricketteamisthebest"); + assertThrows(Descriptor.FormException.class, () -> new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "bad-foo", "someone", + "Virat", "tooshort")); + assertThrows(Descriptor.FormException.class, () -> new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "bad-bar", "duck", + "Rohit", "")); + assertThrows(Descriptor.FormException.class, () -> new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "bad-foo", "not too bad", + "Gill", null)); + }); + } + + @Test + public void invalidIsNotSavedInFIPSModeTest() throws Throwable { + rule.then(r -> + { + UsernamePasswordCredentialsImpl entry = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "all-good", "Best captain and player in the world", + "Pat Cummins", "theaustraliancricketteamisthebest"); + CredentialsStore store = CredentialsProvider.lookupStores(Jenkins.get()).iterator().next(); + store.addCredentials(Domain.global(), entry); + store.save(); + // Valid password is saved + UsernamePasswordCredentialsImpl cred = CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentialsInItem(UsernamePasswordCredentialsImpl.class, null, ACL.SYSTEM2), + CredentialsMatchers.withId("all-good")); + assertThat(cred, notNullValue()); + assertThrows(Descriptor.FormException.class, () -> store.addCredentials(Domain.global(), new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "all-good", "someone", + "foo", "tooshort"))); + store.save(); + // Invalid password size threw an exception, so it wasn't saved + cred = CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentialsInItem(UsernamePasswordCredentialsImpl.class, null, ACL.SYSTEM2), + CredentialsMatchers.withId("all-good")); + assertThat(cred, notNullValue()); + assertThat(cred.getPassword().getPlainText(), is("theaustraliancricketteamisthebest")); + }); + } + + private static void checkInvalidKeyIsNotSavedInFIPSMode(JenkinsRule r) throws IOException { + + } + + @Test + public void formValidationTest() throws Throwable { + rule.then(r -> { + UsernamePasswordCredentialsImpl.DescriptorImpl descriptor = ExtensionList.lookupSingleton(UsernamePasswordCredentialsImpl.DescriptorImpl.class); + FormValidation result = descriptor.doCheckPassword("theaustraliancricketteamisthebest"); + assertThat(result.getMessage(), nullValue()); + result = descriptor.doCheckPassword("foo"); + assertThat(result.getMessage(), not(emptyString())); + assertThat(result.getMessage(), containsString(StringEscapeUtils.escapeHtml4(Messages.passwordTooShortFIPS()))); + }); + } + +} diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImplTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImplTest.java index 4ff083003..9c3f83558 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImplTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImplTest.java @@ -27,6 +27,8 @@ import com.cloudbees.plugins.credentials.CredentialsNameProvider; import com.cloudbees.plugins.credentials.CredentialsScope; import java.util.logging.Level; + +import hudson.model.Descriptor; import jenkins.model.Jenkins; import org.junit.Test; import static org.junit.Assert.*; @@ -40,7 +42,7 @@ public class UsernamePasswordCredentialsImplTest { @Rule public JenkinsRule r = new JenkinsRule(); // needed for Secret.fromString to work @Rule public LoggerRule logging = new LoggerRule().record(CredentialsNameProvider.class, Level.FINE); - @Test public void displayName() { + @Test public void displayName() throws Exception { UsernamePasswordCredentialsImpl creds = new UsernamePasswordCredentialsImpl(null, "abc123", "Bob’s laptop", "bob", "s3cr3t"); assertEquals("bob/****** (Bob’s laptop)", CredentialsNameProvider.name(creds)); creds.setUsernameSecret(true); @@ -65,7 +67,7 @@ public class UsernamePasswordCredentialsImplTest { assertTrue(c.isUsernameSecret()); } public static final class SpecialUsernamePasswordCredentialsImpl extends UsernamePasswordCredentialsImpl { - public SpecialUsernamePasswordCredentialsImpl(CredentialsScope scope, String id, String description, String username, String password) { + public SpecialUsernamePasswordCredentialsImpl(CredentialsScope scope, String id, String description, String username, String password) throws Descriptor.FormException { super(scope, id, description, username, password); } transient boolean initialized;