Note that (unsandboxed) script execution can easily result in logging configuration being changed, so if you rely
+ * on complete logging of scripting actions, make sure to set up logging to remote systems.
+ *
+ * @see jenkins.model.Jenkins#_doScript(StaplerRequest, org.kohsuke.stapler.StaplerResponse, javax.servlet.RequestDispatcher, hudson.remoting.VirtualChannel, hudson.security.ACL)
+ * @see hudson.cli.GroovyCommand
+ * @see hudson.cli.GroovyshCommand
+ * @see jenkins.util.groovy.GroovyHookScript
+ *
+ * @since TODO
+ */
+public interface ScriptListener extends ExtensionPoint {
+
+ /**
+ * Called just before scripts are executed.
+ *
+ * Examples include:
+ *
-
Jenkins reports the following general usage statistics:
-
- - Your Jenkins version
- - For your controller and each agent, the OS type and number of executors
- - Installed plugins and their versions
- - Number of items (like jobs) of each item type
-
-
- This does not report any personally identifiable information. The only information reported by Jenkins is information inherently revealed by the HTTP protocol, such as the IP address.
-
- These usage statistics are aggregated, updated monthly, and published to stats.jenkins.io
-
-
+
- In addition to the general usage statistics listed above, the Jenkins project collects telemetry data from specific trials to inform future development.
+
+
+ The Jenkins project collects telemetry data from specific trials to inform future development.
+
+
+ In addition to the general usage statistics listed above, the Jenkins project collects telemetry data from specific trials to inform future development.
+
+
Each trial has a specific purpose and a defined end date, after which collection stops, independent of the installed versions of Jenkins or plugins.
Once a trial is complete, the trial results may be aggregated and shared with the developer community.
diff --git a/core/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java b/core/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java
new file mode 100644
index 000000000000..9d7e2b7b5980
--- /dev/null
+++ b/core/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java
@@ -0,0 +1,146 @@
+package hudson.security;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+import hudson.security.HudsonPrivateSecurityRealm.JBCryptEncoder;
+import hudson.security.HudsonPrivateSecurityRealm.PBKDF2PasswordEncoder;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.time.Duration;
+import javax.crypto.SecretKeyFactory;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class HudsonPrivateSecurityRealmTest {
+
+ // MySecurePassword
+ private static final String PBKDF2_HMAC_SHA512_ENCODED_PASSWORD =
+ "$HMACSHA512:210000:30f9e0a5470a8bc67f128ca1aae25dd4$88abaca4f442caeff0096ec0f75df2d77cc31a956c564133232f4d2532a72c8d4380a718d5b2a3dccab9e752027eeadd8f9f2c0c624505531bf3a57ec7d08aad";
+
+ /*
+ * This exists so that we can easily check the complexity of how long this takes (ie is the number of iterations we
+ * use correct for the state of CPUs).
+ * We do not want to assert that the range < x and > y as that would make the test flaky on overloaded
+ * or slow hardware, so this is commented out but left for ease of running locally when desired.
+ */
+ //@Test
+ public void timingPBKDF2() {
+ // ignore the salt generation - check just matching....
+ PBKDF2PasswordEncoder encoder = new PBKDF2PasswordEncoder();
+ String encoded = encoder.encode("thisIsMyPassword1");
+
+ long start = System.nanoTime();
+ for (int i = 0; i < 10; i++) {
+ System.out.println(encoder.matches("thisIsMyPassword" + i, encoded));
+ }
+ long end = System.nanoTime();
+ long duration = end - start;
+ long duration_per_iteration = duration / 10;
+
+ Duration d = Duration.ofNanos(duration_per_iteration);
+ System.out.println("PBKDF2 took " + d.toNanos() + "ns");
+ System.out.println("PBKDF2 took " + d.toMillis() + "ms");
+ System.out.println("PBKDF2 took " + d.toSeconds() + "s");
+ }
+
+ /*
+ * This exists so that we can easily check the complexity of how long this takes (ie is the number of iterations we
+ * use correct for the state of CPUs).
+ * We do not want to assert that the range < x and > y as that would make the test flaky on overloaded
+ * or slow hardware, so this is commented out but left for ease of running locally when desired.
+ */
+ //@Test
+ public void timingBcrypt() {
+ // ignore the salt generation - check just matching....
+ JBCryptEncoder encoder = new JBCryptEncoder();
+ String encoded = encoder.encode("thisIsMyPassword1");
+
+ long start = System.nanoTime();
+ for (int i = 0; i < 10; i++) {
+ System.out.println(encoder.matches("thisIsMyPassword" + i, encoded));
+ }
+ long end = System.nanoTime();
+ long duration = end - start;
+ long duration_per_iteration = duration / 10;
+
+ Duration d = Duration.ofNanos(duration_per_iteration);
+ System.out.println("BCrypt took " + d.toNanos() + "ns");
+ System.out.println("BCrypt took " + d.toMillis() + "ms");
+ System.out.println("BCrypt took " + d.toSeconds() + "s");
+ }
+
+ @Test
+ public void testPBKDF2RegExp() {
+ PBKDF2PasswordEncoder encoder = new PBKDF2PasswordEncoder();
+ String encoded = encoder.encode("thisIsMyPassword");
+ assertTrue(encoder.isHashValid(encoded));
+
+ // and a static one for other tests...
+ assertTrue(encoder.isHashValid(PBKDF2_HMAC_SHA512_ENCODED_PASSWORD));
+
+ assertFalse(encoder.isHashValid(
+ "$HMACSHA512:1000:fe899fcfcef4302ec3f0d36164efefdc$0781364eae9dac4ef1c4c3bf34c28e13965b46105fec0b6fcf4bae78e246fb5e51a1694fff19acac2dfb37b16055092644f3682c25beea9a7a286bf94e52f63b"),
+ "not enough iterations");
+ assertFalse(encoder.isHashValid(
+ "$HMACSHA512:500000:fe899fcfcef4302ec3f0d36164efefdc$0781364eae9dac4ef1c4c3bf34c28e13965b46105fec0b6fcf4bae78e246fb5e51a1694fff19acac2dfb37b16055092644f3682c25beea9a7a286bf94e52f63b"),
+ "too many iterations");
+ assertFalse(encoder.isHashValid(
+ "$HMACSHA512:210000:fe899fcfcef4302ec3f0d36164efef$0781364eae9dac4ef1c4c3bf34c28e13965b46105fec0b6fcf4bae78e246fb5e51a1694fff19acac2dfb37b16055092644f3682c25beea9a7a286bf94e52f63b"),
+ "salt too short");
+ assertFalse(encoder.isHashValid(
+ "$HMACSHA512:210000:f6865c02cc759fd061db0f3121a093e094$0781364eae9dac4ef1c4c3bf34c28e13965b46105fec0b6fcf4bae78e246fb5e51a1694fff19acac2dfb37b16055092644f3682c25beea9a7a286bf94e52f63b"),
+ "salt too long");
+ assertFalse(encoder.isHashValid(
+ "$HMACSHA512:210000:f6865c02cc759fd061db0f3121a093e094$079bd3a0c2851248343584a9a4625360e9ebb13c36be49542268d2ebdbd1fb71f004db9ce7335a61885985e32e08cb20215f"),
+ "hash result too short");
+ assertFalse(encoder.isHashValid(
+ "$HMACSHA512:210000:f6865c02cc759fd061db0f3121a093e094$0781364eae9dac4ef1c4c3bf34c28e13965b46105fec0b6fcf4bae78e246fb5e51a1694fff19acac2dfb37b16055092644f3682c25beea9a7a286bf94e52f63b42662"),
+ "hash result too long");
+ assertFalse(encoder.isHashValid(
+ "$HMACSHA256:210000:f6865c02cc759fd061db0f3121a093e094$0781364eae9dac4ef1c4c3bf34c28e13965b46105fec0b6fcf4bae78e246fb5e51a1694fff19acac2dfb37b16055092644f3682c25beea9a7a286bf94e52f63b42662"),
+ "wrong format");
+ assertFalse(encoder.isHashValid(
+ "::$sfdfssdf"),
+ "wrong format");
+
+ }
+
+ @Test
+ public void testPBKDF2PasswordMatching() {
+ PBKDF2PasswordEncoder encoder = new PBKDF2PasswordEncoder();
+ String encoded = encoder.encode("thisIsMyPassword");
+ assertTrue(encoder.matches("thisIsMyPassword", encoded));
+ assertFalse(encoder.matches("thisIsNotMyPassword", encoded));
+ }
+
+ @Test
+ public void passwordPBKDF2WithMissingAgorithm() throws Exception {
+ HudsonPrivateSecurityRealm.PBKDF2PasswordEncoder pbkdf2PasswordEncoder = new HudsonPrivateSecurityRealm.PBKDF2PasswordEncoder();
+ try (var ignored = mockStatic(SecretKeyFactory.class)) {
+ when(SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")).thenThrow(NoSuchAlgorithmException.class);
+ assertThrows(RuntimeException.class, () -> pbkdf2PasswordEncoder.encode("password"));
+
+ assertTrue(pbkdf2PasswordEncoder.isHashValid(PBKDF2_HMAC_SHA512_ENCODED_PASSWORD));
+ assertThrows(RuntimeException.class, () -> pbkdf2PasswordEncoder.matches("MySecurePassword", PBKDF2_HMAC_SHA512_ENCODED_PASSWORD));
+ }
+ }
+
+ @Test
+ public void passwordPBKDF2HashWithInvalidKeySpec() throws Exception {
+ HudsonPrivateSecurityRealm.PBKDF2PasswordEncoder pbkdf2PasswordEncoder = new HudsonPrivateSecurityRealm.PBKDF2PasswordEncoder();
+ try (var ignored = mockStatic(SecretKeyFactory.class)) {
+ SecretKeyFactory skf = mock(SecretKeyFactory.class);
+ when(SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")).thenReturn(skf);
+ when(skf.generateSecret(Mockito.any())).thenThrow(InvalidKeySpecException.class);
+ assertThrows(RuntimeException.class, () -> pbkdf2PasswordEncoder.encode("password"));
+
+ assertTrue(pbkdf2PasswordEncoder.isHashValid(PBKDF2_HMAC_SHA512_ENCODED_PASSWORD));
+ assertThrows(RuntimeException.class, () -> pbkdf2PasswordEncoder.matches("MySecurePassword", PBKDF2_HMAC_SHA512_ENCODED_PASSWORD));
+ }
+ }
+}
diff --git a/test/src/test/java/hudson/PluginManagerSecurity3072Test.java b/test/src/test/java/hudson/PluginManagerSecurity3072Test.java
deleted file mode 100644
index 774d6b5ce1c6..000000000000
--- a/test/src/test/java/hudson/PluginManagerSecurity3072Test.java
+++ /dev/null
@@ -1,107 +0,0 @@
-package hudson;
-
-import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
-import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
-import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
-import static org.awaitility.Awaitility.await;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeFalse;
-
-import hudson.model.RootAction;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.EnumSet;
-import java.util.HashSet;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import javax.servlet.ServletException;
-import jenkins.model.Jenkins;
-import org.htmlunit.html.HtmlForm;
-import org.htmlunit.html.HtmlPage;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.jvnet.hudson.test.Issue;
-import org.jvnet.hudson.test.JenkinsRule;
-import org.jvnet.hudson.test.TestExtension;
-import org.kohsuke.stapler.StaplerRequest;
-import org.kohsuke.stapler.StaplerResponse;
-
-public class PluginManagerSecurity3072Test {
-
- @Rule
- public JenkinsRule r = PluginManagerUtil.newJenkinsRule();
-
- @Rule
- public TemporaryFolder tmp = new TemporaryFolder();
-
- @Test
- @Issue("SECURITY-3072")
- public void verifyUploadedPluginFromURLPermission() throws Exception {
- assumeFalse(Functions.isWindows());
-
- HtmlPage page = r.createWebClient().goTo("pluginManager/advanced");
- HtmlForm f = page.getFormByName("uploadPlugin");
- f.getInputByName("pluginUrl").setValue(Jenkins.get().getRootUrl() + "pluginManagerGetPlugin/htmlpublisher.jpi");
- r.submit(f);
-
- File filesRef = Files.createTempFile("tmp", ".tmp").toFile();
- File filesTmpDir = filesRef.getParentFile();
- filesRef.deleteOnExit();
-
- final Set
[] filesPermission = new Set[]{new HashSet<>()};
- await().pollInterval(250, TimeUnit.MILLISECONDS)
- .atMost(10, TimeUnit.SECONDS)
- .until(() -> {
- Optional lastUploadedPluginDir = Arrays.stream(Objects.requireNonNull(
- filesTmpDir.listFiles((file, fileName) ->
- fileName.startsWith("uploadDir")))).
- max(Comparator.comparingLong(File::lastModified));
- if (lastUploadedPluginDir.isPresent()) {
- filesPermission[0] = Files.getPosixFilePermissions(lastUploadedPluginDir.get().toPath(), LinkOption.NOFOLLOW_LINKS);
- Optional pluginFile = Arrays.stream(Objects.requireNonNull(
- lastUploadedPluginDir.get().listFiles((file, fileName) ->
- fileName.startsWith("uploaded")))).
- max(Comparator.comparingLong(File::lastModified));
- assertTrue(pluginFile.isPresent());
- return true;
- } else {
- return false;
- }
- });
- assertEquals(EnumSet.of(OWNER_EXECUTE, OWNER_READ, OWNER_WRITE), filesPermission[0]);
- }
-
- @TestExtension("verifyUploadedPluginFromURLPermission")
- public static final class ReturnPluginJpiAction implements RootAction {
-
- @Override
- public String getIconFileName() {
- return "gear2.png";
- }
-
- @Override
- public String getDisplayName() {
- return "URL to retrieve a plugin jpi";
- }
-
- @Override
- public String getUrlName() {
- return "pluginManagerGetPlugin";
- }
-
- public void doDynamic(StaplerRequest staplerRequest, StaplerResponse staplerResponse) throws ServletException, IOException {
- staplerResponse.setContentType("application/octet-stream");
- staplerResponse.setStatus(200);
- staplerResponse.serveFile(staplerRequest, PluginManagerTest.class.getClassLoader().getResource("plugins/htmlpublisher.jpi"));
- }
- }
-}
diff --git a/test/src/test/java/hudson/PluginManagerTest.java b/test/src/test/java/hudson/PluginManagerTest.java
index 507354b8e1b8..dddd9239d97a 100644
--- a/test/src/test/java/hudson/PluginManagerTest.java
+++ b/test/src/test/java/hudson/PluginManagerTest.java
@@ -804,6 +804,43 @@ public void noInjectionOnAvailablePluginsPage() throws Exception {
}
}
+ @Test
+ @Issue("SECURITY-3072")
+ public void verifyUploadedPluginFromURLPermission() throws Exception {
+ assumeFalse(Functions.isWindows());
+
+ HtmlPage page = r.createWebClient().goTo("pluginManager/advanced");
+ HtmlForm f = page.getFormByName("uploadPlugin");
+ f.getInputByName("pluginUrl").setValue(Jenkins.get().getRootUrl() + "pluginManagerGetPlugin/htmlpublisher.jpi");
+ r.submit(f);
+
+ File filesRef = Files.createTempFile("tmp", ".tmp").toFile();
+ File filesTmpDir = filesRef.getParentFile();
+ filesRef.deleteOnExit();
+
+ final Set[] filesPermission = new Set[]{new HashSet<>()};
+ await().pollInterval(250, TimeUnit.MILLISECONDS)
+ .atMost(10, TimeUnit.SECONDS)
+ .until(() -> {
+ Optional lastUploadedPluginDir = Arrays.stream(Objects.requireNonNull(
+ filesTmpDir.listFiles((file, fileName) ->
+ fileName.startsWith("uploadDir")))).
+ max(Comparator.comparingLong(File::lastModified));
+ if (lastUploadedPluginDir.isPresent()) {
+ filesPermission[0] = Files.getPosixFilePermissions(lastUploadedPluginDir.get().toPath(), LinkOption.NOFOLLOW_LINKS);
+ Optional pluginFile = Arrays.stream(Objects.requireNonNull(
+ lastUploadedPluginDir.get().listFiles((file, fileName) ->
+ fileName.startsWith("uploaded")))).
+ max(Comparator.comparingLong(File::lastModified));
+ assertTrue(pluginFile.isPresent());
+ return true;
+ } else {
+ return false;
+ }
+ });
+ assertEquals(EnumSet.of(OWNER_EXECUTE, OWNER_READ, OWNER_WRITE), filesPermission[0]);
+ }
+
static class AlertHandlerImpl implements AlertHandler {
List messages = Collections.synchronizedList(new ArrayList<>());
@@ -838,4 +875,29 @@ public void doDynamic(StaplerRequest staplerRequest, StaplerResponse staplerResp
}
}
+ @TestExtension("verifyUploadedPluginFromURLPermission")
+ public static final class Security3072JpiAction implements RootAction {
+
+ @Override
+ public String getIconFileName() {
+ return "gear2.png";
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "URL to retrieve a plugin jpi";
+ }
+
+ @Override
+ public String getUrlName() {
+ return "pluginManagerGetPlugin";
+ }
+
+ public void doDynamic(StaplerRequest staplerRequest, StaplerResponse staplerResponse) throws ServletException, IOException {
+ staplerResponse.setContentType("application/octet-stream");
+ staplerResponse.setStatus(200);
+ staplerResponse.serveFile(staplerRequest, PluginManagerTest.class.getClassLoader().getResource("plugins/htmlpublisher.jpi"));
+ }
+ }
+
}
diff --git a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmFIPSTest.java b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmFIPSTest.java
new file mode 100644
index 000000000000..155e9cab1adc
--- /dev/null
+++ b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmFIPSTest.java
@@ -0,0 +1,133 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2015, CloudBees, Inc. and others
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package hudson.security;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.Assert.assertThrows;
+
+import hudson.model.User;
+import hudson.security.HudsonPrivateSecurityRealm.Details;
+import java.util.logging.Level;
+import org.hamcrest.Matcher;
+import org.htmlunit.FailingHttpStatusCodeException;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.jvnet.hudson.test.FlagRule;
+import org.jvnet.hudson.test.For;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.JenkinsRule.WebClient;
+import org.jvnet.hudson.test.LoggerRule;
+
+
+@For(HudsonPrivateSecurityRealm.class)
+public class HudsonPrivateSecurityRealmFIPSTest {
+
+ // the jbcrypt encoded for of "a" without the quotes
+ private static final String JBCRYPT_ENCODED_PASSWORD = "#jbcrypt:$2a$06$m0CrhHm10qJ3lXRY.5zDGO3rS2KdeeWLuGmsfGlMfOxih58VYVfxe";
+
+ @ClassRule
+ // do not use the FIPS140 class here as that initializes the field before we set the property!
+ public static TestRule flagRule = FlagRule.systemProperty("jenkins.security.FIPS140.COMPLIANCE", "true");
+
+ @Rule
+ public LoggerRule lr = new LoggerRule().record(HudsonPrivateSecurityRealm.class, Level.WARNING).capture(5);
+
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ @Test
+ public void generalLogin() throws Exception {
+ HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null);
+ j.jenkins.setSecurityRealm(securityRealm);
+
+ User u1 = securityRealm.createAccount("user", "password");
+ u1.setFullName("A User");
+ u1.save();
+
+ // we should be using PBKDF2 hasher
+ String hashedPassword = u1.getProperty(Details.class).getPassword();
+ assertThat(hashedPassword, startsWith("$PBKDF2$HMACSHA512:210000:"));
+
+ WebClient wc = j.createWebClient();
+ wc.login("user", "password");
+
+ assertThrows(FailingHttpStatusCodeException.class, () -> wc.login("user", "wrongPass"));
+ }
+
+ @Test
+ public void userCreationWithHashedPasswords() throws Exception {
+ HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null);
+ j.jenkins.setSecurityRealm(securityRealm);
+ // "password" after it has gone through the KDF
+ securityRealm.createAccountWithHashedPassword("user_hashed",
+ "$PBKDF2$HMACSHA512:210000:ffbb207b847010af98cdd2b09c79392c$f67c3b985daf60db83a9088bc2439f7b77016d26c1439a9877c4f863c377272283ce346edda4578a5607ea620a4beb662d853b800f373297e6f596af797743a6");
+ WebClient wc = j.createWebClient();
+
+ // login should succeed
+ wc.login("user_hashed", "password");
+
+ assertThrows(FailingHttpStatusCodeException.class, () -> wc.login("user_hashed", "password2"));
+ assertThat(lr, not(hasIncorrectHashingLogEntry()));
+ }
+
+ @Test
+ public void userLoginAfterEnablingFIPS() throws Exception {
+ HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null);
+ j.jenkins.setSecurityRealm(securityRealm);
+
+ User u1 = securityRealm.createAccount("user", "a");
+ u1.setFullName("A User");
+ // overwrite the password property using an password created using an incorrect algorithm
+ u1.addProperty(Details.fromHashedPassword(JBCRYPT_ENCODED_PASSWORD));
+
+ u1.save();
+ assertThat(u1.getProperty(Details.class).getPassword(), is(JBCRYPT_ENCODED_PASSWORD));
+
+ try (WebClient wc = j.createWebClient()) {
+ assertThrows(FailingHttpStatusCodeException.class, () -> wc.login("user", "a"));
+ }
+ assertThat(lr, hasIncorrectHashingLogEntry());
+ }
+
+ @Test
+ public void userCreationWithJBCryptPasswords() throws Exception {
+ HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null);
+
+ IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class,
+ () -> securityRealm.createAccountWithHashedPassword("user_hashed_incorrect_algorithm", JBCRYPT_ENCODED_PASSWORD));
+ assertThat(illegalArgumentException.getMessage(),
+ is("The hashed password was hashed with an incorrect algorithm. Jenkins is expecting $PBKDF2"));
+ }
+
+ private static Matcher hasIncorrectHashingLogEntry() {
+ return LoggerRule.recorded(is(
+ "A password appears to be stored (or is attempting to be stored) that was created with a different hashing/encryption algorithm, check the FIPS-140 state of the system has not changed inadvertently"));
+ }
+}
diff --git a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java
index 25d1dd18cd01..4363a92e4468 100644
--- a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java
+++ b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java
@@ -41,6 +41,7 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.ExtensionList;
import hudson.model.User;
+import hudson.security.HudsonPrivateSecurityRealm.Details;
import hudson.security.pages.SignupPage;
import java.lang.reflect.Field;
import java.net.URI;
@@ -51,11 +52,14 @@
import java.util.Base64;
import java.util.Collections;
import java.util.List;
+import java.util.logging.Level;
import jenkins.security.ApiTokenProperty;
import jenkins.security.SecurityListener;
import jenkins.security.apitoken.ApiTokenPropertyConfiguration;
import jenkins.security.seed.UserSeedProperty;
import org.apache.commons.lang.StringUtils;
+import org.hamcrest.Matcher;
+import org.htmlunit.FailingHttpStatusCodeException;
import org.htmlunit.HttpMethod;
import org.htmlunit.WebRequest;
import org.htmlunit.html.HtmlForm;
@@ -71,15 +75,23 @@
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.JenkinsRule.WebClient;
+import org.jvnet.hudson.test.LoggerRule;
import org.jvnet.hudson.test.TestExtension;
import org.mindrot.jbcrypt.BCrypt;
@For({UserSeedProperty.class, HudsonPrivateSecurityRealm.class})
public class HudsonPrivateSecurityRealmTest {
+ // the PBKDF encoded form of "password" without the quotes
+ private static final String PBKDF_ENDOCED_PASSWORD =
+ "$PBKDF2$HMACSHA512:210000:ffbb207b847010af98cdd2b09c79392c$f67c3b985daf60db83a9088bc2439f7b77016d26c1439a9877c4f863c377272283ce346edda4578a5607ea620a4beb662d853b800f373297e6f596af797743a6";
+
@Rule
public JenkinsRule j = new JenkinsRule();
+ @Rule
+ public LoggerRule lr = new LoggerRule().record(HudsonPrivateSecurityRealm.class, Level.WARNING).capture(5);
+
private SpySecurityListenerImpl spySecurityListener;
@Before
@@ -512,6 +524,7 @@ public void createAccountSupportsHashedPasswords() throws Exception {
XmlPage w2 = (XmlPage) wc.goTo("whoAmI/api/xml", "application/xml");
assertThat(w2, hasXPath("//name", is("user_hashed")));
+ assertThat(lr, not(hasIncorrectHashingLogEntry()));
}
@Test
@@ -718,6 +731,40 @@ public void changingPassword_withSeedDisable_hasNoImpact() throws Exception {
}
}
+ @Test
+ public void userLoginAfterDisablingFIPS() throws Exception {
+ HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null);
+ j.jenkins.setSecurityRealm(securityRealm);
+
+ User u1 = securityRealm.createAccount("user", "password");
+ u1.setFullName("A User");
+ // overwrite the password property using an password created using an incorrect algorithm
+ u1.addProperty(Details.fromHashedPassword(PBKDF_ENDOCED_PASSWORD));
+
+ u1.save();
+ assertThat(u1.getProperty(Details.class).getPassword(), is(PBKDF_ENDOCED_PASSWORD));
+
+ try (WebClient wc = j.createWebClient()) {
+ assertThrows(FailingHttpStatusCodeException.class, () -> wc.login("user", "password"));
+ }
+ assertThat(lr, hasIncorrectHashingLogEntry());
+ }
+
+ @Test
+ public void userCreationWithPBKDFPasswords() throws Exception {
+ HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null);
+
+ IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class,
+ () -> securityRealm.createAccountWithHashedPassword("user_hashed_incorrect_algorithm", PBKDF_ENDOCED_PASSWORD));
+ assertThat(illegalArgumentException.getMessage(),
+ is("The hashed password was hashed with an incorrect algorithm. Jenkins is expecting #jbcrypt:"));
+ }
+
+ private static Matcher hasIncorrectHashingLogEntry() {
+ return LoggerRule.recorded(is(
+ "A password appears to be stored (or is attempting to be stored) that was created with a different hashing/encryption algorithm, check the FIPS-140 state of the system has not changed inadvertently"));
+ }
+
private User prepareRealmAndAlice() throws Exception {
j.jenkins.setDisableRememberMe(false);
HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null);
diff --git a/test/src/test/java/jenkins/model/JenkinsSecurity3073Test.java b/test/src/test/java/jenkins/model/JenkinsSecurity3073Test.java
deleted file mode 100644
index 96c3fb3c634a..000000000000
--- a/test/src/test/java/jenkins/model/JenkinsSecurity3073Test.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package jenkins.model;
-
-import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
-import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
-import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
-import static org.awaitility.Awaitility.await;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assume.assumeFalse;
-
-import hudson.Functions;
-import java.io.File;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.EnumSet;
-import java.util.HashSet;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import org.apache.commons.io.FileUtils;
-import org.htmlunit.html.HtmlForm;
-import org.htmlunit.html.HtmlPage;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.jvnet.hudson.test.Issue;
-import org.jvnet.hudson.test.JenkinsRule;
-
-public class JenkinsSecurity3073Test {
-
-
- @Rule
- public JenkinsRule j = new JenkinsRule();
-
- @Rule
- public TemporaryFolder tmp = new TemporaryFolder();
-
- @Test
- @Issue("SECURITY-3073")
- public void verifyUploadedFingerprintFilePermission() throws Exception {
- assumeFalse(Functions.isWindows());
-
- HtmlPage page = j.createWebClient().goTo("fingerprintCheck");
- // The form doesn't have a name, the page contain the search form and the one we're interested in
- HtmlForm form = page.getForms().get(1);
- File dir = tmp.newFolder();
- File plugin = new File(dir, "htmlpublisher.jpi");
- // We're using a plugin to have a file above DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD
- FileUtils.copyURLToFile(Objects.requireNonNull(getClass().getClassLoader().getResource("plugins/htmlpublisher.jpi")), plugin);
- form.getInputByName("name").setValueAttribute(plugin.getAbsolutePath());
- j.submit(form);
-
- File filesRef = Files.createTempFile("tmp", ".tmp").toFile();
- File filesTmpDir = filesRef.getParentFile();
- filesRef.deleteOnExit();
-
- final Set[] filesPermission = new Set[]{new HashSet<>()};
- await().pollInterval(250, TimeUnit.MILLISECONDS)
- .atMost(10, TimeUnit.SECONDS)
- .until(() -> {
- Optional lastUploadedPlugin = Arrays.stream(Objects.requireNonNull(
- filesTmpDir.listFiles((file, fileName) ->
- fileName.startsWith("jenkins-multipart-uploads")))).
- max(Comparator.comparingLong(File::lastModified));
- if (lastUploadedPlugin.isPresent()) {
- filesPermission[0] = Files.getPosixFilePermissions(lastUploadedPlugin.get().toPath(), LinkOption.NOFOLLOW_LINKS);
- return true;
- } else {
- return false;
- }
- });
- assertEquals(EnumSet.of(OWNER_EXECUTE, OWNER_READ, OWNER_WRITE), filesPermission[0]);
- }
-}
diff --git a/test/src/test/java/jenkins/model/JenkinsTest.java b/test/src/test/java/jenkins/model/JenkinsTest.java
index 5c0c9acedbb2..6750eabbd3f1 100644
--- a/test/src/test/java/jenkins/model/JenkinsTest.java
+++ b/test/src/test/java/jenkins/model/JenkinsTest.java
@@ -24,6 +24,10 @@
package jenkins.model;
+import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
+import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
+import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
+import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.containsString;
@@ -38,9 +42,11 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.ExtensionList;
+import hudson.Functions;
import hudson.XmlFile;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
@@ -67,27 +73,40 @@
import hudson.util.FormValidation;
import hudson.util.HttpResponses;
import hudson.util.VersionNumber;
+import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.Socket;
import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import jenkins.AgentProtocol;
+import org.apache.commons.io.FileUtils;
import org.htmlunit.FailingHttpStatusCodeException;
import org.htmlunit.HttpMethod;
import org.htmlunit.Page;
import org.htmlunit.TextPage;
import org.htmlunit.WebRequest;
import org.htmlunit.WebResponse;
+import org.htmlunit.html.HtmlForm;
import org.htmlunit.html.HtmlPage;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
+import org.junit.rules.TemporaryFolder;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.JenkinsRule.WebClient;
@@ -110,6 +129,46 @@ public class JenkinsTest {
@Rule public JenkinsRule j = new JenkinsRule();
+ @Rule
+ public TemporaryFolder tmp = new TemporaryFolder();
+
+ @Test
+ @Issue("SECURITY-3073")
+ public void verifyUploadedFingerprintFilePermission() throws Exception {
+ assumeFalse(Functions.isWindows());
+
+ HtmlPage page = j.createWebClient().goTo("fingerprintCheck");
+ // The form doesn't have a name, the page contain the search form and the one we're interested in
+ HtmlForm form = page.getForms().get(1);
+ File dir = tmp.newFolder();
+ File plugin = new File(dir, "htmlpublisher.jpi");
+ // We're using a plugin to have a file above DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD
+ FileUtils.copyURLToFile(Objects.requireNonNull(getClass().getClassLoader().getResource("plugins/htmlpublisher.jpi")), plugin);
+ form.getInputByName("name").setValueAttribute(plugin.getAbsolutePath());
+ j.submit(form);
+
+ File filesRef = Files.createTempFile("tmp", ".tmp").toFile();
+ File filesTmpDir = filesRef.getParentFile();
+ filesRef.deleteOnExit();
+
+ final Set[] filesPermission = new Set[]{new HashSet<>()};
+ await().pollInterval(250, TimeUnit.MILLISECONDS)
+ .atMost(10, TimeUnit.SECONDS)
+ .until(() -> {
+ Optional lastUploadedPlugin = Arrays.stream(Objects.requireNonNull(
+ filesTmpDir.listFiles((file, fileName) ->
+ fileName.startsWith("jenkins-multipart-uploads")))).
+ max(Comparator.comparingLong(File::lastModified));
+ if (lastUploadedPlugin.isPresent()) {
+ filesPermission[0] = Files.getPosixFilePermissions(lastUploadedPlugin.get().toPath(), LinkOption.NOFOLLOW_LINKS);
+ return true;
+ } else {
+ return false;
+ }
+ });
+ assertEquals(EnumSet.of(OWNER_EXECUTE, OWNER_READ, OWNER_WRITE), filesPermission[0]);
+ }
+
@Issue("SECURITY-406")
@Test
public void testUserCreationFromUrlForAdmins() throws Exception {
diff --git a/test/src/test/java/jenkins/model/ScriptListenerTest.java b/test/src/test/java/jenkins/model/ScriptListenerTest.java
new file mode 100644
index 000000000000..2b8fcee441be
--- /dev/null
+++ b/test/src/test/java/jenkins/model/ScriptListenerTest.java
@@ -0,0 +1,182 @@
+package jenkins.model;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasSize;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import groovy.lang.Binding;
+import hudson.ExtensionList;
+import hudson.cli.CLICommandInvoker;
+import hudson.cli.GroovyCommand;
+import hudson.cli.GroovyshCommand;
+import hudson.model.User;
+import hudson.util.RemotingDiagnostics;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.List;
+import java.util.logging.Level;
+import jenkins.util.DefaultScriptListener;
+import jenkins.util.ScriptListener;
+import org.htmlunit.HttpMethod;
+import org.htmlunit.WebRequest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.LoggerRule;
+import org.jvnet.hudson.test.TestExtension;
+
+public class ScriptListenerTest {
+
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ @Rule
+ public LoggerRule logging = new LoggerRule();
+
+ @Test
+ public void consoleUsageIsLogged() throws IOException {
+ final String output = "hello from script console";
+ final String script = "println '" + output + "'";
+
+ logging.record(DefaultScriptListener.class.getName(), Level.FINEST).capture(100);
+
+ try (JenkinsRule.WebClient wc = j.createWebClient()) {
+ final WebRequest request = new WebRequest(new URL(wc.getContextPath() + "scriptText?script=" + script), HttpMethod.POST);
+ wc.getPage(wc.addCrumb(request));
+ }
+
+ { // DefaultScriptListener
+ final List messages = logging.getMessages();
+ assertThat(messages, hasSize(2));
+
+ assertThat(messages.get(0), containsString("Execution of script: '" + script + "' with binding: '[:]' in feature: 'class hudson.util.RemotingDiagnostics' and context: 'hudson.remoting.LocalChannel@"));
+ assertThat(messages.get(0), containsString("' with correlation: '"));
+ assertThat(messages.get(0), containsString("' (no user)"));
+
+ assertThat(messages.get(1), containsString("Script output: 'hello from script console\n' in feature: 'class hudson.util.RemotingDiagnostics' and context: 'hudson.remoting.LocalChannel@"));
+ assertThat(messages.get(1), containsString("' with correlation: '"));
+ assertThat(messages.get(1), containsString("' (no user)"));
+ }
+
+ { // DummyScriptUsageListener
+ final DummyScriptUsageListener listener = ExtensionList.lookupSingleton(DummyScriptUsageListener.class);
+ String execution = listener.getExecutionString();
+
+ assertThat(execution, containsString(RemotingDiagnostics.class.getName()));
+ assertThat(execution, containsString(script));
+ assertThat(listener.getOutput(), containsString(output));
+ }
+ }
+
+ @Test
+ public void groovyCliUsageIsLogged() {
+ final String output = "hello from groovy CLI";
+ final String script = "println '" + output + "'";
+
+ logging.record(DefaultScriptListener.class.getName(), Level.FINEST).capture(100);
+
+ InputStream scriptStream = new ByteArrayInputStream(script.getBytes());
+ new CLICommandInvoker(j, "groovy").withArgs("=").withStdin(scriptStream).invoke();
+
+ { // DefaultScriptListener
+ final List messages = logging.getMessages();
+ assertThat(messages, hasSize(3));
+
+ assertThat(messages.get(0), containsString("Execution of script: '" + script + "' with binding: '["));
+ assertThat(messages.get(0), containsString("]' in feature: 'class hudson.cli.GroovyCommand' and context: 'null' with correlation: '"));
+ assertThat(messages.get(0), containsString("' (no user)"));
+
+ assertThat(messages.get(1), containsString("Script output: 'hello from groovy CLI' in feature: 'class hudson.cli.GroovyCommand' and context: 'null' with correlation: '"));
+ assertThat(messages.get(1), containsString("' (no user)"));
+
+ assertThat(messages.get(2), containsString("Script output: '\n' in feature: 'class hudson.cli.GroovyCommand' and context: 'null' with correlation: '"));
+ assertThat(messages.get(2), containsString("' (no user)"));
+ }
+
+ { // DummyScriptUsageListener
+ final DummyScriptUsageListener listener = ExtensionList.lookupSingleton(DummyScriptUsageListener.class);
+ String execution = listener.getExecutionString();
+
+ assertThat(execution, containsString(GroovyCommand.class.getName()));
+ assertThat(execution, containsString(script));
+ assertThat(listener.getOutput(), containsString(output));
+ }
+ }
+
+ @Test
+ public void groovyShCliUsageIsLogged() {
+ final String output = "hello from groovysh CLI";
+ final String script = "println '" + output + "'";
+
+ logging.record(DefaultScriptListener.class.getName(), Level.FINEST).capture(100);
+
+ InputStream scriptStream = new ByteArrayInputStream(script.getBytes());
+ new CLICommandInvoker(j, "groovysh").withStdin(scriptStream).invoke();
+
+ { // DefaultScriptListener
+ final List messages = logging.getMessages();
+ assertThat(messages, hasSize(9));
+
+ assertThat(messages.get(0), containsString("Execution of script: 'null' with binding: 'null' in feature: 'class hudson.cli.GroovyshCommand' and context: 'null' with correlation: '"));
+ assertThat(messages.get(0), containsString("' (no user)"));
+
+ // Only match short substrings to not have to deal with color escape codes in the output
+ assertThat(messages.get(1), containsString("Groovy Shell")); // Groovy Shell (2.4.21, JVM: 11.0.15)
+ assertThat(messages.get(2), containsString(":help")); // Type ':help' or ':h' for help.
+ assertThat(messages.get(3), containsString("Script output: '-------------------"));
+ assertThat(messages.get(4), containsString("000")); // groovy:000>
+
+ assertThat(messages.get(5), containsString("Execution of script: '" + script + "' with binding: '["));
+ assertThat(messages.get(5), containsString("]' in feature: 'class hudson.cli.GroovyshCommand' and context: 'null' with correlation: '"));
+ assertThat(messages.get(5), containsString("' (no user)"));
+
+ assertThat(messages.get(6), containsString("Script output: 'hello from groovysh CLI\n' in feature: 'class hudson.cli.GroovyshCommand' and context: 'null' with correlation: '"));
+ assertThat(messages.get(6), containsString("' (no user)"));
+
+ // Only match short substrings to not have to deal with color escape codes in the output
+ assertThat(messages.get(7), containsString("===>")); // ===> null
+ assertThat(messages.get(8), containsString("000")); // groovy:000>
+ }
+
+ { // DummyScriptUsageListener
+ final DummyScriptUsageListener listener = ExtensionList.lookupSingleton(DummyScriptUsageListener.class);
+ String execution = listener.getExecutionString();
+
+ assertThat(execution, containsString(GroovyshCommand.class.getName()));
+ assertThat(execution, containsString(script));
+ assertThat(listener.getOutput(), containsString(output));
+ }
+ }
+
+ @TestExtension
+ public static class DummyScriptUsageListener implements ScriptListener {
+ private final StringBuilder script = new StringBuilder();
+ private final StringBuilder output = new StringBuilder();
+
+ @Override
+ public void onScriptExecution(String script, Binding binding, @NonNull Object feature, Object context, @NonNull String correlationId, User u) {
+ String username = "null";
+ if (u != null) {
+ username = u.getFullName();
+ }
+ String expectedOutFormat = "Script: '%s' in '%s' with '%s' by '%s'";
+ this.script.append(String.format(expectedOutFormat, script, feature, context, correlationId, username)).append("\n");
+ }
+
+ @Override
+ public void onScriptOutput(String output, @NonNull Object feature, Object context, @NonNull String correlationId, User user) {
+ this.output.append(output);
+ }
+
+ String getExecutionString() {
+ return script.toString();
+ }
+
+ String getOutput() {
+ return output.toString();
+ }
+ }
+}
diff --git a/war/src/main/webapp/scripts/hudson-behavior.js b/war/src/main/webapp/scripts/hudson-behavior.js
index cd35a3cd6032..7284a14e2ba1 100644
--- a/war/src/main/webapp/scripts/hudson-behavior.js
+++ b/war/src/main/webapp/scripts/hudson-behavior.js
@@ -639,17 +639,7 @@ function updateValidationArea(validationArea, content) {
// Only change content if different, causes an unnecessary animation otherwise
if (validationArea.innerHTML !== content) {
validationArea.innerHTML = content;
- validationArea.style.height =
- validationArea.children[0].offsetHeight + "px";
-
- // Only include the notice in the validation-error-area, move all other elements out
- if (validationArea.children.length > 1) {
- Array.from(validationArea.children)
- .slice(1)
- .forEach((element) => {
- validationArea.after(element);
- });
- }
+ validationArea.style.height = "auto";
Behaviour.applySubtree(validationArea);
// For errors with additional details, apply the subtree to the expandable details pane