From 7dc9143ce20883b073c45385109c5b9bd73bf7ec Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Fri, 27 Oct 2017 00:43:47 +0200 Subject: [PATCH 01/85] [JENKINS-27026] - Update SSHD Module from 2.0 to 2.3 to pick the fix --- war/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/war/pom.xml b/war/pom.xml index 1c4625ec0f..c1fb6b7ada 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -134,7 +134,7 @@ THE SOFTWARE. org.jenkins-ci.modules sshd - 2.0 + 2.3 org.jenkins-ci.ui From bb1685141e8c5ff343a35ff8949ff8c63412e53b Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 6 Nov 2017 11:16:25 -0800 Subject: [PATCH 02/85] [maven-release-plugin] prepare release jenkins-2.73.3 --- cli/pom.xml | 2 +- core/pom.xml | 2 +- pom.xml | 4 ++-- test/pom.xml | 2 +- war/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 3c50776345..1daec1d3b8 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 2.73.3-SNAPSHOT + 2.73.3 cli diff --git a/core/pom.xml b/core/pom.xml index a20c7ff2ef..216e374357 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.73.3-SNAPSHOT + 2.73.3 jenkins-core diff --git a/pom.xml b/pom.xml index f04af10b32..d29d5db560 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.73.3-SNAPSHOT + 2.73.3 pom Jenkins main module @@ -58,7 +58,7 @@ THE SOFTWARE. scm:git:git://github.com/jenkinsci/jenkins.git scm:git:ssh://git@github.com/jenkinsci/jenkins.git https://github.com/jenkinsci/jenkins - HEAD + jenkins-2.73.3 diff --git a/test/pom.xml b/test/pom.xml index 6882fefe91..f3d7dc4f7e 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.73.3-SNAPSHOT + 2.73.3 test diff --git a/war/pom.xml b/war/pom.xml index 5c84682c70..a1377daa03 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.73.3-SNAPSHOT + 2.73.3 jenkins-war From 34b3a43d0969c7d48d66676b19a692647260c7a3 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 6 Nov 2017 11:16:25 -0800 Subject: [PATCH 03/85] [maven-release-plugin] prepare for next development iteration --- cli/pom.xml | 2 +- core/pom.xml | 2 +- pom.xml | 4 ++-- test/pom.xml | 2 +- war/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 1daec1d3b8..d5aa1608e7 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 2.73.3 + 2.73.4-SNAPSHOT cli diff --git a/core/pom.xml b/core/pom.xml index 216e374357..9a582a5af4 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.73.3 + 2.73.4-SNAPSHOT jenkins-core diff --git a/pom.xml b/pom.xml index d29d5db560..4b9d54ce7f 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.73.3 + 2.73.4-SNAPSHOT pom Jenkins main module @@ -58,7 +58,7 @@ THE SOFTWARE. scm:git:git://github.com/jenkinsci/jenkins.git scm:git:ssh://git@github.com/jenkinsci/jenkins.git https://github.com/jenkinsci/jenkins - jenkins-2.73.3 + HEAD diff --git a/test/pom.xml b/test/pom.xml index f3d7dc4f7e..bfde49ef1d 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.73.3 + 2.73.4-SNAPSHOT test diff --git a/war/pom.xml b/war/pom.xml index a1377daa03..e476822fe7 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.73.3 + 2.73.4-SNAPSHOT jenkins-war From d3bb74334b95cfe4a78823d8db987fdc4938355a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Fri, 10 Nov 2017 10:48:19 +0100 Subject: [PATCH 04/85] Towards 2.89.1 --- cli/pom.xml | 2 +- core/pom.xml | 2 +- pom.xml | 4 ++-- test/pom.xml | 2 +- war/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 2aa1057cff..c37054f397 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 2.89 + 2.89.1-SNAPSHOT cli diff --git a/core/pom.xml b/core/pom.xml index 1baacd133b..05c8d0244b 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.89 + 2.89.1-SNAPSHOT jenkins-core diff --git a/pom.xml b/pom.xml index 482259c7de..be96e24162 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.89 + 2.89.1-SNAPSHOT pom Jenkins main module @@ -58,7 +58,7 @@ THE SOFTWARE. scm:git:git://github.com/jenkinsci/jenkins.git scm:git:ssh://git@github.com/jenkinsci/jenkins.git https://github.com/jenkinsci/jenkins - jenkins-2.89 + HEAD diff --git a/test/pom.xml b/test/pom.xml index ef480ad414..da7682aef1 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.89 + 2.89.1-SNAPSHOT test diff --git a/war/pom.xml b/war/pom.xml index b5e99f8104..29df2b41ce 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.89 + 2.89.1-SNAPSHOT jenkins-war From 3a0a575ecf4f68ecb1e91a249c304d683e5c273c Mon Sep 17 00:00:00 2001 From: benru Date: Thu, 23 Oct 2014 20:27:19 +0100 Subject: [PATCH 05/85] JENKINS-25286: Export assigned labels for slaves and the label expression for jobs in JSON and XML APIs Adds @Export attribute to assignedLabels for Computer and AbstractProject Also adjusts visibility on Label "name" parameter so it appears by default --- core/src/main/java/hudson/model/AbstractProject.java | 1 + core/src/main/java/hudson/model/Computer.java | 6 ++++++ core/src/main/java/hudson/model/Label.java | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java index ffee6413d8..b6be75139f 100644 --- a/core/src/main/java/hudson/model/AbstractProject.java +++ b/core/src/main/java/hudson/model/AbstractProject.java @@ -407,6 +407,7 @@ public abstract class AbstractProject

,R extends A /** * Gets the textual representation of the assigned label as it was entered by the user. */ + @Exported(name="labelExpression") public String getAssignedLabelString() { if (canRoam || assignedNode==null) return null; try { diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index f950cce781..3a968f54a9 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -776,6 +776,12 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces return "computer/" + Util.rawEncode(getName()) + "/"; } + @Exported + public Set getAssignedLabels() { + Node node = getNode(); + return (node != null) ? node.getAssignedLabels() : Collections.EMPTY_SET; + } + /** * Returns projects that are tied on this node. */ diff --git a/core/src/main/java/hudson/model/Label.java b/core/src/main/java/hudson/model/Label.java index cf6fdbff8b..0c26057765 100644 --- a/core/src/main/java/hudson/model/Label.java +++ b/core/src/main/java/hudson/model/Label.java @@ -127,7 +127,7 @@ public abstract class Label extends Actionable implements Comparable

+ 清除字符串前后的清白字符。 +
From 2fa723b1ee4ba09d087877a7efc89245c6ca4c65 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Wed, 6 Dec 2017 11:10:45 -0800 Subject: [PATCH 13/85] [maven-release-plugin] prepare release jenkins-2.89.1 --- cli/pom.xml | 2 +- core/pom.xml | 2 +- pom.xml | 4 ++-- test/pom.xml | 2 +- war/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index c37054f397..149bd085fb 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 2.89.1-SNAPSHOT + 2.89.1 cli diff --git a/core/pom.xml b/core/pom.xml index 05c8d0244b..7810dd6d28 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.89.1-SNAPSHOT + 2.89.1 jenkins-core diff --git a/pom.xml b/pom.xml index 4beccc327a..da396fbf7b 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.89.1-SNAPSHOT + 2.89.1 pom Jenkins main module @@ -58,7 +58,7 @@ THE SOFTWARE. scm:git:git://github.com/jenkinsci/jenkins.git scm:git:ssh://git@github.com/jenkinsci/jenkins.git https://github.com/jenkinsci/jenkins - HEAD + jenkins-2.89.1 diff --git a/test/pom.xml b/test/pom.xml index da7682aef1..7f1e787369 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.89.1-SNAPSHOT + 2.89.1 test diff --git a/war/pom.xml b/war/pom.xml index 092fe2503a..ce2df93097 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.89.1-SNAPSHOT + 2.89.1 jenkins-war From 2a1406e4bf07f2fccdd59780c4ffaa1e161b629e Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Wed, 6 Dec 2017 11:10:45 -0800 Subject: [PATCH 14/85] [maven-release-plugin] prepare for next development iteration --- cli/pom.xml | 2 +- core/pom.xml | 2 +- pom.xml | 4 ++-- test/pom.xml | 2 +- war/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 149bd085fb..1fc896f74f 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 2.89.1 + 2.89.2-SNAPSHOT cli diff --git a/core/pom.xml b/core/pom.xml index 7810dd6d28..6278a3fead 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.89.1 + 2.89.2-SNAPSHOT jenkins-core diff --git a/pom.xml b/pom.xml index da396fbf7b..4e57abc3bf 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.89.1 + 2.89.2-SNAPSHOT pom Jenkins main module @@ -58,7 +58,7 @@ THE SOFTWARE. scm:git:git://github.com/jenkinsci/jenkins.git scm:git:ssh://git@github.com/jenkinsci/jenkins.git https://github.com/jenkinsci/jenkins - jenkins-2.89.1 + HEAD diff --git a/test/pom.xml b/test/pom.xml index 7f1e787369..839aed6dbd 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.89.1 + 2.89.2-SNAPSHOT test diff --git a/war/pom.xml b/war/pom.xml index ce2df93097..a7f48bb18a 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.89.1 + 2.89.2-SNAPSHOT jenkins-war From 79481041974776746f71c4e5e68304c4cb91c71e Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Thu, 7 Dec 2017 10:08:39 -0500 Subject: [PATCH 15/85] [JENKINS-20474] Optimize away ACL construction and AuthorizationStrategy calls when checking permissions for SYSTEM. --- core/src/main/java/hudson/security/ACL.java | 15 +++- .../hudson/security/AccessControlled.java | 3 + .../test/java/hudson/security/ACLTest.java | 75 +++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 test/src/test/java/hudson/security/ACLTest.java diff --git a/core/src/main/java/hudson/security/ACL.java b/core/src/main/java/hudson/security/ACL.java index a2c463d97c..0efe770050 100644 --- a/core/src/main/java/hudson/security/ACL.java +++ b/core/src/main/java/hudson/security/ACL.java @@ -64,6 +64,9 @@ public abstract class ACL { */ public final void checkPermission(@Nonnull Permission p) { Authentication a = Jenkins.getAuthentication(); + if (a == SYSTEM) { + return; + } if(!hasPermission(a,p)) throw new AccessDeniedException2(a,p); } @@ -75,7 +78,11 @@ public abstract class ACL { * if the user doesn't have the permission. */ public final boolean hasPermission(@Nonnull Permission p) { - return hasPermission(Jenkins.getAuthentication(),p); + Authentication a = Jenkins.getAuthentication(); + if (a == SYSTEM) { + return true; + } + return hasPermission(a, p); } /** @@ -101,6 +108,9 @@ public abstract class ACL { public final void checkCreatePermission(@Nonnull ItemGroup c, @Nonnull TopLevelItemDescriptor d) { Authentication a = Jenkins.getAuthentication(); + if (a == SYSTEM) { + return; + } if (!hasCreatePermission(a, c, d)) { throw new AccessDeniedException(Messages.AccessDeniedException2_MissingPermission(a.getName(), Item.CREATE.group.title+"/"+Item.CREATE.name + Item.CREATE + "/" + d.getDisplayName())); @@ -136,6 +146,9 @@ public abstract class ACL { public final void checkCreatePermission(@Nonnull ViewGroup c, @Nonnull ViewDescriptor d) { Authentication a = Jenkins.getAuthentication(); + if (a == SYSTEM) { + return; + } if (!hasCreatePermission(a, c, d)) { throw new AccessDeniedException(Messages.AccessDeniedException2_MissingPermission(a.getName(), View.CREATE.group.title + "/" + View.CREATE.name + View.CREATE + "/" + d.getDisplayName())); diff --git a/core/src/main/java/hudson/security/AccessControlled.java b/core/src/main/java/hudson/security/AccessControlled.java index 50f977c349..5a2b246a2b 100644 --- a/core/src/main/java/hudson/security/AccessControlled.java +++ b/core/src/main/java/hudson/security/AccessControlled.java @@ -59,6 +59,9 @@ public interface AccessControlled { * @since FIXME */ default boolean hasPermission(@Nonnull Authentication a, @Nonnull Permission permission) { + if (a == ACL.SYSTEM) { + return true; + } return getACL().hasPermission(a, permission); } diff --git a/test/src/test/java/hudson/security/ACLTest.java b/test/src/test/java/hudson/security/ACLTest.java new file mode 100644 index 0000000000..c97f01198b --- /dev/null +++ b/test/src/test/java/hudson/security/ACLTest.java @@ -0,0 +1,75 @@ +/* + * The MIT License + * + * Copyright 2017 CloudBees, Inc. + * + * 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 hudson.model.FreeStyleProject; +import hudson.model.Item; +import java.util.Collection; +import java.util.Collections; +import org.acegisecurity.Authentication; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +public class ACLTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + + @Issue("JENKINS-20474") + @Test + public void bypassStrategyOnSystem() throws Exception { + FreeStyleProject p = r.createFreeStyleProject(); + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + r.jenkins.setAuthorizationStrategy(new DoNotBotherMe()); + assertTrue(p.hasPermission(Item.CONFIGURE)); + assertTrue(p.hasPermission(ACL.SYSTEM, Item.CONFIGURE)); + p.checkPermission(Item.CONFIGURE); + p.checkAbortPermission(); + assertEquals(Collections.singletonList(p), r.jenkins.getAllItems()); + } + + private static class DoNotBotherMe extends AuthorizationStrategy { + + @Override + public ACL getRootACL() { + return new ACL() { + @Override + public boolean hasPermission(Authentication a, Permission permission) { + throw new AssertionError("should not have needed to check " + permission + " for " + a); + } + }; + } + + @Override + public Collection getGroups() { + return Collections.emptySet(); + } + + } + +} From bbec44313a378db102e1bcda163b9d9e002bd353 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 09:24:39 -0500 Subject: [PATCH 16/85] surefire.skipAfterFailureCount=100 --- test/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/pom.xml b/test/pom.xml index 2f87f51d5f..49ba42663a 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -320,6 +320,7 @@ THE SOFTWARE. true 4 + 100 From d3645f7dfe5acbef8c2a1e921e69b49f49f24581 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 10:44:58 -0500 Subject: [PATCH 17/85] Updating Powermock and Objenesis to avoid Mockito/Hamcrest library mismatches. --- cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java | 2 +- pom.xml | 9 +++++++-- test/pom.xml | 1 - 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java b/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java index 8697026508..d5636f3bd7 100644 --- a/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java +++ b/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java @@ -47,7 +47,7 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; @RunWith(PowerMockRunner.class) -@PrepareForTest(CLI.class) // When mocking new operator caller has to be @PreparedForTest, not class itself +@PrepareForTest({CLI.class, CLIConnectionFactory.class}) // When mocking new operator caller has to be @PreparedForTest, not class itself public class PrivateKeyProviderTest { @Test diff --git a/pom.xml b/pom.xml index c798586c2b..e5cf8ea367 100644 --- a/pom.xml +++ b/pom.xml @@ -153,12 +153,17 @@ THE SOFTWARE. org.powermock powermock-module-junit4 - 1.6.2 + 1.6.6 org.powermock powermock-api-mockito - 1.6.2 + 1.6.6 + + + org.objenesis + objenesis + 2.6 diff --git a/test/pom.xml b/test/pom.xml index 2f87f51d5f..e6a94a49c8 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -207,7 +207,6 @@ THE SOFTWARE. org.objenesis objenesis - 2.5.1 test From af95fef755b23c07f14e54eeb0ae86c91bb94299 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 12:22:23 -0500 Subject: [PATCH 18/85] Removing core/src/main/groovy/. --- core/pom.xml | 2 +- .../groovy/hudson/util/LoadMonitor.groovy | 116 ------------------ .../groovy/AbstractGroovyViewModule.groovy | 47 ------- .../util/groovy/AbstractGroovyViewModule.java | 48 ++++++++ pom.xml | 28 ----- test/pom.xml | 1 - 6 files changed, 49 insertions(+), 193 deletions(-) delete mode 100644 core/src/main/groovy/hudson/util/LoadMonitor.groovy delete mode 100644 core/src/main/groovy/jenkins/util/groovy/AbstractGroovyViewModule.groovy create mode 100644 core/src/main/java/jenkins/util/groovy/AbstractGroovyViewModule.java diff --git a/core/pom.xml b/core/pom.xml index a4b053eee5..5b8e70a1ee 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -799,10 +799,10 @@ THE SOFTWARE. org.codehaus.gmaven gmaven-plugin - + generateTestStubs testCompile diff --git a/core/src/main/groovy/hudson/util/LoadMonitor.groovy b/core/src/main/groovy/hudson/util/LoadMonitor.groovy deleted file mode 100644 index 97d7018161..0000000000 --- a/core/src/main/groovy/hudson/util/LoadMonitor.groovy +++ /dev/null @@ -1,116 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi - * - * 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.util - -import hudson.model.Computer -import jenkins.util.Timer -import jenkins.model.Jenkins -import hudson.model.Label -import hudson.model.Queue.BlockedItem -import hudson.model.Queue.BuildableItem -import hudson.model.Queue.WaitingItem -import hudson.triggers.SafeTimerTask -import java.text.DateFormat -import java.util.concurrent.TimeUnit - -/** - * Spits out the load information. - * - *

- * I'm using this code to design the auto scaling feature. - * In future this might be useful data to expose to the UI. - * - * @author Kohsuke Kawaguchi - */ -public class LoadMonitorImpl extends SafeTimerTask { - - private final File dataFile; - private List labels; - - public LoadMonitorImpl(File dataFile) { - this.dataFile = dataFile; - labels = Jenkins.getInstance().labels*.name; - printHeaders(); - Timer.get().scheduleAtFixedRate(this,0,10*1000, TimeUnit.MILLISECONDS); - } - - private String quote(Object s) { "\"${s}\""; } - - protected void printHeaders() { - def headers = ["# of executors","# of busy executors","BuildableItems in Q","BuildableItem avg wait time"]; - def data = ["timestamp"]; - data += headers; - data += ["WaitingItems in Q","BlockedItems in Q"]; - - for( String label : labels) - data += headers.collect { "${it} (${label}}" } - - dataFile.append(data.collect({ quote(it) }).join(",")+"\n"); - } - - @Override - protected void doRun() { - def now = new Date(); - def data = []; - data.add(quote(FORMATTER.format(now))); - - def h = Jenkins.getInstance(); - - def items = h.queue.items; - def filterByType = {Class type -> items.findAll { type.isInstance(it) } } - - def builder = {List cs, Closure itemFilter -> - // number of total executor, number of busy executor - data.add(cs.sum { it.isOffline() ? 0 : it.numExecutors }); - data.add(cs.sum {Computer c -> - c.executors.findAll { !it.isIdle() }.size() - }); - - // queue statistics - def is = filterByType(BuildableItem).findAll(itemFilter); - data.add(is.size()); - data.add(is.sum {BuildableItem bi -> now.time - bi.buildableStartMilliseconds }?:0 / Math.max(1,is.size()) ); - }; - - - // for the whole thing - builder(Arrays.asList(h.computers),{ it->true }); - - data.add(filterByType(WaitingItem).size()); - data.add(filterByType(BlockedItem).size()); - - // per label stats - for (String label : labels) { - Label l = h.getLabel(label) - builder(l.nodes.collect { it.toComputer() }) { BuildableItem bi -> bi.task.assignedLabel==l }; - } - - dataFile.append(data.join(",")+"\n"); - } - - private static final DateFormat FORMATTER = DateFormat.getDateTimeInstance(); -} - -new LoadMonitorImpl(new File("/files/hudson/load.txt")); - diff --git a/core/src/main/groovy/jenkins/util/groovy/AbstractGroovyViewModule.groovy b/core/src/main/groovy/jenkins/util/groovy/AbstractGroovyViewModule.groovy deleted file mode 100644 index 08cb1c23a3..0000000000 --- a/core/src/main/groovy/jenkins/util/groovy/AbstractGroovyViewModule.groovy +++ /dev/null @@ -1,47 +0,0 @@ -package jenkins.util.groovy - -import lib.FormTagLib -import lib.LayoutTagLib -import org.kohsuke.stapler.jelly.groovy.JellyBuilder -import org.kohsuke.stapler.jelly.groovy.Namespace -import lib.JenkinsTagLib - -/** - * Base class for utility classes for Groovy view scripts - *

- * Usage from script of a subclass, say ViewHelper: - *

- * new ViewHelper(delegate).method(); - *

- * see ModularizeViewScript in ui-samples for an example how to use this class. - * - * @author Stefan Wolf (wolfs) - */ -abstract class AbstractGroovyViewModule { - JellyBuilder builder - FormTagLib f - LayoutTagLib l - JenkinsTagLib t - Namespace st - - public AbstractGroovyViewModule(JellyBuilder b) { - builder = b - f= builder.namespace(FormTagLib) - l=builder.namespace(LayoutTagLib) - t=builder.namespace(JenkinsTagLib) - st=builder.namespace("jelly:stapler") - - } - - def methodMissing(String name, args) { - builder.invokeMethod(name,args) - } - - def propertyMissing(String name) { - builder.getProperty(name) - } - - def propertyMissing(String name, value) { - builder.setProperty(name, value) - } -} diff --git a/core/src/main/java/jenkins/util/groovy/AbstractGroovyViewModule.java b/core/src/main/java/jenkins/util/groovy/AbstractGroovyViewModule.java new file mode 100644 index 0000000000..9a18a67128 --- /dev/null +++ b/core/src/main/java/jenkins/util/groovy/AbstractGroovyViewModule.java @@ -0,0 +1,48 @@ +package jenkins.util.groovy; + +import groovy.lang.GroovyObjectSupport; +import lib.FormTagLib; +import lib.LayoutTagLib; +import org.kohsuke.stapler.jelly.groovy.JellyBuilder; +import org.kohsuke.stapler.jelly.groovy.Namespace; +import lib.JenkinsTagLib; + +/** + * Base class for utility classes for Groovy view scripts + *

+ * Usage from script of a subclass, say ViewHelper: + *

+ * new ViewHelper(delegate).method(); + *

+ * see ModularizeViewScript in ui-samples for an example how to use + * this class. + */ +public abstract class AbstractGroovyViewModule extends GroovyObjectSupport { + + public JellyBuilder builder; + public FormTagLib f; + public LayoutTagLib l; + public JenkinsTagLib t; + public Namespace st; + + public AbstractGroovyViewModule(JellyBuilder b) { + builder = b; + f = builder.namespace(FormTagLib.class); + l = builder.namespace(LayoutTagLib.class); + t = builder.namespace(JenkinsTagLib.class); + st = builder.namespace("jelly:stapler"); + } + + public Object methodMissing(String name, Object args) { + return builder.invokeMethod(name, args); + } + + public Object propertyMissing(String name) { + return builder.getProperty(name); + } + + public void propertyMissing(String name, Object value) { + builder.setProperty(name, value); + } + +} diff --git a/pom.xml b/pom.xml index c798586c2b..9e95d80f7a 100644 --- a/pom.xml +++ b/pom.xml @@ -722,34 +722,6 @@ THE SOFTWARE. org.jenkins-ci.tools maven-jenkins-dev-plugin - - - - org.codehaus.gmaven - gmaven-plugin - - - - generateStubs - compile - generateTestStubs - testCompile - - - - - - diff --git a/test/pom.xml b/test/pom.xml index 2f87f51d5f..afa1599871 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -244,7 +244,6 @@ THE SOFTWARE. org.codehaus.gmaven gmaven-plugin - default From 4af739f2f29dae01d32ebe59f45c0252576a18e6 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 12:35:40 -0500 Subject: [PATCH 19/85] Javadoc error. --- .../java/jenkins/util/groovy/AbstractGroovyViewModule.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/jenkins/util/groovy/AbstractGroovyViewModule.java b/core/src/main/java/jenkins/util/groovy/AbstractGroovyViewModule.java index 9a18a67128..ccf4720293 100644 --- a/core/src/main/java/jenkins/util/groovy/AbstractGroovyViewModule.java +++ b/core/src/main/java/jenkins/util/groovy/AbstractGroovyViewModule.java @@ -9,11 +9,11 @@ import lib.JenkinsTagLib; /** * Base class for utility classes for Groovy view scripts - *

+ *

* Usage from script of a subclass, say ViewHelper: - *

+ *

* new ViewHelper(delegate).method(); - *

+ *

* see ModularizeViewScript in ui-samples for an example how to use * this class. */ From dbdcaaf36be97fc77aabbc53a309edace51e9936 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 13:01:47 -0500 Subject: [PATCH 20/85] SecretRewriterTest --- .../java/hudson/util/HistoricalSecrets.java | 2 +- .../hudson/util/SecretRewriterTest.groovy | 120 ------------------ .../java/hudson/util/SecretRewriterTest.java | 116 +++++++++++++++++ 3 files changed, 117 insertions(+), 121 deletions(-) delete mode 100644 core/src/test/groovy/hudson/util/SecretRewriterTest.groovy create mode 100644 core/src/test/java/hudson/util/SecretRewriterTest.java diff --git a/core/src/main/java/hudson/util/HistoricalSecrets.java b/core/src/main/java/hudson/util/HistoricalSecrets.java index 37a6fa39e1..962ab26089 100644 --- a/core/src/main/java/hudson/util/HistoricalSecrets.java +++ b/core/src/main/java/hudson/util/HistoricalSecrets.java @@ -80,5 +80,5 @@ public class HistoricalSecrets { return Util.toAes128Key(secret); } - private static final String MAGIC = "::::MAGIC::::"; + static final String MAGIC = "::::MAGIC::::"; } diff --git a/core/src/test/groovy/hudson/util/SecretRewriterTest.groovy b/core/src/test/groovy/hudson/util/SecretRewriterTest.groovy deleted file mode 100644 index a429bef54e..0000000000 --- a/core/src/test/groovy/hudson/util/SecretRewriterTest.groovy +++ /dev/null @@ -1,120 +0,0 @@ -package hudson.util - -import com.trilead.ssh2.crypto.Base64 -import hudson.FilePath -import hudson.Functions -import jenkins.security.ConfidentialStoreRule -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TemporaryFolder - -import javax.crypto.Cipher - -import static org.junit.Assume.assumeFalse - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class SecretRewriterTest { - @Rule - public MockSecretRule mockSecretRule = new MockSecretRule() - - @Rule - public ConfidentialStoreRule confidentialStoreRule = new ConfidentialStoreRule(); - - @Rule public TemporaryFolder tmp = new TemporaryFolder() - - def FOO_PATTERN = /\{[A-Za-z0-9+\/]+={0,2}}<\/foo>/ - def MSG_PATTERN = /\{[A-Za-z0-9+\/]+={0,2}}<\/msg>/ - def FOO_PATTERN2 = /(\{[A-Za-z0-9+\/]+={0,2}}<\/foo>){2}/ - def ABC_FOO_PATTERN = /\s\{[A-Za-z0-9+\/]+={0,2}}<\/foo>\s<\/abc>/ - - @Test - void singleFileRewrite() { - def o = encryptOld('foobar') // old - def n = encryptNew('foobar') // new - roundtrip "${o}", - {assert it ==~ FOO_PATTERN} - - - roundtrip "${o}${o}", - {assert it ==~ FOO_PATTERN2} - - roundtrip "${n}", - {assert it == "${n}"} - - roundtrip " thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret ", - {assert it == "thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret"} - - // to be rewritten, it needs to be between a tag - roundtrip "$o", {assert it == "$o"} - roundtrip "$o", {assert it == "$o"} - - // - roundtrip "\n$o\n", {assert it ==~ ABC_FOO_PATTERN} - } - - void roundtrip(String before, Closure check) { - def sr = new SecretRewriter(null); - def f = File.createTempFile("test", "xml", tmp.root) - f.text = before - sr.rewrite(f,null) - check(f.text.replaceAll(System.getProperty("line.separator"), "\n").trim()) - //assert after.replaceAll(System.getProperty("line.separator"), "\n").trim()==f.text.replaceAll(System.getProperty("line.separator"), "\n").trim() - } - - String encryptOld(str) { - def cipher = Secret.getCipher("AES"); - cipher.init(Cipher.ENCRYPT_MODE, HistoricalSecrets.legacyKey); - return new String(Base64.encode(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes("UTF-8")))) - } - - String encryptNew(str) { - return Secret.fromString(str).encryptedValue - } - - /** - * Directory rewrite and recursion detection - */ - @Test - void recursionDetection() { - assumeFalse("Symlinks don't work on Windows very well", Functions.isWindows()) - def sw = new SecretRewriter(); - def st = StreamTaskListener.fromStdout() - - def o = encryptOld("Hello world") - def n = encryptNew("Hello world") - def payload = "$o" - def answer = "$n" - - // set up some directories with stuff - def t = tmp.newFolder("t") - def dirs = ["a", "b", "c", "c/d", "c/d/e"] - dirs.each { p -> - def d = new File(t, p) - d.mkdir() - new File(d,"foo.xml").text = payload - } - - // stuff outside - def t2 = tmp.newFolder("t2") - new File(t2,"foo.xml").text = payload - - // some recursions as well as valid symlinks - new FilePath(t).child("c/symlink").symlinkTo("..",st) - new FilePath(t).child("b/symlink").symlinkTo(".",st) - new FilePath(t).child("a/symlink").symlinkTo(t2.absolutePath,st) - - assert 6==sw.rewriteRecursive(t, st) - - dirs.each { p-> - assert new File(t,"$p/foo.xml").text.trim() ==~ MSG_PATTERN - } - - // t2 is only reachable by following a symlink. this should be covered, too - assert new File(t2,"foo.xml").text.trim() ==~ MSG_PATTERN - } - -} diff --git a/core/src/test/java/hudson/util/SecretRewriterTest.java b/core/src/test/java/hudson/util/SecretRewriterTest.java new file mode 100644 index 0000000000..5ca97ff060 --- /dev/null +++ b/core/src/test/java/hudson/util/SecretRewriterTest.java @@ -0,0 +1,116 @@ +package hudson.util; + +import com.trilead.ssh2.crypto.Base64; +import hudson.FilePath; +import hudson.Functions; +import hudson.model.TaskListener; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; +import javax.crypto.Cipher; +import jenkins.security.ConfidentialStoreRule; +import org.apache.commons.io.FileUtils; +import static org.junit.Assert.*; +import static org.junit.Assume.*; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class SecretRewriterTest { + + @Rule + public MockSecretRule mockSecretRule = new MockSecretRule(); + + @Rule + public ConfidentialStoreRule confidentialStoreRule = new ConfidentialStoreRule(); + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + private static final Pattern FOO_PATTERN = Pattern.compile("[{][A-Za-z0-9+/]+={0,2}[}]"); + private static final Pattern MSG_PATTERN = Pattern.compile("[{][A-Za-z0-9+/]+={0,2}[}]"); + private static final Pattern FOO_PATTERN2 = Pattern.compile("([{][A-Za-z0-9+/]+={0,2}[}]){2}"); + private static final Pattern ABC_FOO_PATTERN = Pattern.compile("\\s[{][A-Za-z0-9+/]+={0,2}[}]\\s"); + + @Test + public void singleFileRewrite() throws Exception { + String o = encryptOld("foobar"); // old + String n = encryptNew("foobar"); // new + assertTrue(FOO_PATTERN.matcher(roundtrip("" + o + "")).matches()); + assertTrue(FOO_PATTERN2.matcher(roundtrip("" + o + "" + o + "")).matches()); + assertEquals("" + n + "", roundtrip("" + n + "")); + assertEquals("thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret", roundtrip(" thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret ")); + // to be rewritten, it needs to be between a tag + assertEquals("" + o, roundtrip("" + o)); + assertEquals(o + "", roundtrip(o + "")); + assertTrue(ABC_FOO_PATTERN.matcher(roundtrip("\n" + o + "\n")).matches()); + } + + private String roundtrip(String before) throws Exception { + SecretRewriter sr = new SecretRewriter(null); + File f = File.createTempFile("test", "xml", tmp.getRoot()); + FileUtils.write(f, before); + sr.rewrite(f, null); + //assert after.replaceAll(System.getProperty("line.separator"), "\n").trim()==f.text.replaceAll(System.getProperty("line.separator"), "\n").trim() + return FileUtils.readFileToString(f).replaceAll(System.getProperty("line.separator"), "\n").trim(); + } + + private String encryptOld(String str) throws Exception { + Cipher cipher = Secret.getCipher("AES"); + cipher.init(Cipher.ENCRYPT_MODE, HistoricalSecrets.getLegacyKey()); + return new String(Base64.encode(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes("UTF-8")))); + } + + private String encryptNew(String str) { + return Secret.fromString(str).getEncryptedValue(); + } + + /** + * Directory rewrite and recursion detection + */ + @Test + public void recursionDetection() throws Exception { + assumeFalse("Symlinks don't work on Windows very well", Functions.isWindows()); + SecretRewriter sw = new SecretRewriter(); + TaskListener st = StreamTaskListener.fromStdout(); + + String o = encryptOld("Hello world"); + String n = encryptNew("Hello world"); + String payload = "" + o + ""; + String answer = "" + n + ""; + + // set up some directories with stuff + File t = tmp.newFolder("t"); + List dirs = Arrays.asList("a", "b", "c", "c/d", "c/d/e"); + for (String p : dirs) { + File d = new File(t, p); + d.mkdir(); + try { + FileUtils.write(new File(d, "foo.xml"), payload); + } catch (IOException x) { + assert false : x; + } + } + + // stuff outside + File t2 = tmp.newFolder("t2"); + FileUtils.write(new File(t2, "foo.xml"), payload); + + // some recursions as well as valid symlinks + new FilePath(t).child("c/symlink").symlinkTo("..", st); + new FilePath(t).child("b/symlink").symlinkTo(".", st); + new FilePath(t).child("a/symlink").symlinkTo(t2.getAbsolutePath(), st); + + assertEquals(6, sw.rewriteRecursive(t, st)); + + for (String p : dirs) { + assertTrue(MSG_PATTERN.matcher(FileUtils.readFileToString(new File(t, p + "/foo.xml")).trim()).matches()); + } + + // t2 is only reachable by following a symlink. this should be covered, too + assertTrue(MSG_PATTERN.matcher(FileUtils.readFileToString(new File(t2, "foo.xml")).trim()).matches()); + } + +} From 5b0cca77050d91dbe8d117aeffe8426e3bef858a Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 13:19:33 -0500 Subject: [PATCH 21/85] SecretTest --- .../hudson/util/SecretTest.java} | 94 ++++++++++--------- 1 file changed, 48 insertions(+), 46 deletions(-) rename core/src/test/{groovy/hudson/util/SecretTest.groovy => java/hudson/util/SecretTest.java} (55%) diff --git a/core/src/test/groovy/hudson/util/SecretTest.groovy b/core/src/test/java/hudson/util/SecretTest.java similarity index 55% rename from core/src/test/groovy/hudson/util/SecretTest.groovy rename to core/src/test/java/hudson/util/SecretTest.java index 8f39ae7ec0..0b9c13b9b6 100644 --- a/core/src/test/groovy/hudson/util/SecretTest.groovy +++ b/core/src/test/java/hudson/util/SecretTest.java @@ -21,51 +21,51 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package hudson.util + +package hudson.util; import com.trilead.ssh2.crypto.Base64; -import jenkins.model.Jenkins +import java.util.Random; +import java.util.regex.Pattern; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import jenkins.model.Jenkins; import jenkins.security.ConfidentialStoreRule; import org.apache.commons.lang.RandomStringUtils; -import org.junit.Rule -import org.junit.Test +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; -import java.util.Random; -import javax.crypto.Cipher -import java.util.regex.Pattern; - -/** - * @author Kohsuke Kawaguchi - */ public class SecretTest { - @Rule - public ConfidentialStoreRule confidentialStore = new ConfidentialStoreRule() @Rule - public MockSecretRule mockSecretRule = new MockSecretRule() + public ConfidentialStoreRule confidentialStore = new ConfidentialStoreRule(); - static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("\\{?[A-Za-z0-9+/]+={0,2}}?"); + @Rule + public MockSecretRule mockSecretRule = new MockSecretRule(); + + private static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("\\{?[A-Za-z0-9+/]+={0,2}}?"); @Test - void testEncrypt() { - def secret = Secret.fromString("abc"); - assert "abc"==secret.plainText; + public void encrypt() { + Secret secret = Secret.fromString("abc"); + assertEquals("abc", secret.getPlainText()); // make sure we got some encryption going - println secret.encryptedValue; - assert !"abc".equals(secret.encryptedValue); + assertNotEquals("abc", secret.getEncryptedValue()); // can we round trip? - assert secret==Secret.fromString(secret.encryptedValue); + assertEquals(secret, Secret.fromString(secret.getEncryptedValue())); //Two consecutive encryption requests of the same object should result in the same encrypted value - SECURITY-304 - assert secret.encryptedValue == secret.encryptedValue + assertEquals(secret.getEncryptedValue(), secret.getEncryptedValue()); //Two consecutive encryption requests of different objects with the same value should not result in the same encrypted value - SECURITY-304 - assert secret.encryptedValue != Secret.fromString(secret.plainText).encryptedValue + assertNotEquals(secret.getEncryptedValue(), Secret.fromString(secret.getPlainText()).getEncryptedValue()); } @Test - void testEncryptedValuePattern() { + public void encryptedValuePattern() { for (int i = 1; i < 100; i++) { String plaintext = RandomStringUtils.random(new Random().nextInt(i)); String ciphertext = Secret.fromString(plaintext).getEncryptedValue(); @@ -83,19 +83,20 @@ public class SecretTest { } @Test - void testDecrypt() { - assert "abc"==Secret.toString(Secret.fromString("abc")) + public void decrypt() { + assertEquals("abc", Secret.toString(Secret.fromString("abc"))); } @Test - void testSerialization() { - def s = Secret.fromString("Mr.Jenkins"); - def xml = Jenkins.XSTREAM.toXML(s); - assert !xml.contains(s.plainText) - assert xml ==~ /\{[A-Za-z0-9+\/]+={0,2}}<\/hudson\.util\.Secret>/ + public void serialization() { + Secret s = Secret.fromString("Mr.Jenkins"); + String xml = Jenkins.XSTREAM.toXML(s); + assertThat(xml, not(containsString(s.getPlainText()))); + // TODO MatchesPattern not available until Hamcrest 2.0 + assertTrue(xml, xml.matches("[{][A-Za-z0-9+/]+={0,2}[}]")); - def o = Jenkins.XSTREAM.fromXML(xml); - assert o==s : xml; + Object o = Jenkins.XSTREAM.fromXML(xml); + assertEquals(xml, s, o); } public static class Foo { @@ -106,27 +107,28 @@ public class SecretTest { * Makes sure the serialization form is backward compatible with String. */ @Test - void testCompatibilityFromString() { - def tagName = Foo.class.name.replace("\$","_-"); - def xml = "<$tagName>secret"; - def foo = new Foo(); + public void testCompatibilityFromString() { + String tagName = Foo.class.getName().replace("$", "_-"); + String xml = "<" + tagName + ">secret"; + Foo foo = new Foo(); Jenkins.XSTREAM.fromXML(xml, foo); - assert "secret"==Secret.toString(foo.password) + assertEquals("secret", Secret.toString(foo.password)); } /** * Secret persisted with Jenkins.getSecretKey() should still decrypt OK. */ @Test - void migrationFromLegacyKeyToConfidentialStore() { - def legacy = HistoricalSecrets.legacyKey - ["Hello world","","\u0000unprintable"].each { str -> - def cipher = Secret.getCipher("AES"); + public void migrationFromLegacyKeyToConfidentialStore() throws Exception { + SecretKey legacy = HistoricalSecrets.getLegacyKey(); + for (String str : new String[] {"Hello world", "", "\u0000unprintable"}) { + Cipher cipher = Secret.getCipher("AES"); cipher.init(Cipher.ENCRYPT_MODE, legacy); - def old = new String(Base64.encode(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes("UTF-8")))) - def s = Secret.fromString(old) - assert s.plainText==str : "secret by the old key should decrypt" - assert s.encryptedValue!=old : "but when encrypting, ConfidentialKey should be in use" + String old = new String(Base64.encode(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes("UTF-8")))); + Secret s = Secret.fromString(old); + assertEquals("secret by the old key should decrypt", str, s.getPlainText()); + assertNotEquals("but when encrypting, ConfidentialKey should be in use", old, s.getEncryptedValue()); } } + } From 27c83f9c4f827faa8bdc29b20e755c6ff63ad51a Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 13:25:49 -0500 Subject: [PATCH 22/85] CryptoConfidentialKeyTest --- .../security/CryptoConfidentialKeyTest.groovy | 37 ------------------- .../security/CryptoConfidentialKeyTest.java | 35 ++++++++++++++++++ 2 files changed, 35 insertions(+), 37 deletions(-) delete mode 100644 core/src/test/groovy/jenkins/security/CryptoConfidentialKeyTest.groovy create mode 100644 core/src/test/java/jenkins/security/CryptoConfidentialKeyTest.java diff --git a/core/src/test/groovy/jenkins/security/CryptoConfidentialKeyTest.groovy b/core/src/test/groovy/jenkins/security/CryptoConfidentialKeyTest.groovy deleted file mode 100644 index e3816a342c..0000000000 --- a/core/src/test/groovy/jenkins/security/CryptoConfidentialKeyTest.groovy +++ /dev/null @@ -1,37 +0,0 @@ -package jenkins.security - -import org.junit.Rule -import org.junit.Test - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class CryptoConfidentialKeyTest { - @Rule - public ConfidentialStoreRule store = new ConfidentialStoreRule() - - def key = new CryptoConfidentialKey("test") - - @Test - void decryptGetsPlainTextBack() { - ["Hello world","","\u0000"].each { str -> - assert key.decrypt().doFinal(key.encrypt().doFinal(str.bytes))==str.bytes - } - } - - @Test - void multipleEncryptsAreIdempotent() { - def str = "Hello world".bytes - assert key.encrypt().doFinal(str)==key.encrypt().doFinal(str) - } - - @Test - void loadingExistingKey() { - def key2 = new CryptoConfidentialKey("test") // this will cause the key to be loaded from the disk - ["Hello world","","\u0000"].each { str -> - assert key2.decrypt().doFinal(key.encrypt().doFinal(str.bytes))==str.bytes - } - } -} diff --git a/core/src/test/java/jenkins/security/CryptoConfidentialKeyTest.java b/core/src/test/java/jenkins/security/CryptoConfidentialKeyTest.java new file mode 100644 index 0000000000..1b302a358b --- /dev/null +++ b/core/src/test/java/jenkins/security/CryptoConfidentialKeyTest.java @@ -0,0 +1,35 @@ +package jenkins.security; + +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; + +public class CryptoConfidentialKeyTest { + + @Rule + public ConfidentialStoreRule store = new ConfidentialStoreRule(); + + private CryptoConfidentialKey key = new CryptoConfidentialKey("test"); + + @Test + public void decryptGetsPlainTextBack() throws Exception { + for (String str : new String[] {"Hello world", "", "\u0000"}) { + assertArrayEquals(str.getBytes(), key.decrypt().doFinal(key.encrypt().doFinal(str.getBytes()))); + } + } + + @Test + public void multipleEncryptsAreIdempotent() throws Exception { + byte[] str = "Hello world".getBytes(); + assertArrayEquals(key.encrypt().doFinal(str), key.encrypt().doFinal(str)); + } + + @Test + public void loadingExistingKey() throws Exception { + CryptoConfidentialKey key2 = new CryptoConfidentialKey("test"); // this will cause the key to be loaded from the disk + for (String str : new String[] {"Hello world", "", "\u0000"}) { + assertArrayEquals(str.getBytes(), key2.decrypt().doFinal(key.encrypt().doFinal(str.getBytes()))); + } + } + +} From 5d7ea02e572c6883fb75fe7c03295309756dde44 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 13:31:54 -0500 Subject: [PATCH 23/85] DefaultConfidentialStoreTest --- .../DefaultConfidentialStoreTest.groovy | 54 ------------------- .../DefaultConfidentialStoreTest.java | 47 ++++++++++++++++ 2 files changed, 47 insertions(+), 54 deletions(-) delete mode 100644 core/src/test/groovy/jenkins/security/DefaultConfidentialStoreTest.groovy create mode 100644 core/src/test/java/jenkins/security/DefaultConfidentialStoreTest.java diff --git a/core/src/test/groovy/jenkins/security/DefaultConfidentialStoreTest.groovy b/core/src/test/groovy/jenkins/security/DefaultConfidentialStoreTest.groovy deleted file mode 100644 index 83cdaf218c..0000000000 --- a/core/src/test/groovy/jenkins/security/DefaultConfidentialStoreTest.groovy +++ /dev/null @@ -1,54 +0,0 @@ -package jenkins.security - -import hudson.FilePath -import hudson.Functions -import hudson.Util -import org.junit.After -import org.junit.Before -import org.junit.Test - -/** - * @author Kohsuke Kawaguchi - */ -public class DefaultConfidentialStoreTest { - - def tmp; - - @Before - void setup() { - tmp = Util.createTempDir() - } - - @After - void tearDown() { - Util.deleteRecursive(tmp) - } - - @Test - void roundtrip() { - tmp.deleteDir() // let ConfidentialStore create a directory - - def store = new DefaultConfidentialStore(tmp); - def key = new ConfidentialKey("test") {}; - - // basic roundtrip - def str = "Hello world!" - store.store(key, str.bytes) - assert new String(store.load(key))==str - - // data storage should have some stuff - assert new File(tmp,"test").exists() - assert new File(tmp,"master.key").exists() - - assert !new File(tmp,"test").text.contains("Hello") // the data shouldn't be a plain text, obviously - - if (!Functions.isWindows()) - assert (new FilePath(tmp).mode()&0777) == 0700 // should be read only - - // if the master key changes, we should gracefully fail to load the store - new File(tmp,"master.key").delete() - def store2 = new DefaultConfidentialStore(tmp) - assert new File(tmp,"master.key").exists() // we should have a new key now - assert store2.load(key)==null; - } -} diff --git a/core/src/test/java/jenkins/security/DefaultConfidentialStoreTest.java b/core/src/test/java/jenkins/security/DefaultConfidentialStoreTest.java new file mode 100644 index 0000000000..539b72ddc4 --- /dev/null +++ b/core/src/test/java/jenkins/security/DefaultConfidentialStoreTest.java @@ -0,0 +1,47 @@ +package jenkins.security; + +import hudson.FilePath; +import hudson.Functions; +import java.io.File; +import org.apache.commons.io.FileUtils; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class DefaultConfidentialStoreTest { + + @Rule + public TemporaryFolder tmpRule = new TemporaryFolder(); + + @Test + public void roundtrip() throws Exception { + File tmp = new File(tmpRule.getRoot(), "tmp"); // let ConfidentialStore create a directory + + DefaultConfidentialStore store = new DefaultConfidentialStore(tmp); + ConfidentialKey key = new ConfidentialKey("test") {}; + + // basic roundtrip + String str = "Hello world!"; + store.store(key, str.getBytes()); + assertEquals(str, new String(store.load(key))); + + // data storage should have some stuff + assertTrue(new File(tmp, "test").exists()); + assertTrue(new File(tmp, "master.key").exists()); + + assertThat(FileUtils.readFileToString(new File(tmp, "test")), not(containsString("Hello"))); // the data shouldn't be a plain text, obviously + + if (!Functions.isWindows()) { + assertEquals(0700, new FilePath(tmp).mode() & 0777); // should be read only + } + + // if the master key changes, we should gracefully fail to load the store + new File(tmp, "master.key").delete(); + DefaultConfidentialStore store2 = new DefaultConfidentialStore(tmp); + assertTrue(new File(tmp, "master.key").exists()); // we should have a new key now + assertNull(store2.load(key)); + } + +} From e094f01edaeaf878be61e4775ea6ea20aea8f234 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 13:37:22 -0500 Subject: [PATCH 24/85] Removing SleepBuilderTest: https://github.com/jenkinsci/jenkins-test-harness/pull/90 --- .../jvnet/hudson/test/SleepBuilderTest.groovy | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 test/src/test/groovy/org/jvnet/hudson/test/SleepBuilderTest.groovy diff --git a/test/src/test/groovy/org/jvnet/hudson/test/SleepBuilderTest.groovy b/test/src/test/groovy/org/jvnet/hudson/test/SleepBuilderTest.groovy deleted file mode 100644 index a50e3e8fea..0000000000 --- a/test/src/test/groovy/org/jvnet/hudson/test/SleepBuilderTest.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package org.jvnet.hudson.test - -import org.junit.Rule -import org.junit.Test - -/** - * @author schristou@cloudbees.com - * Date: 9/17/13 - * Time: 11:43 AM - * To change this template use File | Settings | File Templates. - */ -class SleepBuilderTest { - @Rule public JenkinsRule j = new JenkinsRule(); - @Test - void testPerform() { - def project = j.createFreeStyleProject(); - def builder = new SleepBuilder(30) - project.buildersList.add(builder); - j.configRoundtrip(project); - j.assertEqualDataBoundBeans(project.buildersList.get(SleepBuilder.class), builder); - } -} From 1cad7b5ff2b03ef859e08614f8d294f466bcf44b Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 14:00:05 -0500 Subject: [PATCH 25/85] HMACConfidentialKeyTest --- .../security/HMACConfidentialKeyTest.groovy | 39 ------------------- .../security/HMACConfidentialKeyTest.java | 38 ++++++++++++++++++ 2 files changed, 38 insertions(+), 39 deletions(-) delete mode 100644 core/src/test/groovy/jenkins/security/HMACConfidentialKeyTest.groovy create mode 100644 core/src/test/java/jenkins/security/HMACConfidentialKeyTest.java diff --git a/core/src/test/groovy/jenkins/security/HMACConfidentialKeyTest.groovy b/core/src/test/groovy/jenkins/security/HMACConfidentialKeyTest.groovy deleted file mode 100644 index 118ff5b904..0000000000 --- a/core/src/test/groovy/jenkins/security/HMACConfidentialKeyTest.groovy +++ /dev/null @@ -1,39 +0,0 @@ -package jenkins.security - -import org.junit.Rule -import org.junit.Test - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class HMACConfidentialKeyTest { - @Rule - public ConfidentialStoreRule store = new ConfidentialStoreRule() - - def key = new HMACConfidentialKey("test",16) - - @Test - void basics() { - def unique = [] as TreeSet - ["Hello world","","\u0000"].each { str -> - def mac = key.mac(str) - unique.add(mac) - assert mac =~ /[0-9A-Fa-f]{32}/ - assert key.checkMac(str,mac) - assert !key.checkMac("garbage",mac) - } - - assert unique.size()==3 // make sure all 3 MAC are different - } - - @Test - void loadingExistingKey() { - // this second key of the same ID will cause it to load the key from the disk - def key2 = new HMACConfidentialKey("test",16) - ["Hello world","","\u0000"].each { str -> - assert key.mac(str)==key2.mac(str) - } - } -} diff --git a/core/src/test/java/jenkins/security/HMACConfidentialKeyTest.java b/core/src/test/java/jenkins/security/HMACConfidentialKeyTest.java new file mode 100644 index 0000000000..93cb94cfdb --- /dev/null +++ b/core/src/test/java/jenkins/security/HMACConfidentialKeyTest.java @@ -0,0 +1,38 @@ +package jenkins.security; + +import java.util.Set; +import java.util.TreeSet; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; + +public class HMACConfidentialKeyTest { + + @Rule + public ConfidentialStoreRule store = new ConfidentialStoreRule(); + + private HMACConfidentialKey key = new HMACConfidentialKey("test", 16); + + @Test + public void basics() { + Set unique = new TreeSet<>(); + for (String str : new String[] {"Hello world", "", "\u0000"}) { + String mac = key.mac(str); + unique.add(mac); + assertTrue(mac, mac.matches("[0-9A-Fa-f]{32}")); + assertTrue(key.checkMac(str, mac)); + assertFalse(key.checkMac("garbage", mac)); + } + assertEquals("all 3 MAC are different", 3, unique.size()); + } + + @Test + public void loadingExistingKey() { + // this second key of the same ID will cause it to load the key from the disk + HMACConfidentialKey key2 = new HMACConfidentialKey("test", 16); + for (String str : new String[] {"Hello world", "", "\u0000"}) { + assertEquals(key.mac(str), key2.mac(str)); + } + } + +} From 39826800222af9e5753015c60ce3a9ac294144ef Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 15:46:17 -0500 Subject: [PATCH 26/85] HexStringConfidentialKeyTest --- .../HexStringConfidentialKeyTest.groovy | 43 ------------------- .../HexStringConfidentialKeyTest.java | 41 ++++++++++++++++++ 2 files changed, 41 insertions(+), 43 deletions(-) delete mode 100644 core/src/test/groovy/jenkins/security/HexStringConfidentialKeyTest.groovy create mode 100644 core/src/test/java/jenkins/security/HexStringConfidentialKeyTest.java diff --git a/core/src/test/groovy/jenkins/security/HexStringConfidentialKeyTest.groovy b/core/src/test/groovy/jenkins/security/HexStringConfidentialKeyTest.groovy deleted file mode 100644 index 7062d2326f..0000000000 --- a/core/src/test/groovy/jenkins/security/HexStringConfidentialKeyTest.groovy +++ /dev/null @@ -1,43 +0,0 @@ -package jenkins.security - -import org.junit.Rule -import org.junit.Test - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class HexStringConfidentialKeyTest { - @Rule - public ConfidentialStoreRule store = new ConfidentialStoreRule() - - @Test - void hexStringShouldProduceHexString() { - def key = new HexStringConfidentialKey("test",8) - assert key.get() =~ /[A-Fa-f0-9]{8}/ - } - - @Test - void multipleGetsAreIdempotent() { - def key = new HexStringConfidentialKey("test",8) - assert key.get()==key.get() - } - - @Test - void specifyLengthAndMakeSureItTakesEffect() { - [8,16,32,256].each { n -> - new HexStringConfidentialKey("test"+n,n).get().length()==n - } - } - - @Test - void loadingExistingKey() { - def key1 = new HexStringConfidentialKey("test",8) - key1.get() // this causes the ke to be generated - - // this second key of the same ID will cause it to load the key from the disk - def key2 = new HexStringConfidentialKey("test",8) - assert key1.get()==key2.get() - } -} diff --git a/core/src/test/java/jenkins/security/HexStringConfidentialKeyTest.java b/core/src/test/java/jenkins/security/HexStringConfidentialKeyTest.java new file mode 100644 index 0000000000..e76ad914f9 --- /dev/null +++ b/core/src/test/java/jenkins/security/HexStringConfidentialKeyTest.java @@ -0,0 +1,41 @@ +package jenkins.security; + +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; + +public class HexStringConfidentialKeyTest { + + @Rule + public ConfidentialStoreRule store = new ConfidentialStoreRule(); + + @Test + public void hexStringShouldProduceHexString() { + HexStringConfidentialKey key = new HexStringConfidentialKey("test", 8); + assertTrue(key.get().matches("[A-Fa-f0-9]{8}")); + } + + @Test + public void multipleGetsAreIdempotent() { + HexStringConfidentialKey key = new HexStringConfidentialKey("test", 8); + assertEquals(key.get(), key.get()); + } + + @Test + public void specifyLengthAndMakeSureItTakesEffect() { + for (int n : new int[] {8, 16, 32, 256}) { + assertEquals(n, new HexStringConfidentialKey("test" + n, n).get().length()); + } + } + + @Test + public void loadingExistingKey() { + HexStringConfidentialKey key1 = new HexStringConfidentialKey("test", 8); + key1.get(); // this causes the ke to be generated + + // this second key of the same ID will cause it to load the key from the disk + HexStringConfidentialKey key2 = new HexStringConfidentialKey("test", 8); + assertEquals(key1.get(), key2.get()); + } + +} From 046fca7e2f0845e0922277cb49a73e3dd0592dd8 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 15:49:41 -0500 Subject: [PATCH 27/85] RSAConfidentialKeyTest --- .../security/RSAConfidentialKeyTest.java} | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) rename core/src/test/{groovy/jenkins/security/RSAConfidentialKeyTest.groovy => java/jenkins/security/RSAConfidentialKeyTest.java} (74%) diff --git a/core/src/test/groovy/jenkins/security/RSAConfidentialKeyTest.groovy b/core/src/test/java/jenkins/security/RSAConfidentialKeyTest.java similarity index 74% rename from core/src/test/groovy/jenkins/security/RSAConfidentialKeyTest.groovy rename to core/src/test/java/jenkins/security/RSAConfidentialKeyTest.java index ed9ef183fe..7b4513940c 100644 --- a/core/src/test/groovy/jenkins/security/RSAConfidentialKeyTest.groovy +++ b/core/src/test/java/jenkins/security/RSAConfidentialKeyTest.java @@ -21,28 +21,26 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package jenkins.security +package jenkins.security; -import org.junit.Rule -import org.junit.Test +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; + +public class RSAConfidentialKeyTest { -/** - * - * - * @author Kohsuke Kawaguchi - */ -class RSAConfidentialKeyTest { @Rule - public ConfidentialStoreRule store = new ConfidentialStoreRule() + public ConfidentialStoreRule store = new ConfidentialStoreRule(); - def key = new RSAConfidentialKey("test") {} + private RSAConfidentialKey key = new RSAConfidentialKey("test") {}; @Test - void loadingExistingKey() { + public void loadingExistingKey() { // this second key of the same ID will cause it to load the key from the disk - def key2 = new RSAConfidentialKey("test") {} + RSAConfidentialKey key2 = new RSAConfidentialKey("test") {}; - assert key.privateKey==key2.privateKey; - assert key.publicKey ==key2.publicKey; + assertEquals(key.getPrivateKey(), key2.getPrivateKey()); + assertEquals(key.getPublicKey(), key2.getPublicKey()); } + } From a8251f66d50ae535d32d8b09da32a08b9baa96c3 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 15:51:44 -0500 Subject: [PATCH 28/85] RSADigitalSignatureConfidentialKeyTest --- ...ADigitalSignatureConfidentialKeyTest.java} | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) rename core/src/test/{groovy/jenkins/security/RSADigitalSignatureConfidentialKeyTest.groovy => java/jenkins/security/RSADigitalSignatureConfidentialKeyTest.java} (65%) diff --git a/core/src/test/groovy/jenkins/security/RSADigitalSignatureConfidentialKeyTest.groovy b/core/src/test/java/jenkins/security/RSADigitalSignatureConfidentialKeyTest.java similarity index 65% rename from core/src/test/groovy/jenkins/security/RSADigitalSignatureConfidentialKeyTest.groovy rename to core/src/test/java/jenkins/security/RSADigitalSignatureConfidentialKeyTest.java index 7c31c13365..16e57e117f 100644 --- a/core/src/test/groovy/jenkins/security/RSADigitalSignatureConfidentialKeyTest.groovy +++ b/core/src/test/java/jenkins/security/RSADigitalSignatureConfidentialKeyTest.java @@ -21,35 +21,32 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package jenkins.security +package jenkins.security; -import hudson.remoting.Base64 -import org.junit.Rule -import org.junit.Test +import hudson.remoting.Base64; +import java.security.Signature; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; -import java.security.Signature +public class RSADigitalSignatureConfidentialKeyTest { -/** - * - * - * @author Kohsuke Kawaguchi - */ -class RSADigitalSignatureConfidentialKeyTest { @Rule - public ConfidentialStoreRule store = new ConfidentialStoreRule() + public ConfidentialStoreRule store = new ConfidentialStoreRule(); - def key = new RSADigitalSignatureConfidentialKey("test"); + private final RSADigitalSignatureConfidentialKey key = new RSADigitalSignatureConfidentialKey("test"); @Test - void dsigSignAndVerify() { - def plainText = "Hello world" - def msg = key.sign(plainText); - println msg; + public void dsigSignAndVerify() throws Exception { + String plainText = "Hello world"; + String msg = key.sign(plainText); + System.out.println(msg); - def sig = Signature.getInstance("SHA256withRSA"); - sig.initVerify(key.publicKey); + Signature sig = Signature.getInstance("SHA256withRSA"); + sig.initVerify(key.getPublicKey()); sig.update(plainText.getBytes("UTF-8")); - assert sig.verify(Base64.decode(msg)) + assertTrue(sig.verify(Base64.decode(msg))); } + } From 9b419100afab9eae182de01935ea8c18482adba2 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 15:59:31 -0500 Subject: [PATCH 29/85] AtmostOneTaskExecutorTest --- core/pom.xml | 19 ------- .../util/AtmostOneTaskExecutorTest.groovy | 47 ------------------ .../util/AtmostOneTaskExecutorTest.java | 49 +++++++++++++++++++ 3 files changed, 49 insertions(+), 66 deletions(-) delete mode 100644 core/src/test/groovy/jenkins/util/AtmostOneTaskExecutorTest.groovy create mode 100644 core/src/test/java/jenkins/util/AtmostOneTaskExecutorTest.java diff --git a/core/pom.xml b/core/pom.xml index 5b8e70a1ee..46a63c7c81 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -796,25 +796,6 @@ THE SOFTWARE. - - org.codehaus.gmaven - gmaven-plugin - - - - generateTestStubs - testCompile - - - - - - org.codehaus.groovy - groovy-all - ${groovy.version} - - - org.codehaus.mojo findbugs-maven-plugin diff --git a/core/src/test/groovy/jenkins/util/AtmostOneTaskExecutorTest.groovy b/core/src/test/groovy/jenkins/util/AtmostOneTaskExecutorTest.groovy deleted file mode 100644 index e39addae77..0000000000 --- a/core/src/test/groovy/jenkins/util/AtmostOneTaskExecutorTest.groovy +++ /dev/null @@ -1,47 +0,0 @@ -package jenkins.util - -import hudson.util.OneShotEvent -import org.junit.Test - -import java.util.concurrent.Callable -import java.util.concurrent.Executors -import java.util.concurrent.atomic.AtomicInteger - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class AtmostOneTaskExecutorTest { - def counter = new AtomicInteger() - - def lock = new OneShotEvent() - - @Test - public void doubleBooking() { - def f1,f2; - - def base = Executors.newCachedThreadPool() - def es = new AtmostOneTaskExecutor(base, - { -> - counter.incrementAndGet() - lock.block() - } as Callable); - f1 = es.submit() - while (counter.get() == 0) - ; // spin lock until executor gets to the choking point - - f2 = es.submit() // this should hang - Thread.sleep(500) // make sure the 2nd task is hanging - assert counter.get() == 1 - assert !f2.isDone() - - lock.signal() // let the first one go - - f1.get(); // first one should complete - - // now 2nd one gets going and hits the choke point - f2.get() - assert counter.get()==2 - } -} diff --git a/core/src/test/java/jenkins/util/AtmostOneTaskExecutorTest.java b/core/src/test/java/jenkins/util/AtmostOneTaskExecutorTest.java new file mode 100644 index 0000000000..c3e8b8461a --- /dev/null +++ b/core/src/test/java/jenkins/util/AtmostOneTaskExecutorTest.java @@ -0,0 +1,49 @@ +package jenkins.util; + +import hudson.util.OneShotEvent; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import static org.junit.Assert.*; +import org.junit.Test; + +public class AtmostOneTaskExecutorTest { + + @SuppressWarnings("empty-statement") + @Test + public void doubleBooking() throws Exception { + AtomicInteger counter = new AtomicInteger(); + OneShotEvent lock = new OneShotEvent(); + Future f1, f2; + + ExecutorService base = Executors.newCachedThreadPool(); + AtmostOneTaskExecutor es = new AtmostOneTaskExecutor(base, () -> { + counter.incrementAndGet(); + try { + lock.block(); + } catch (InterruptedException x) { + assert false : x; + } + return null; + }); + f1 = es.submit(); + while (counter.get() == 0) { + // spin lock until executor gets to the choking point + } + + f2 = es.submit(); // this should hang + Thread.sleep(500); // make sure the 2nd task is hanging + assertEquals(1, counter.get()); + assertFalse(f2.isDone()); + + lock.signal(); // let the first one go + + f1.get(); // first one should complete + + // now 2nd one gets going and hits the choke point + f2.get(); + assertEquals(2, counter.get()); + } + +} From 46c8f144716bfbcc5cd31a8eff64c14c5190708b Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 16:04:43 -0500 Subject: [PATCH 30/85] Deleting ancient cobertura profile, which was broken at least by Java 8 if not earlier. If we want code coverage we can use JaCoCo as plugin-pom does. --- core/pom.xml | 36 ----------- core/src/build-script/Cobertura.groovy | 82 -------------------------- core/src/build-script/readme.txt | 4 -- core/src/build-script/unitTest.groovy | 11 ---- 4 files changed, 133 deletions(-) delete mode 100644 core/src/build-script/Cobertura.groovy delete mode 100644 core/src/build-script/readme.txt delete mode 100644 core/src/build-script/unitTest.groovy diff --git a/core/pom.xml b/core/pom.xml index 46a63c7c81..fbc89c67de 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -870,41 +870,5 @@ THE SOFTWARE. true - - - cobertura - - - - org.codehaus.gmaven - gmaven-plugin - - - - - test - - execute - - - - ${project.basedir}/src/build-script - - ${project.basedir}/src/build-script/unitTest.groovy - - - - - - - maven-surefire-plugin - - - true - - - - - diff --git a/core/src/build-script/Cobertura.groovy b/core/src/build-script/Cobertura.groovy deleted file mode 100644 index 49b773b49b..0000000000 --- a/core/src/build-script/Cobertura.groovy +++ /dev/null @@ -1,82 +0,0 @@ -import org.apache.maven.project.MavenProject; - -/** - * Cobertura invoker. - */ -public class Cobertura { - private final MavenProject project; - // maven helper - private def maven; - // ant builder - private def ant; - /** - * Cobertura data file. - */ - private final File ser; - - def Cobertura(project, maven, ant, ser) { - this.project = maven.project; - this.maven = maven; - this.ant = ant; - this.ser =ser; - - // define cobertura tasks - ant.taskdef(resource:"tasks.properties") - } - - // function that ensures that the given directory exists - private String dir(String dir) { - new File(project.basedir,dir).mkdirs(); - return dir; - } - - /** - * Instruments the given class dirs/jars by cobertura - * - * @param files - * List of jar files and class dirs to instrument. - */ - def instrument(files) { - ant."cobertura-instrument"(todir:dir("target/cobertura-classes"),datafile:ser) { - fileset(dir:"target/classes"); - files.each{ fileset(file:it) } - } - } - - def runTests() { - ant.junit(fork:true, forkMode:"once", failureproperty:"failed", printsummary:true) { - classpath { - junitClasspath() - } - batchtest(todir:dir("target/surefire-reports")) { - fileset(dir:"target/test-classes") { - include(name:"**/*Test.class") - } - formatter(type:"xml") - } - sysproperty(key:"net.sourceforge.cobertura.datafile",value:ser) - sysproperty(key:"hudson.ClassicPluginStrategy.useAntClassLoader",value:"true") - } - } - - def junitClasspath() { - ant.pathelement(path: "target/cobertura-classes") // put the instrumented classes first - ant.fileset(dir:"target/cobertura-classes",includes:"*.jar") // instrumented jar files - ant.pathelement(path: maven.resolveArtifact("net.sourceforge.cobertura:cobertura:1.9")) // cobertura runtime - project.getTestClasspathElements().each { ant.pathelement(path: it) } // the rest of the dependencies - } - - def report(dirs) { - maven.attachArtifact(ser,"ser","cobertura") - ["html","xml"].each { format -> - ant."cobertura-report"(format:format,datafile:ser,destdir:dir("target/cobertura-reports"),srcdir:"src/main/java") { - dirs.each{ fileset(dir:it) } - } - } - } - - def makeBuildFailIfTestFail() { - if(ant.project.getProperty("failed")!=null && !Boolean.getBoolean("testFailureIgnore")) - throw new Exception("Some unit tests failed"); - } -} diff --git a/core/src/build-script/readme.txt b/core/src/build-script/readme.txt deleted file mode 100644 index 6c53a01026..0000000000 --- a/core/src/build-script/readme.txt +++ /dev/null @@ -1,4 +0,0 @@ -Parts of the build scripts that are written in Groovy. - -IntelliJ needs Groovy files to be in its source directory to provide completion and such, -so we need this to be in its own directory. \ No newline at end of file diff --git a/core/src/build-script/unitTest.groovy b/core/src/build-script/unitTest.groovy deleted file mode 100644 index 653391aad7..0000000000 --- a/core/src/build-script/unitTest.groovy +++ /dev/null @@ -1,11 +0,0 @@ -// run unit tests - -ant.project.setBaseDir(project.basedir) -ser=new File(project.basedir,"target/cobertura.ser"); // store cobertura data file in a module-specific location - -cob = new Cobertura(project,maven,ant,ser); - -cob.instrument([]) -cob.runTests() -cob.report([]) -cob.makeBuildFailIfTestFail(); From 9d43c3c9bce891854890046987f64770d38cda9a Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 16:07:34 -0500 Subject: [PATCH 31/85] Obsolete m2e exclusion. --- pom.xml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/pom.xml b/pom.xml index 9e95d80f7a..3f0adde0c5 100644 --- a/pom.xml +++ b/pom.xml @@ -584,20 +584,6 @@ THE SOFTWARE. - - - org.codehaus.gmaven - gmaven-plugin - 1.5-jenkins-1 - - execute - testCompile - - - - - - org.apache.maven.plugins From e39007250a90291b0b83afaa32f05ddb902bb1dc Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 16:09:35 -0500 Subject: [PATCH 32/85] Cleaning up some more unusable Cobertura executions. --- pom.xml | 9 --------- test/pom.xml | 29 --------------------------- test/src/build-script/unitTest.groovy | 16 --------------- 3 files changed, 54 deletions(-) delete mode 100644 test/src/build-script/unitTest.groovy diff --git a/pom.xml b/pom.xml index 3f0adde0c5..52e85c8a2c 100644 --- a/pom.xml +++ b/pom.xml @@ -349,11 +349,6 @@ THE SOFTWARE. ant-junit 1.7.0 - - net.sourceforge.cobertura - cobertura - 1.9 - javax.servlet @@ -516,10 +511,6 @@ THE SOFTWARE. antlr-maven-plugin 2.1 - - org.codehaus.mojo - cobertura-maven-plugin - org.codehaus.mojo findbugs-maven-plugin diff --git a/test/pom.xml b/test/pom.xml index afa1599871..f0c8d40325 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -321,35 +321,6 @@ THE SOFTWARE. 4 - - - cobertura - - - - org.codehaus.gmaven - gmaven-plugin - - - - - test - - execute - - - - - ${project.basedir}/../core/src/build-script - - ${project.basedir}/src/build-script/unitTest.groovy - - - - - - - jacoco diff --git a/test/src/build-script/unitTest.groovy b/test/src/build-script/unitTest.groovy deleted file mode 100644 index 73e5a56465..0000000000 --- a/test/src/build-script/unitTest.groovy +++ /dev/null @@ -1,16 +0,0 @@ -// run unit tests -import org.apache.commons.io.FileUtils - -ant.project.setBaseDir(project.basedir) - -// start from where the core left off, and build from there -ser=new File(project.basedir,"target/cobertura.ser"); -FileUtils.copyFile(maven.resolveArtifact("${project.groupId}:hudson-core:${project.version}:cobertura:ser"),ser) - -cob = new Cobertura(project,maven,ant,ser); - -// instrumenting remote.jar causes all native m2 job to fail, because forked Maven doesn't have cobertura runtime -cob.instrument(["hudson-core"].collect{ m -> maven.resolveArtifact("${project.groupId}:${m}:${project.version}") }) -cob.runTests() -cob.report(["../core/src/main/java"]) -cob.makeBuildFailIfTestFail(); From aa5e776d9a61a290a11ba055d9ac114738098bbc Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 16:15:47 -0500 Subject: [PATCH 33/85] All gmaven usage is now contained in the test module. --- pom.xml | 28 ---------------------------- test/pom.xml | 22 ++++++++++++++++++++++ 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/pom.xml b/pom.xml index 52e85c8a2c..469439f8a8 100644 --- a/pom.xml +++ b/pom.xml @@ -339,34 +339,6 @@ THE SOFTWARE. - - org.codehaus.gmaven - gmaven-plugin - 1.5-jenkins-3 - - - org.apache.ant - ant-junit - 1.7.0 - - - - javax.servlet - javax.servlet-api - 3.1.0 - - - org.codehaus.gmaven.runtime - gmaven-runtime-2.0 - 1.5-jenkins-3 - - - - - 2.0 - - org.apache.maven.plugins maven-deploy-plugin diff --git a/test/pom.xml b/test/pom.xml index f0c8d40325..6d70dd8805 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -244,6 +244,7 @@ THE SOFTWARE. org.codehaus.gmaven gmaven-plugin + 1.5-jenkins-3 default @@ -266,7 +267,28 @@ THE SOFTWARE. ant-launcher 1.8.0 + + org.apache.ant + ant-junit + 1.7.0 + + + + javax.servlet + javax.servlet-api + 3.1.0 + + + org.codehaus.gmaven.runtime + gmaven-runtime-2.0 + 1.5-jenkins-3 + + + + 2.0 + maven-deploy-plugin From 21ea7956f928c8567b1f113ea9b855a5f3ab04a9 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 16:19:22 -0500 Subject: [PATCH 34/85] Deleting GroovyTest, which only tested the ability to write Jenkins tests in Groovy, not Jenkins itself. --- test/src/test/groovy/hudson/GroovyTest.groovy | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 test/src/test/groovy/hudson/GroovyTest.groovy diff --git a/test/src/test/groovy/hudson/GroovyTest.groovy b/test/src/test/groovy/hudson/GroovyTest.groovy deleted file mode 100644 index e512cf56ef..0000000000 --- a/test/src/test/groovy/hudson/GroovyTest.groovy +++ /dev/null @@ -1,45 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc. - * - * 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 - -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.JenkinsRule - -/** - * First groovy test! - * - * @author Kohsuke Kawaguchi - */ -public class GroovyTest { - - @Rule - public JenkinsRule j = new JenkinsRule() - - @Test - void test() { - def wc = j.createWebClient(); - wc.goTo(""); - } -} From c3c01c0335e2f1f8d87fb4fb57bcdfd3c8124049 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 16:27:19 -0500 Subject: [PATCH 35/85] RelativePathTest --- .../groovy/hudson/RelativePathTest.groovy | 55 ------------------ .../test/java/hudson/RelativePathTest.java | 58 +++++++++++++++++++ 2 files changed, 58 insertions(+), 55 deletions(-) delete mode 100644 test/src/test/groovy/hudson/RelativePathTest.groovy create mode 100644 test/src/test/java/hudson/RelativePathTest.java diff --git a/test/src/test/groovy/hudson/RelativePathTest.groovy b/test/src/test/groovy/hudson/RelativePathTest.groovy deleted file mode 100644 index 25902f811c..0000000000 --- a/test/src/test/groovy/hudson/RelativePathTest.groovy +++ /dev/null @@ -1,55 +0,0 @@ -package hudson - -import hudson.model.AbstractDescribableImpl -import hudson.model.Describable -import hudson.model.Descriptor -import hudson.util.ListBoxModel -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.HudsonTestCase -import org.jvnet.hudson.test.TestExtension -import org.kohsuke.stapler.QueryParameter - -/** - * Regression test for JENKINS-18776 - * - * @author Kohsuke Kawaguchi - */ -class RelativePathTest extends HudsonTestCase implements Describable { - @Issue("JENKINS-18776") - void testRelativePath() { - // I was having trouble causing annotation processing on test stubs - jenkins.getDescriptorOrDie(RelativePathTest.class) - jenkins.getDescriptorOrDie(Model.class) - - createWebClient().goTo("/self/"); - assert jenkins.getDescriptorOrDie(Model.class).touched - } - - String getName() { - return "Alice"; - } - - Model getModel() { - return new Model(); - } - - DescriptorImpl getDescriptor() { - return jenkins.getDescriptorOrDie(getClass()); - } - - @TestExtension - static class DescriptorImpl extends Descriptor {} - - static class Model extends AbstractDescribableImpl { - @TestExtension - static class DescriptorImpl extends Descriptor { - boolean touched; - - ListBoxModel doFillAbcItems(@RelativePath("..") @QueryParameter String name) { - assert name=="Alice"; - touched = true; - return new ListBoxModel().add("foo").add("bar") - } - } - } -} diff --git a/test/src/test/java/hudson/RelativePathTest.java b/test/src/test/java/hudson/RelativePathTest.java new file mode 100644 index 0000000000..494a37f9e9 --- /dev/null +++ b/test/src/test/java/hudson/RelativePathTest.java @@ -0,0 +1,58 @@ +package hudson; + +import hudson.model.AbstractDescribableImpl; +import hudson.model.Describable; +import hudson.model.Descriptor; +import hudson.util.ListBoxModel; +import org.jvnet.hudson.test.HudsonTestCase; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.QueryParameter; + +public class RelativePathTest extends HudsonTestCase implements Describable { + + @Issue("JENKINS-18776") + public void testRelativePath() throws Exception { + // I was having trouble causing annotation processing on test stubs +// jenkins.getDescriptorOrDie(RelativePathTest.class); +// jenkins.getDescriptorOrDie(Model.class); + + createWebClient().goTo("/self/"); + assertTrue(((Model.DescriptorImpl) jenkins.getDescriptorOrDie(Model.class)).touched); + } + + @Override // TODO this is horrible, should change the property here and in @QueryParameter and in RelativePathTest/index.groovy to, say, personName + public String getName() { + return "Alice"; + } + + public Model getModel() { + return new Model(); + } + + @Override // TODO would suffice to extend AbstractDescribableImpl + public DescriptorImpl getDescriptor() { + return (DescriptorImpl) jenkins.getDescriptorOrDie(getClass()); + } + + @TestExtension + public static class DescriptorImpl extends Descriptor {} + + public static class Model extends AbstractDescribableImpl { + + @TestExtension + public static class DescriptorImpl extends Descriptor { + + boolean touched; + + public ListBoxModel doFillAbcItems(@RelativePath("..") @QueryParameter String name) { + assertEquals("Alice", name); + touched = true; + return new ListBoxModel().add("foo").add("bar"); + } + + } + + } + +} From 5ed56faaf3689a1392a02e4c4325fcd343efea61 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 16:46:30 -0500 Subject: [PATCH 36/85] BuildCommandTest --- .../groovy/hudson/cli/BuildCommandTest.groovy | 296 ----------------- .../java/hudson/cli/BuildCommand2Test.java | 74 ----- .../java/hudson/cli/BuildCommandTest.java | 313 ++++++++++++++++++ 3 files changed, 313 insertions(+), 370 deletions(-) delete mode 100644 test/src/test/groovy/hudson/cli/BuildCommandTest.groovy delete mode 100644 test/src/test/java/hudson/cli/BuildCommand2Test.java create mode 100644 test/src/test/java/hudson/cli/BuildCommandTest.java diff --git a/test/src/test/groovy/hudson/cli/BuildCommandTest.groovy b/test/src/test/groovy/hudson/cli/BuildCommandTest.groovy deleted file mode 100644 index 86ea1ecc83..0000000000 --- a/test/src/test/groovy/hudson/cli/BuildCommandTest.groovy +++ /dev/null @@ -1,296 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc. - * - * 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.cli - -import org.apache.commons.io.output.TeeOutputStream - -import static org.hamcrest.Matchers.*; -import static hudson.cli.CLICommandInvoker.Matcher.*; -import static org.junit.Assert.* -import hudson.Extension - -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.CaptureEnvironmentBuilder -import org.jvnet.hudson.test.JenkinsRule -import org.jvnet.hudson.test.TestBuilder -import org.jvnet.hudson.test.TestExtension -import org.kohsuke.stapler.StaplerRequest - -import hudson.Launcher -import hudson.cli.CLICommandInvoker; -import hudson.model.AbstractBuild -import hudson.model.Action -import hudson.model.BuildListener -import hudson.model.Executor -import hudson.model.FreeStyleProject; -import hudson.model.ParameterDefinition.ParameterDescriptor -import hudson.model.ParameterValue -import hudson.model.ParametersAction -import hudson.model.ParametersDefinitionProperty -import hudson.model.Queue.QueueDecisionHandler -import hudson.model.Queue.Task; -import hudson.model.SimpleParameterDefinition -import hudson.model.StringParameterDefinition -import hudson.model.StringParameterValue -import hudson.model.labels.LabelAtom -import hudson.tasks.Shell -import hudson.util.OneShotEvent - -import java.util.concurrent.Executor - -import net.sf.json.JSONObject - -/** - * {@link BuildCommand} test. - * - * @author Kohsuke Kawaguchi - */ -public class BuildCommandTest { - - @Rule public JenkinsRule j = new JenkinsRule(); - - /** - * Just schedules a build and return. - */ - @Test void async() { - def p = j.createFreeStyleProject(); - def started = new OneShotEvent(); - def completed = new OneShotEvent(); - p.buildersList.add([perform: {AbstractBuild build, Launcher launcher, BuildListener listener -> - started.signal(); - completed.block(); - return true; - }] as TestBuilder); - - // this should be asynchronous - def cli = new CLI(j.URL) - try { - assertEquals(0,cli.execute(["build", p.name])) - started.block() - assertTrue(p.getBuildByNumber(1).isBuilding()) - completed.signal() - } finally { - cli.close(); - } - } - - /** - * Tests synchronous execution. - */ - @Test void sync() { - def p = j.createFreeStyleProject(); - p.buildersList.add(new Shell("sleep 3")); - - def cli = new CLI(j.URL) - try { - cli.execute(["build","-s",p.name]) - assertFalse(p.getBuildByNumber(1).isBuilding()) - } finally { - cli.close(); - } - } - - /** - * Tests synchronous execution with retried verbose output - */ - @Test void syncWOutputStreaming() { - def p = j.createFreeStyleProject(); - p.buildersList.add(new Shell("sleep 3")); - - def cli =new CLI(j.URL) - try { - cli.execute(["build","-s","-v","-r","5",p.name]) - assertFalse(p.getBuildByNumber(1).isBuilding()) - } finally { - cli.close(); - } - } - - @Test void parameters() { - def p = j.createFreeStyleProject(); - p.addProperty(new ParametersDefinitionProperty([new StringParameterDefinition("key",null)])); - - def cli = new CLI(j.URL) - try { - cli.execute(["build","-s","-p","key=foobar",p.name]) - def b = j.assertBuildStatusSuccess(p.getBuildByNumber(1)) - assertEquals("foobar",b.getAction(ParametersAction.class).getParameter("key").value) - } finally { - cli.close(); - } - } - - @Test void defaultParameters() { - def p = j.createFreeStyleProject(); - p.addProperty(new ParametersDefinitionProperty([new StringParameterDefinition("key","default"), new StringParameterDefinition("key2","default2") ])); - - def cli = new CLI(j.URL) - try { - cli.execute(["build","-s","-p","key=foobar",p.name]) - def b = j.assertBuildStatusSuccess(p.getBuildByNumber(1)) - assertEquals("foobar",b.getAction(ParametersAction.class).getParameter("key").value) - assertEquals("default2",b.getAction(ParametersAction.class).getParameter("key2").value) - } finally { - cli.close(); - } - } - - // TODO randomly fails: Started test0 #1 - @Test void consoleOutput() { - def p = j.createFreeStyleProject() - def cli = new CLI(j.URL) - try { - def o = new ByteArrayOutputStream() - cli.execute(["build","-s","-v",p.name],System.in,new TeeOutputStream(System.out,o),System.err) - j.assertBuildStatusSuccess(p.getBuildByNumber(1)) - assertTrue(o.toString(), o.toString().contains("Started from command line by anonymous")) - assertTrue(o.toString().contains("Finished: SUCCESS")) - } finally { - cli.close() - } - } - - // TODO randomly fails: Started test0 #1 - @Test void consoleOutputWhenBuildSchedulingRefused() { - def p = j.createFreeStyleProject() - def cli = new CLI(j.URL) - try { - def o = new ByteArrayOutputStream() - cli.execute(["build","-s","-v",p.name],System.in,System.out,new TeeOutputStream(System.err,o)) - assertTrue(o.toString(), o.toString().contains(BuildCommand.BUILD_SCHEDULING_REFUSED)) - } finally { - cli.close() - } - } - // <=> - @TestExtension("consoleOutputWhenBuildSchedulingRefused") - static class UnschedulingVetoer extends QueueDecisionHandler { - public boolean shouldSchedule(Task task, List actions) { - return false; - } - } - - @Test void refuseToBuildDisabledProject() { - - def project = j.createFreeStyleProject("the-project"); - project.disable(); - def invoker = new CLICommandInvoker(j, new BuildCommand()); - def result = invoker.invokeWithArgs("the-project"); - - assertThat(result, failedWith(4)); - assertThat(result.stderr(), containsString("ERROR: Cannot build the-project because it is disabled.")); - assertNull("Project should not be built", project.getBuildByNumber(1)); - } - - @Test void refuseToBuildNewlyCopiedProject() { - - def original = j.createFreeStyleProject("original"); - def newOne = (FreeStyleProject) j.jenkins.copy(original, "new-one"); - def invoker = new CLICommandInvoker(j, new BuildCommand()); - def result = invoker.invokeWithArgs("new-one"); - - assertThat(result, failedWith(4)); - assertThat(result.stderr(), containsString("ERROR: Cannot build new-one because its configuration has not been saved.")); - assertNull("Project should not be built", newOne.getBuildByNumber(1)); - } - - @Test void correctlyParseMapValuesContainingEqualsSign() { - - def project = j.createFreeStyleProject("the-project"); - project.addProperty(new ParametersDefinitionProperty([ - new StringParameterDefinition("expr", null) - ])); - - def invoker = new CLICommandInvoker(j, new BuildCommand()); - def result = invoker.invokeWithArgs("the-project", "-p", "expr=a=b", "-s"); - - assertThat(result, succeeded()); - assertEquals("a=b", project.getBuildByNumber(1).getBuildVariables().get("expr")); - } - - @Issue("JENKINS-15094") - @Test public void executorsAliveOnParameterWithNullDefaultValue() throws Exception { - def slave = j.createSlave(); - FreeStyleProject project = j.createFreeStyleProject("foo"); - project.setAssignedNode(slave); - - // Create test parameter with Null default value - def nullDefaultDefinition = new NullDefaultValueParameterDefinition(); - ParametersDefinitionProperty pdp = new ParametersDefinitionProperty( - new StringParameterDefinition("string", "defaultValue", "description"), - nullDefaultDefinition); - project.addProperty(pdp); - CaptureEnvironmentBuilder builder = new CaptureEnvironmentBuilder(); - project.getBuildersList().add(builder); - - // Warmup - j.buildAndAssertSuccess(project); - - for (def exec : slave.toComputer().getExecutors()) { - assertTrue("Executor has died before the test start: "+exec, exec.isActive()); - } - - // Create CLI & run command - def invoker = new CLICommandInvoker(j, new BuildCommand()); - def result = invoker.invokeWithArgs("foo","-p","string=value"); - assertThat(result, failedWith(2)); - assertThat(result.stderr(), containsString("ERROR: No default value for the parameter \'FOO\'.")); - - Thread.sleep(5000); // Give the job 5 seconds to be submitted - assertNull("Build should not be scheduled", j.jenkins.getQueue().getItem(project)); - assertNull("Build should not be scheduled", project.getBuildByNumber(2)); - - // Check executors health after a timeout - for (def exec : slave.toComputer().getExecutors()) { - assertTrue("Executor is dead: "+exec, exec.isActive()); - } - } - - public static final class NullDefaultValueParameterDefinition extends SimpleParameterDefinition { - - /*package*/ NullDefaultValueParameterDefinition() { - super("FOO", "Always null default value"); - } - - @Override - public ParameterValue createValue(String value) { - return new StringParameterValue("FOO", "BAR"); - } - - @Override - public ParameterValue createValue(StaplerRequest req, JSONObject jo) { - return createValue("BAR"); - } - - @Override - public ParameterValue getDefaultParameterValue() { - return null; // Equals to super.getDefaultParameterValue(); - } - - @Extension - public static class DescriptorImpl extends ParameterDescriptor {} - } -} diff --git a/test/src/test/java/hudson/cli/BuildCommand2Test.java b/test/src/test/java/hudson/cli/BuildCommand2Test.java deleted file mode 100644 index 1f5366e1bb..0000000000 --- a/test/src/test/java/hudson/cli/BuildCommand2Test.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * The MIT License - * - * Copyright 2017 CloudBees, Inc. - * - * 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.cli; - -import hudson.Launcher; -import hudson.model.AbstractBuild; -import hudson.model.BuildListener; -import hudson.model.FileParameterDefinition; -import hudson.model.FreeStyleBuild; -import hudson.model.FreeStyleProject; -import hudson.model.ParametersDefinitionProperty; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import org.junit.ClassRule; -import org.junit.Test; -import static org.junit.Assert.*; -import org.junit.Rule; -import org.jvnet.hudson.test.BuildWatcher; -import org.jvnet.hudson.test.Issue; -import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.TestBuilder; - -public class BuildCommand2Test { - - @ClassRule - public static BuildWatcher buildWatcher = new BuildWatcher(); - - @Rule - public JenkinsRule r = new JenkinsRule(); - - @Issue("JENKINS-41745") - @Test - public void fileParameter() throws Exception { - FreeStyleProject p = r.createFreeStyleProject("myjob"); - p.addProperty(new ParametersDefinitionProperty(new FileParameterDefinition("file", null))); - p.getBuildersList().add(new TestBuilder() { - @Override - public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { - listener.getLogger().println("Found in my workspace: " + build.getWorkspace().child("file").readToString()); - return true; - } - }); - assertThat(new CLICommandInvoker(r, "build"). - withStdin(new ByteArrayInputStream("uploaded content here".getBytes())). - invokeWithArgs("-f", "-p", "file=", "myjob"), - CLICommandInvoker.Matcher.succeeded()); - FreeStyleBuild b = p.getBuildByNumber(1); - assertNotNull(b); - r.assertLogContains("uploaded content here", b); - } - -} diff --git a/test/src/test/java/hudson/cli/BuildCommandTest.java b/test/src/test/java/hudson/cli/BuildCommandTest.java new file mode 100644 index 0000000000..a55d7c1fe9 --- /dev/null +++ b/test/src/test/java/hudson/cli/BuildCommandTest.java @@ -0,0 +1,313 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, Inc. + * + * 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.cli; + +import hudson.Extension; +import hudson.Launcher; +import static hudson.cli.CLICommandInvoker.Matcher.*; +import hudson.model.AbstractBuild; +import hudson.model.Action; +import hudson.model.BuildListener; +import hudson.model.Executor; +import hudson.model.FileParameterDefinition; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.ParameterDefinition.ParameterDescriptor; +import hudson.model.ParameterValue; +import hudson.model.ParametersAction; +import hudson.model.ParametersDefinitionProperty; +import hudson.model.Queue.QueueDecisionHandler; +import hudson.model.Queue.Task; +import hudson.model.SimpleParameterDefinition; +import hudson.model.StringParameterDefinition; +import hudson.model.StringParameterValue; +import hudson.model.TopLevelItem; +import hudson.slaves.DumbSlave; +import hudson.tasks.Shell; +import hudson.util.OneShotEvent; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import net.sf.json.JSONObject; +import org.apache.commons.io.output.TeeOutputStream; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.CaptureEnvironmentBuilder; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestBuilder; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.StaplerRequest; + +/** + * {@link BuildCommand} test. + */ +public class BuildCommandTest { + + @ClassRule + public static BuildWatcher buildWatcher = new BuildWatcher(); + + @Rule + public JenkinsRule j = new JenkinsRule(); + + /** + * Just schedules a build and return. + */ + @Test + public void async() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + OneShotEvent started = new OneShotEvent(); + OneShotEvent completed = new OneShotEvent(); + p.getBuildersList().add(new TestBuilder() { + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + started.signal(); + completed.block(); + return true; + } + }); + + // this should be asynchronous + try (CLI cli = new CLI(j.getURL())) { + assertEquals(0, cli.execute("build", p.getName())); + started.block(); + assertTrue(p.getBuildByNumber(1).isBuilding()); + completed.signal(); + } + } + + /** + * Tests synchronous execution. + */ + @Test + public void sync() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + p.getBuildersList().add(new Shell("sleep 3")); + + try (CLI cli = new CLI(j.getURL())) { + cli.execute("build", "-s", p.getName()); + assertFalse(p.getBuildByNumber(1).isBuilding()); + } + } + + /** + * Tests synchronous execution with retried verbose output + */ + @Test + public void syncWOutputStreaming() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + p.getBuildersList().add(new Shell("sleep 3")); + + try (CLI cli = new CLI(j.getURL())) { + cli.execute("build", "-s", "-v", "-r", "5", p.getName()); + assertFalse(p.getBuildByNumber(1).isBuilding()); + } + } + + @Test + public void parameters() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + p.addProperty(new ParametersDefinitionProperty(new StringParameterDefinition("key", null))); + + try (CLI cli = new CLI(j.getURL())) { + cli.execute("build", "-s", "-p", "key=foobar", p.getName()); + FreeStyleBuild b = j.assertBuildStatusSuccess(p.getBuildByNumber(1)); + assertEquals("foobar", b.getAction(ParametersAction.class).getParameter("key").getValue()); + } + } + + @Test + public void defaultParameters() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + p.addProperty(new ParametersDefinitionProperty(new StringParameterDefinition("key", "default"), new StringParameterDefinition("key2", "default2"))); + + try (CLI cli = new CLI(j.getURL())) { + cli.execute("build", "-s", "-p", "key=foobar", p.getName()); + FreeStyleBuild b = j.assertBuildStatusSuccess(p.getBuildByNumber(1)); + assertEquals("foobar", b.getAction(ParametersAction.class).getParameter("key").getValue()); + assertEquals("default2", b.getAction(ParametersAction.class).getParameter("key2").getValue()); + } + } + + // TODO randomly fails: Started test0 #1 + @Test + public void consoleOutput() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + try (CLI cli = new CLI(j.getURL())) { + ByteArrayOutputStream o = new ByteArrayOutputStream(); + cli.execute(Arrays.asList("build", "-s", "-v", p.getName()), System.in, new TeeOutputStream(System.out, o), System.err); + j.assertBuildStatusSuccess(p.getBuildByNumber(1)); + assertThat(o.toString(), allOf(containsString("Started from command line by anonymous"), containsString("Finished: SUCCESS"))); + } + } + + // TODO randomly fails: Started test0 #1 + @Test + public void consoleOutputWhenBuildSchedulingRefused() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + try (CLI cli = new CLI(j.getURL())) { + ByteArrayOutputStream o = new ByteArrayOutputStream(); + cli.execute(Arrays.asList("build", "-s", "-v", p.getName()), System.in, System.out, new TeeOutputStream(System.err, o)); + assertThat(o.toString(), containsString(BuildCommand.BUILD_SCHEDULING_REFUSED)); + } + } + // <=> + @TestExtension("consoleOutputWhenBuildSchedulingRefused") + public static class UnschedulingVetoer extends QueueDecisionHandler { + @Override + public boolean shouldSchedule(Task task, List actions) { + return false; + } + } + + @Test + public void refuseToBuildDisabledProject() throws Exception { + FreeStyleProject project = j.createFreeStyleProject("the-project"); + project.disable(); + CLICommandInvoker invoker = new CLICommandInvoker(j, new BuildCommand()); + CLICommandInvoker.Result result = invoker.invokeWithArgs("the-project"); + + assertThat(result, failedWith(4)); + assertThat(result.stderr(), containsString("ERROR: Cannot build the-project because it is disabled.")); + assertNull("Project should not be built", project.getBuildByNumber(1)); + } + + @Test + public void refuseToBuildNewlyCopiedProject() throws Exception { + FreeStyleProject original = j.createFreeStyleProject("original"); + FreeStyleProject newOne = (FreeStyleProject) j.jenkins.copy(original, "new-one"); + CLICommandInvoker invoker = new CLICommandInvoker(j, new BuildCommand()); + CLICommandInvoker.Result result = invoker.invokeWithArgs("new-one"); + + assertThat(result, failedWith(4)); + assertThat(result.stderr(), containsString("ERROR: Cannot build new-one because its configuration has not been saved.")); + assertNull("Project should not be built", newOne.getBuildByNumber(1)); + } + + @Test + public void correctlyParseMapValuesContainingEqualsSign() throws Exception { + FreeStyleProject project = j.createFreeStyleProject("the-project"); + project.addProperty(new ParametersDefinitionProperty(new StringParameterDefinition("expr", null))); + + CLICommandInvoker invoker = new CLICommandInvoker(j, new BuildCommand()); + CLICommandInvoker.Result result = invoker.invokeWithArgs("the-project", "-p", "expr=a=b", "-s"); + + assertThat(result, succeeded()); + assertEquals("a=b", project.getBuildByNumber(1).getBuildVariables().get("expr")); + } + + @Issue("JENKINS-15094") + @Test + public void executorsAliveOnParameterWithNullDefaultValue() throws Exception { + DumbSlave slave = j.createSlave(); + FreeStyleProject project = j.createFreeStyleProject("foo"); + project.setAssignedNode(slave); + + // Create test parameter with Null default value + NullDefaultValueParameterDefinition nullDefaultDefinition = new NullDefaultValueParameterDefinition(); + ParametersDefinitionProperty pdp = new ParametersDefinitionProperty( + new StringParameterDefinition("string", "defaultValue", "description"), + nullDefaultDefinition); + project.addProperty(pdp); + CaptureEnvironmentBuilder builder = new CaptureEnvironmentBuilder(); + project.getBuildersList().add(builder); + + // Warmup + j.buildAndAssertSuccess(project); + + for (Executor exec : slave.toComputer().getExecutors()) { + assertTrue("Executor has died before the test start: " + exec, exec.isActive()); + } + + // Create CLI & run command + CLICommandInvoker invoker = new CLICommandInvoker(j, new BuildCommand()); + CLICommandInvoker.Result result = invoker.invokeWithArgs("foo", "-p", "string=value"); + assertThat(result, failedWith(2)); + assertThat(result.stderr(), containsString("ERROR: No default value for the parameter \'FOO\'.")); + + Thread.sleep(5000); // Give the job 5 seconds to be submitted + assertNull("Build should not be scheduled", j.jenkins.getQueue().getItem(project)); + assertNull("Build should not be scheduled", project.getBuildByNumber(2)); + + // Check executors health after a timeout + for (Executor exec : slave.toComputer().getExecutors()) { + assertTrue("Executor is dead: " + exec, exec.isActive()); + } + } + + public static final class NullDefaultValueParameterDefinition extends SimpleParameterDefinition { + + /*package*/ NullDefaultValueParameterDefinition() { + super("FOO", "Always null default value"); + } + + @Override + public ParameterValue createValue(String value) { + return new StringParameterValue("FOO", "BAR"); + } + + @Override + public ParameterValue createValue(StaplerRequest req, JSONObject jo) { + return createValue("BAR"); + } + + @Override + public ParameterValue getDefaultParameterValue() { + return null; // Equals to super.getDefaultParameterValue(); + } + + @Extension + public static class DescriptorImpl extends ParameterDescriptor {} + + } + + @Issue("JENKINS-41745") + @Test + public void fileParameter() throws Exception { + FreeStyleProject p = j.createFreeStyleProject("myjob"); + p.addProperty(new ParametersDefinitionProperty(new FileParameterDefinition("file", null))); + p.getBuildersList().add(new TestBuilder() { + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + listener.getLogger().println("Found in my workspace: " + build.getWorkspace().child("file").readToString()); + return true; + } + }); + assertThat(new CLICommandInvoker(j, "build"). + withStdin(new ByteArrayInputStream("uploaded content here".getBytes())). + invokeWithArgs("-f", "-p", "file=", "myjob"), + CLICommandInvoker.Matcher.succeeded()); + FreeStyleBuild b = p.getBuildByNumber(1); + assertNotNull(b); + j.assertLogContains("uploaded content here", b); + } + +} From 26c4ab77db3802ac256ab5f0aa5a9cf38aaecc03 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 16:54:17 -0500 Subject: [PATCH 37/85] SetBuildParameterCommandTest --- .../cli/SetBuildParameterCommandTest.groovy | 82 ----------------- .../cli/SetBuildParameterCommandTest.java | 88 +++++++++++++++++++ 2 files changed, 88 insertions(+), 82 deletions(-) delete mode 100644 test/src/test/groovy/hudson/cli/SetBuildParameterCommandTest.groovy create mode 100644 test/src/test/java/hudson/cli/SetBuildParameterCommandTest.java diff --git a/test/src/test/groovy/hudson/cli/SetBuildParameterCommandTest.groovy b/test/src/test/groovy/hudson/cli/SetBuildParameterCommandTest.groovy deleted file mode 100644 index dcfba02ed8..0000000000 --- a/test/src/test/groovy/hudson/cli/SetBuildParameterCommandTest.groovy +++ /dev/null @@ -1,82 +0,0 @@ -package hudson.cli - -import hudson.Functions -import hudson.Launcher -import hudson.model.AbstractBuild -import hudson.model.BuildListener -import hudson.model.ParametersAction -import hudson.model.ParametersDefinitionProperty -import hudson.model.ParameterDefinition -import hudson.model.Result -import hudson.model.StringParameterDefinition -import hudson.tasks.BatchFile -import hudson.tasks.Builder -import hudson.tasks.Shell -import jenkins.model.JenkinsLocationConfiguration -import org.junit.Assert -import org.junit.ClassRule -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.BuildWatcher -import org.jvnet.hudson.test.JenkinsRule -import org.jvnet.hudson.test.TestBuilder - -/** - * @author Kohsuke Kawaguchi - */ -public class SetBuildParameterCommandTest { - @Rule - public JenkinsRule j = new JenkinsRule(); - - @ClassRule - public static BuildWatcher buildWatcher = new BuildWatcher(); - - @Test - public void cli() { - JenkinsLocationConfiguration.get().url = j.URL; - - def p = j.createFreeStyleProject(); - p.buildersList.add(new TestBuilder() { - @Override - boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { - def jar = j.jenkins.servletContext.getResource("/WEB-INF/jenkins-cli.jar") - build.workspace.child("cli.jar").copyFrom(jar); - - return true; - } - }); - List pd = [new StringParameterDefinition("a", ""), new StringParameterDefinition("b", "")]; - p.addProperty(new ParametersDefinitionProperty(pd)) - p.buildersList.add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")) - p.buildersList.add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter a x")) - p.buildersList.add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter b y")) - - def r = [:]; - - def b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - b.getAction(ParametersAction.class).parameters.each { v -> r[v.name]=v.value } - - assert r.equals(["a":"x", "b":"y"]); - - if (Functions.isWindows()) { - p.buildersList.add(new BatchFile("set BUILD_NUMBER=1\r\njava -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")) - } else { - p.buildersList.add(new Shell("BUILD_NUMBER=1 java -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")) - } - def b2 = j.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); - j.assertLogContains("#1 is not currently being built", b2) - r = [:]; - b.getAction(ParametersAction.class).parameters.each { v -> r[v.name]=v.value } - assert r.equals(["a":"x", "b":"y"]); - } - - //TODO: determine if this should be pulled out into JenkinsRule or something - /** - * Create a script based builder (either Shell or BatchFile) depending on platform - * @param script the contents of the script to run - * @return A Builder instance of either Shell or BatchFile - */ - private Builder createScriptBuilder(String script) { - return Functions.isWindows() ? new BatchFile(script) : new Shell(script); - } -} diff --git a/test/src/test/java/hudson/cli/SetBuildParameterCommandTest.java b/test/src/test/java/hudson/cli/SetBuildParameterCommandTest.java new file mode 100644 index 0000000000..937abe4628 --- /dev/null +++ b/test/src/test/java/hudson/cli/SetBuildParameterCommandTest.java @@ -0,0 +1,88 @@ +package hudson.cli; + +import com.google.common.collect.ImmutableMap; +import hudson.Functions; +import hudson.Launcher; +import hudson.model.AbstractBuild; +import hudson.model.BuildListener; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.ParametersAction; +import hudson.model.ParametersDefinitionProperty; +import hudson.model.ParameterDefinition; +import hudson.model.Result; +import hudson.model.StringParameterDefinition; +import hudson.tasks.BatchFile; +import hudson.tasks.Builder; +import hudson.tasks.Shell; +import java.io.IOException; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import static org.junit.Assert.*; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestBuilder; + +public class SetBuildParameterCommandTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @ClassRule + public static BuildWatcher buildWatcher = new BuildWatcher(); + + @Test + public void cli() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + p.getBuildersList().add(new TestBuilder() { + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + URL jar = j.jenkins.servletContext.getResource("/WEB-INF/jenkins-cli.jar"); + build.getWorkspace().child("cli.jar").copyFrom(jar); + + return true; + } + }); + List pd = Arrays.asList(new StringParameterDefinition("a", ""), new StringParameterDefinition("b", "")); + p.addProperty(new ParametersDefinitionProperty(pd)); + p.getBuildersList().add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")); + p.getBuildersList().add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter a x")); + p.getBuildersList().add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter b y")); + + Map r = new TreeMap<>(); + + FreeStyleBuild b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + b.getAction(ParametersAction.class).getParameters().forEach(v -> r.put(v.getName(), v.getValue())); + + assertEquals(ImmutableMap.of("a", "x", "b", "y"), r); + + if (Functions.isWindows()) { + p.getBuildersList().add(new BatchFile("set BUILD_NUMBER=1\r\njava -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")); + } else { + p.getBuildersList().add(new Shell("BUILD_NUMBER=1 java -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")); + } + FreeStyleBuild b2 = j.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); + j.assertLogContains("#1 is not currently being built", b2); + r.clear(); + b.getAction(ParametersAction.class).getParameters().forEach(v -> r.put(v.getName(), v.getValue())); + assertEquals(ImmutableMap.of("a", "x", "b", "y"), r); + } + + //TODO: determine if this should be pulled out into JenkinsRule or something + /** + * Create a script based builder (either Shell or BatchFile) depending on + * platform + * @param script the contents of the script to run + * @return A Builder instance of either Shell or BatchFile + */ + private Builder createScriptBuilder(String script) { + return Functions.isWindows() ? new BatchFile(script) : new Shell(script); + } + +} From ba44ed593def941cc1184c740826ad8504e4d81f Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 17:00:05 -0500 Subject: [PATCH 38/85] AbstractItemTest --- .../hudson/model/AbstractItemTest.groovy | 42 ------------------ .../java/hudson/model/AbstractItemTest.java | 43 +++++++++++++++++++ 2 files changed, 43 insertions(+), 42 deletions(-) delete mode 100644 test/src/test/groovy/hudson/model/AbstractItemTest.groovy create mode 100644 test/src/test/java/hudson/model/AbstractItemTest.java diff --git a/test/src/test/groovy/hudson/model/AbstractItemTest.groovy b/test/src/test/groovy/hudson/model/AbstractItemTest.groovy deleted file mode 100644 index 8c7cbe8ef4..0000000000 --- a/test/src/test/groovy/hudson/model/AbstractItemTest.groovy +++ /dev/null @@ -1,42 +0,0 @@ -package hudson.model - -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.JenkinsRule - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class AbstractItemTest { - @Rule - public JenkinsRule j = new JenkinsRule() - - /** - * Tests the reload functionality - */ - @Test - void reload() { - def jenkins = j.jenkins; - def p = jenkins.createProject(FreeStyleProject.class,"foo"); - p.description = "Hello World"; - - def b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - b.description = "This is my build" - - // update on disk representation - def f = p.configFile.file - f.text = f.text.replaceAll("Hello World","Good Evening"); - - // reload away - p.doReload() - - assert p.description == "Good Evening"; - - def b2 = p.getBuildByNumber(1) - - assert !b.is(b2); // should be different object - assert b.description == b2.description // but should have the same properties - } -} diff --git a/test/src/test/java/hudson/model/AbstractItemTest.java b/test/src/test/java/hudson/model/AbstractItemTest.java new file mode 100644 index 0000000000..267f8f3524 --- /dev/null +++ b/test/src/test/java/hudson/model/AbstractItemTest.java @@ -0,0 +1,43 @@ +package hudson.model; + +import java.io.File; +import jenkins.model.Jenkins; +import org.apache.commons.io.FileUtils; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +public class AbstractItemTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + /** + * Tests the reload functionality + */ + @Test + public void reload() throws Exception { + Jenkins jenkins = j.jenkins; + FreeStyleProject p = jenkins.createProject(FreeStyleProject.class, "foo"); + p.setDescription("Hello World"); + + FreeStyleBuild b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + b.setDescription("This is my build"); + + // update on disk representation + File f = p.getConfigFile().getFile(); + FileUtils.writeStringToFile(f, FileUtils.readFileToString(f).replaceAll("Hello World", "Good Evening")); + + // reload away + p.doReload(); + + assertEquals("Good Evening", p.getDescription()); + + FreeStyleBuild b2 = p.getBuildByNumber(1); + + assertNotEquals(b, b2); // should be different object + assertEquals(b.getDescription(), b2.getDescription()); // but should have the same properties + } + +} From ea2e4f5788449535767809bcdbc7c32f3706129a Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 17:37:37 -0500 Subject: [PATCH 39/85] AbstractProjectTest --- .../hudson/model/AbstractProjectTest.groovy | 596 ---------------- .../hudson/model/AbstractProject2Test.java | 73 -- .../hudson/model/AbstractProjectTest.java | 646 ++++++++++++++++++ 3 files changed, 646 insertions(+), 669 deletions(-) delete mode 100644 test/src/test/groovy/hudson/model/AbstractProjectTest.groovy delete mode 100644 test/src/test/java/hudson/model/AbstractProject2Test.java create mode 100644 test/src/test/java/hudson/model/AbstractProjectTest.java diff --git a/test/src/test/groovy/hudson/model/AbstractProjectTest.groovy b/test/src/test/groovy/hudson/model/AbstractProjectTest.groovy deleted file mode 100644 index a8941e0c66..0000000000 --- a/test/src/test/groovy/hudson/model/AbstractProjectTest.groovy +++ /dev/null @@ -1,596 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi - * - * 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.model; - -import com.gargoylesoftware.htmlunit.ElementNotFoundException -import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; -import com.gargoylesoftware.htmlunit.HttpMethod -import com.gargoylesoftware.htmlunit.WebRequest; -import hudson.tasks.BatchFile; -import hudson.tasks.BuildStepMonitor; -import hudson.tasks.Recorder; -import com.gargoylesoftware.htmlunit.html.HtmlPage -import hudson.maven.MavenModuleSet; -import hudson.security.*; -import hudson.tasks.Shell; -import hudson.scm.NullSCM; -import hudson.scm.SCM -import hudson.scm.SCMDescriptor -import hudson.Launcher; -import hudson.FilePath; -import hudson.Functions; -import hudson.Util; -import hudson.tasks.ArtifactArchiver -import hudson.triggers.SCMTrigger; -import hudson.triggers.TimerTrigger -import hudson.triggers.Trigger -import hudson.triggers.TriggerDescriptor; -import hudson.util.StreamTaskListener; -import hudson.util.OneShotEvent -import jenkins.model.Jenkins -import org.junit.Rule -import org.junit.Test; -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.TestExtension; -import org.jvnet.hudson.test.recipes.PresetData; -import org.jvnet.hudson.test.recipes.PresetData.DataSet -import org.apache.commons.io.FileUtils; -import org.junit.Assume; -import org.jvnet.hudson.test.MockFolder -import org.kohsuke.args4j.CmdLineException - -import static org.junit.Assert.fail -import static org.junit.Assert.assertEquals; - -/** - * @author Kohsuke Kawaguchi - */ -public class AbstractProjectTest { - - @Rule public JenkinsRule j = new JenkinsRule(); - - @Test - public void testConfigRoundtrip() { - def project = j.createFreeStyleProject(); - def l = j.jenkins.getLabel("foo && bar"); - project.assignedLabel = l; - j.configRoundtrip((Item) project); - - assert l == project.getAssignedLabel(); - } - - /** - * Tests the workspace deletion. - */ - @Test - public void testWipeWorkspace() { - def project = j.createFreeStyleProject(); - project.buildersList.add(Functions.isWindows() ? new BatchFile("echo hello") : new Shell("echo hello")); - - def b = project.scheduleBuild2(0).get(); - - assert b.workspace.exists(): "Workspace should exist by now"; - - project.doDoWipeOutWorkspace(); - - assert !b.workspace.exists(): "Workspace should be gone by now"; - } - - /** - * Makes sure that the workspace deletion is protected. - */ - @Test - @PresetData(DataSet.NO_ANONYMOUS_READACCESS) - public void testWipeWorkspaceProtected() { - def project = j.createFreeStyleProject(); - project.getBuildersList().add(Functions.isWindows() ? new BatchFile("echo hello") : new Shell("echo hello")); - - def b = project.scheduleBuild2(0).get(); - - assert b.getWorkspace().exists(): "Workspace should exist by now"; - - // make sure that the action link is protected - com.gargoylesoftware.htmlunit.WebClient wc = j.createWebClient(); - try { - wc.getPage(new WebRequest(new URL(wc.getContextPath() + project.getUrl() + "doWipeOutWorkspace"), HttpMethod.POST)); - fail("Expected HTTP status code 403") - } catch (FailingHttpStatusCodeException e) { - assertEquals(HttpURLConnection.HTTP_FORBIDDEN, e.getStatusCode()); - } - } - - /** - * Makes sure that the workspace deletion link is not provided - * when the user doesn't have an access. - */ - @Test - @PresetData(DataSet.ANONYMOUS_READONLY) - public void testWipeWorkspaceProtected2() { - ((GlobalMatrixAuthorizationStrategy) j.jenkins.getAuthorizationStrategy()).add(AbstractProject.WORKSPACE,"anonymous"); - - // make sure that the deletion is protected in the same way - testWipeWorkspaceProtected(); - - // there shouldn't be any "wipe out workspace" link for anonymous user - def webClient = j.createWebClient(); - HtmlPage page = webClient.getPage(j.jenkins.getItem("test0")); - - page = (HtmlPage)page.getAnchorByText("Workspace").click(); - try { - String wipeOutLabel = ResourceBundle.getBundle("hudson/model/AbstractProject/sidepanel").getString("Wipe Out Workspace"); - page.getAnchorByText(wipeOutLabel); - fail("shouldn't find a link"); - } catch (ElementNotFoundException e) { - // OK - } - } - - /** - * Tests the <optionalBlock @field> round trip behavior by using {@link AbstractProject#concurrentBuild} - */ - @Test - public void testOptionalBlockDataBindingRoundtrip() { - def p = j.createFreeStyleProject(); - [true,false].each { b -> - p.concurrentBuild = b; - j.submit(j.createWebClient().getPage(p,"configure").getFormByName("config")); - assert b==p.isConcurrentBuild(); - } - } - - /** - * Tests round trip configuration of the blockBuildWhenUpstreamBuilding field - */ - @Test - @Issue("JENKINS-4423") - public void testConfiguringBlockBuildWhenUpstreamBuildingRoundtrip() { - def p = j.createFreeStyleProject(); - p.blockBuildWhenUpstreamBuilding = false; - - def form = j.createWebClient().getPage(p, "configure").getFormByName("config"); - def input = form.getInputByName("blockBuildWhenUpstreamBuilding"); - assert !input.isChecked(): "blockBuildWhenUpstreamBuilding check box is checked."; - - input.setChecked(true); - j.submit(form); - assert p.blockBuildWhenUpstreamBuilding: "blockBuildWhenUpstreamBuilding was not updated from configuration form"; - - form = j.createWebClient().getPage(p, "configure").getFormByName("config"); - input = form.getInputByName("blockBuildWhenUpstreamBuilding"); - assert input.isChecked(): "blockBuildWhenUpstreamBuilding check box is not checked."; - } - - /** - * Unless the concurrent build option is enabled, polling and build should be mutually exclusive - * to avoid allocating unnecessary workspaces. - */ - @Test - @Issue("JENKINS-4202") - public void testPollingAndBuildExclusion() { - final OneShotEvent sync = new OneShotEvent(); - - final FreeStyleProject p = j.createFreeStyleProject(); - def b1 = j.buildAndAssertSuccess(p); - - p.scm = new NullSCM() { - @Override - public boolean pollChanges(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener) { - try { - sync.block(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - return true; - } - - /** - * Don't write 'this', so that subtypes can be implemented as anonymous class. - */ - private Object writeReplace() { return new Object(); } - - @Override public boolean requiresWorkspaceForPolling() { - return true; - } - @Override public SCMDescriptor getDescriptor() { - return new SCMDescriptor(null) {}; - } - }; - Thread t = new Thread() { - @Override public void run() { - p.pollSCMChanges(StreamTaskListener.fromStdout()); - } - }; - try { - t.start(); - def f = p.scheduleBuild2(0); - - // add a bit of delay to make sure that the blockage is happening - Thread.sleep(3000); - - // release the polling - sync.signal(); - - def b2 = j.assertBuildStatusSuccess(f); - - // they should have used the same workspace. - assert b1.workspace == b2.workspace; - } finally { - t.interrupt(); - } - } - - @Test - @Issue("JENKINS-1986") - public void testBuildSymlinks() { - Assume.assumeFalse("If we're on Windows, don't bother doing this", Functions.isWindows()); - - def job = j.createFreeStyleProject(); - job.buildersList.add(new Shell("echo \"Build #\$BUILD_NUMBER\"\n")); - def build = job.scheduleBuild2(0, new Cause.UserCause()).get(); - File lastSuccessful = new File(job.rootDir, "lastSuccessful"), - lastStable = new File(job.rootDir, "lastStable"); - // First build creates links - assertSymlinkForBuild(lastSuccessful, 1); - assertSymlinkForBuild(lastStable, 1); - FreeStyleBuild build2 = job.scheduleBuild2(0, new Cause.UserCause()).get(); - // Another build updates links - assertSymlinkForBuild(lastSuccessful, 2); - assertSymlinkForBuild(lastStable, 2); - // Delete latest build should update links - build2.delete(); - assertSymlinkForBuild(lastSuccessful, 1); - assertSymlinkForBuild(lastStable, 1); - // Delete all builds should remove links - build.delete(); - assert !lastSuccessful.exists(): "lastSuccessful link should be removed"; - assert !lastStable.exists(): "lastStable link should be removed"; - } - - private static void assertSymlinkForBuild(File file, int buildNumber) - throws IOException, InterruptedException { - assert file.exists(): "should exist and point to something that exists"; - assert Util.isSymlink(file): "should be symlink"; - String s = FileUtils.readFileToString(new File(file, "log")); - assert s.contains("Build #" + buildNumber + "\n") : "link should point to build #$buildNumber, but link was: ${Util.resolveSymlink(file, TaskListener.NULL)}\nand log was:\n$s"; - } - - @Test - @Issue("JENKINS-2543") - public void testSymlinkForPostBuildFailure() { - Assume.assumeFalse("If we're on Windows, don't bother doing this", Functions.isWindows()); - - // Links should be updated after post-build actions when final build result is known - def job = j.createFreeStyleProject(); - job.buildersList.add(new Shell("echo \"Build #\$BUILD_NUMBER\"\n")); - def build = job.scheduleBuild2(0, new Cause.UserCause()).get(); - assert Result.SUCCESS == build.result; - File lastSuccessful = new File(job.rootDir, "lastSuccessful"), - lastStable = new File(job.rootDir, "lastStable"); - // First build creates links - assertSymlinkForBuild(lastSuccessful, 1); - assertSymlinkForBuild(lastStable, 1); - // Archive artifacts that don't exist to create failure in post-build action - job.publishersList.add(new ArtifactArchiver("*.foo", "", false, false)); - build = job.scheduleBuild2(0, new Cause.UserCause()).get(); - assert Result.FAILURE == build.getResult(); - // Links should not be updated since build failed - assertSymlinkForBuild(lastSuccessful, 1); - assertSymlinkForBuild(lastStable, 1); - } - - /* TODO too slow, seems capable of causing testWorkspaceLock to time out: - @Test - @Issue("JENKINS-15156") - public void testGetBuildAfterGC() { - FreeStyleProject job = j.createFreeStyleProject(); - job.scheduleBuild2(0, new Cause.UserIdCause()).get(); - j.jenkins.queue.clearLeftItems(); - MemoryAssert.assertGC(new WeakReference(job.getLastBuild())); - assert job.lastBuild != null; - } - */ - - @Test - @Issue("JENKINS-17137") - public void testExternalBuildDirectorySymlinks() { - Assume.assumeFalse(Functions.isWindows()); // symlinks may not be available - def form = j.createWebClient().goTo("configure").getFormByName("config"); - def builds = j.createTmpDir(); - form.getInputByName("_.rawBuildsDir").valueAttribute = builds.toString() + "/\${ITEM_FULL_NAME}"; - j.submit(form); - assert builds.toString() + "/\${ITEM_FULL_NAME}" == j.jenkins.getRawBuildsDir(); - def p = j.jenkins.createProject(MockFolder.class, "d").createProject(FreeStyleProject.class, "p"); - def b1 = p.scheduleBuild2(0).get(); - def link = new File(p.rootDir, "lastStable"); - assert link.exists(); - assert b1.rootDir.absolutePath == resolveAll(link).absolutePath; - def b2 = p.scheduleBuild2(0).get(); - assert link.exists(); - assert b2.rootDir.absolutePath == resolveAll(link).absolutePath; - b2.delete(); - assert link.exists(); - assert b1.rootDir.absolutePath == resolveAll(link).absolutePath; - b1.delete(); - assert !link.exists(); - } - - private File resolveAll(File link) throws InterruptedException, IOException { - while (true) { - File f = Util.resolveSymlinkToFile(link); - if (f==null) return link; - link = f; - } - } - - @Test - @Issue("JENKINS-17138") - public void testExternalBuildDirectoryRenameDelete() { - def form = j.createWebClient().goTo("configure").getFormByName("config"); - def builds = j.createTmpDir(); - form.getInputByName("_.rawBuildsDir").setValueAttribute(builds.toString() + "/\${ITEM_FULL_NAME}"); - j.submit(form); - assert builds.toString() + "/\${ITEM_FULL_NAME}" == j.jenkins.rawBuildsDir; - def p = j.jenkins.createProject(MockFolder.class, "d").createProject(FreeStyleProject.class, "prj"); - def b = p.scheduleBuild2(0).get(); - def oldBuildDir = new File(builds, "d/prj"); - assert new File(oldBuildDir, b.id) == b.rootDir; - assert b.getRootDir().isDirectory(); - p.renameTo("proj"); - def newBuildDir = new File(builds, "d/proj"); - assert new File(newBuildDir, b.id) == b.rootDir; - assert b.rootDir.isDirectory(); - p.delete(); - assert !b.rootDir.isDirectory(); - } - - @Test - @Issue("JENKINS-18678") - public void testRenameJobLostBuilds() throws Exception { - def p = j.createFreeStyleProject("initial"); - j.assertBuildStatusSuccess(p.scheduleBuild2(0)); - assertEquals(1, p.getBuilds().size()); - p.renameTo("edited"); - p._getRuns().purgeCache(); - assertEquals(1, p.getBuilds().size()); - def d = j.jenkins.createProject(MockFolder.class, "d"); - Items.move(p, d); - assertEquals(p, j.jenkins.getItemByFullName("d/edited")); - p._getRuns().purgeCache(); - assertEquals(1, p.getBuilds().size()); - d.renameTo("d2"); - p = j.jenkins.getItemByFullName("d2/edited"); - p._getRuns().purgeCache(); - assertEquals(1, p.getBuilds().size()); - } - - @Test - @Issue("JENKINS-17575") - public void testDeleteRedirect() { - j.createFreeStyleProject("j1"); - assert "" == deleteRedirectTarget("job/j1"); - j.createFreeStyleProject("j2"); - Jenkins.getInstance().addView(new AllView("v1")); - assert "view/v1/" == deleteRedirectTarget("view/v1/job/j2"); - MockFolder d = Jenkins.getInstance().createProject(MockFolder.class, "d"); - d.addView(new AllView("v2")); - ["j3","j4","j5"].each { n -> d.createProject(FreeStyleProject.class, n) } - assert "job/d/" == deleteRedirectTarget("job/d/job/j3"); - assert "job/d/view/v2/" == deleteRedirectTarget("job/d/view/v2/job/j4"); - assert "view/v1/job/d/" == deleteRedirectTarget("view/v1/job/d/job/j5"); - assert "view/v1/" == deleteRedirectTarget("view/v1/job/d"); // JENKINS-23375 - } - - private String deleteRedirectTarget(String job) { - def wc = j.createWebClient(); - String base = wc.getContextPath(); - String loc = wc.getPage(wc.addCrumb(new WebRequest(new URL(base + job + "/doDelete"), HttpMethod.POST))).getUrl().toString(); - assert loc.startsWith(base): loc; - return loc.substring(base.length()); - } - - @Test - @Issue("JENKINS-18407") - public void testQueueSuccessBehavior() { - // prevent any builds to test the behaviour - j.jenkins.numExecutors = 0; - - def p = j.createFreeStyleProject() - def f = p.scheduleBuild2(0) - assert f!=null; - def g = p.scheduleBuild2(0) - assert f==g; - - p.makeDisabled(true) - assert p.scheduleBuild2(0)==null - } - - /** - * Do the same as {@link #testQueueSuccessBehavior()} but over HTTP - */ - @Test - @Issue("JENKINS-18407") - public void testQueueSuccessBehaviorOverHTTP() { - // prevent any builds to test the behaviour - j.jenkins.numExecutors = 0; - - def p = j.createFreeStyleProject() - def wc = j.createWebClient(); - - def rsp = wc.getPage("${j.getURL()}${p.url}build").webResponse - assert rsp.statusCode==201; - assert rsp.getResponseHeaderValue("Location")!=null; - - def rsp2 = wc.getPage("${j.getURL()}${p.url}build").webResponse - assert rsp2.statusCode==201; - assert rsp.getResponseHeaderValue("Location")==rsp2.getResponseHeaderValue("Location") - - p.makeDisabled(true) - - try { - wc.getPage("${j.getURL()}${p.url}build") - fail(); - } catch (FailingHttpStatusCodeException e) { - // request should fail - } - } - - /** - * We used to store {@link AbstractProject#triggers} as {@link Vector}, so make sure - * we can still read back the configuration from that. - */ - @Test - public void testVectorTriggers() { - AbstractProject p = j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")) - assert p.triggers().size()==1 - def t = p.triggers()[0] - assert t.class==SCMTrigger.class; - assert t.spec=="*/10 * * * *" - } - - @Test - @Issue("JENKINS-18813") - public void testRemoveTrigger() { - AbstractProject p = j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")) - - TriggerDescriptor SCM_TRIGGER_DESCRIPTOR = j.jenkins.getDescriptorOrDie(SCMTrigger.class) - p.removeTrigger(SCM_TRIGGER_DESCRIPTOR); - assert p.triggers().size()==0 - } - - @Test - @Issue("JENKINS-18813") - public void testAddTriggerSameType() { - AbstractProject p = j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")) - - def newTrigger = new SCMTrigger("H/5 * * * *") - p.addTrigger(newTrigger); - - assert p.triggers().size()==1 - def t = p.triggers()[0] - assert t.class==SCMTrigger.class; - assert t.spec=="H/5 * * * *" - } - - @Test - @Issue("JENKINS-18813") - public void testAddTriggerDifferentType() { - AbstractProject p = j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")) - - def newTrigger = new TimerTrigger("20 * * * *") - p.addTrigger(newTrigger); - - assert p.triggers().size()==2 - def t = p.triggers()[1] - assert t == newTrigger - } - - /** - * Trying to POST to config.xml by a different job type should fail. - */ - @Test - public void testConfigDotXmlSubmissionToDifferentType() { - j.jenkins.pluginManager.installDetachedPlugin("javadoc") - j.jenkins.pluginManager.installDetachedPlugin("junit") - j.jenkins.pluginManager.installDetachedPlugin("display-url-api") - j.jenkins.pluginManager.installDetachedPlugin("mailer") - j.jenkins.pluginManager.installDetachedPlugin("maven-plugin") - - j.jenkins.crumbIssuer = null - def p = j.createFreeStyleProject() - - HttpURLConnection con = postConfigDotXml(p, "") - - // this should fail with a type mismatch error - // the error message should report both what was submitted and what was expected - assert con.responseCode == 500 - def msg = con.errorStream.text - println msg - assert msg.contains(FreeStyleProject.class.name) - assert msg.contains(MavenModuleSet.class.name) - - // control. this should work - con = postConfigDotXml(p, "") - assert con.responseCode == 200 - } - - private HttpURLConnection postConfigDotXml(FreeStyleProject p, String xml) { - HttpURLConnection con = new URL(j.getURL(), "job/${p.name}/config.xml").openConnection() - con.requestMethod = "POST" - con.setRequestProperty("Content-Type", "application/xml") - con.doOutput = true - con.outputStream.withStream { s -> - s.write(xml.bytes) - } - return con - } - - @Test - @Issue("JENKINS-27549") - public void testLoadingWithNPEOnTriggerStart() { - AbstractProject project = j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/npeTrigger.xml")) - - assert project.triggers().size() == 1 - } - - @Test - @Issue("JENKINS-30742") - public void testResolveForCLI() { - try { - AbstractProject not_found = AbstractProject.resolveForCLI("never_created"); - fail("Exception should occur before!"); - } catch (CmdLineException e) { - assert e.getMessage().contentEquals("No such job \u2018never_created\u2019 exists."); - } - - AbstractProject project = j.jenkins.createProject(FreeStyleProject.class, "never_created"); - try { - AbstractProject not_found = AbstractProject.resolveForCLI("never_created1"); - fail("Exception should occur before!"); - } catch (CmdLineException e) { - assert e.getMessage().contentEquals("No such job \u2018never_created1\u2019 exists. Perhaps you meant \u2018never_created\u2019?") - } - - } - - static class MockBuildTriggerThrowsNPEOnStart extends Trigger { - @Override - public void start(hudson.model.Item project, boolean newInstance) { throw new NullPointerException(); } - - @Override - public TriggerDescriptor getDescriptor() { - return DESCRIPTOR; - } - - public static final TriggerDescriptor DESCRIPTOR = new DescriptorImpl() - - @TestExtension("testLoadingWithNPEOnTriggerStart") - static class DescriptorImpl extends TriggerDescriptor { - - public boolean isApplicable(hudson.model.Item item) { - return false; - } - } - } -} diff --git a/test/src/test/java/hudson/model/AbstractProject2Test.java b/test/src/test/java/hudson/model/AbstractProject2Test.java deleted file mode 100644 index a5f330ca43..0000000000 --- a/test/src/test/java/hudson/model/AbstractProject2Test.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * The MIT License - * - * Copyright 2017 CloudBees, Inc. - * - * 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.model; - -import hudson.tasks.BuildTrigger; -import java.util.Collections; -import jenkins.model.Jenkins; -import static org.hamcrest.Matchers.*; -import org.junit.Test; -import static org.junit.Assert.*; -import org.junit.Rule; -import org.jvnet.hudson.test.Issue; -import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.MockAuthorizationStrategy; - -// TODO merge with AbstractProjectTest when converted from Groovy to Java -public class AbstractProject2Test { - - @Rule - public JenkinsRule r = new JenkinsRule(); - - @Issue("SECURITY-617") - @Test - public void upstreamDownstreamExportApi() throws Exception { - FreeStyleProject us = r.createFreeStyleProject("upstream-project"); - FreeStyleProject ds = r.createFreeStyleProject("downstream-project"); - us.getPublishersList().add(new BuildTrigger(Collections.singleton(ds), Result.SUCCESS)); - r.jenkins.rebuildDependencyGraph(); - assertEquals(Collections.singletonList(ds), us.getDownstreamProjects()); - assertEquals(Collections.singletonList(us), ds.getUpstreamProjects()); - r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); - r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy(). - grant(Jenkins.READ).everywhere().toEveryone(). - grant(Item.READ).everywhere().to("alice"). - grant(Item.READ).onItems(us).to("bob"). - grant(Item.READ).onItems(ds).to("charlie")); - String api = r.createWebClient().login("alice").goTo(us.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); - System.out.println(api); - assertThat(api, containsString("downstream-project")); - api = r.createWebClient().login("alice").goTo(ds.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); - System.out.println(api); - assertThat(api, containsString("upstream-project")); - api = r.createWebClient().login("bob").goTo(us.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); - System.out.println(api); - assertThat(api, not(containsString("downstream-project"))); - api = r.createWebClient().login("charlie").goTo(ds.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); - System.out.println(api); - assertThat(api, not(containsString("upstream-project"))); - } - -} diff --git a/test/src/test/java/hudson/model/AbstractProjectTest.java b/test/src/test/java/hudson/model/AbstractProjectTest.java new file mode 100644 index 0000000000..308fc12993 --- /dev/null +++ b/test/src/test/java/hudson/model/AbstractProjectTest.java @@ -0,0 +1,646 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi + * + * 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.model; + +import com.gargoylesoftware.htmlunit.ElementNotFoundException; +import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; +import com.gargoylesoftware.htmlunit.HttpMethod; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.WebResponse; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlInput; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import hudson.FilePath; +import hudson.Functions; +import hudson.Launcher; +import hudson.Util; +import hudson.maven.MavenModuleSet; +import hudson.scm.NullSCM; +import hudson.scm.SCM; +import hudson.scm.SCMDescriptor; +import hudson.security.GlobalMatrixAuthorizationStrategy; +import hudson.tasks.ArtifactArchiver; +import hudson.tasks.BatchFile; +import hudson.tasks.BuildTrigger; +import hudson.tasks.Shell; +import hudson.triggers.SCMTrigger; +import hudson.triggers.TimerTrigger; +import hudson.triggers.Trigger; +import hudson.triggers.TriggerDescriptor; +import hudson.util.OneShotEvent; +import hudson.util.StreamTaskListener; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Collections; +import java.util.ResourceBundle; +import java.util.Vector; +import java.util.concurrent.Future; +import jenkins.model.Jenkins; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; +import org.jvnet.hudson.test.MockFolder; +import org.jvnet.hudson.test.TestExtension; +import org.jvnet.hudson.test.TestPluginManager; +import org.jvnet.hudson.test.recipes.PresetData; +import org.jvnet.hudson.test.recipes.PresetData.DataSet; +import org.kohsuke.args4j.CmdLineException; + +public class AbstractProjectTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + public void configRoundtrip() throws Exception { + FreeStyleProject project = j.createFreeStyleProject(); + Label l = j.jenkins.getLabel("foo && bar"); + project.setAssignedLabel(l); + j.configRoundtrip(project); + + assertEquals(l, project.getAssignedLabel()); + } + + /** + * Tests the workspace deletion. + */ + @Test + public void wipeWorkspace() throws Exception { + FreeStyleProject project = j.createFreeStyleProject(); + project.getBuildersList().add(Functions.isWindows() ? new BatchFile("echo hello") : new Shell("echo hello")); + + FreeStyleBuild b = project.scheduleBuild2(0).get(); + + assertTrue("Workspace should exist by now", b.getWorkspace().exists()); + + project.doDoWipeOutWorkspace(); + + assertFalse("Workspace should be gone by now", b.getWorkspace().exists()); + } + + /** + * Makes sure that the workspace deletion is protected. + */ + @Test + @PresetData(DataSet.NO_ANONYMOUS_READACCESS) + public void wipeWorkspaceProtected() throws Exception { + FreeStyleProject project = j.createFreeStyleProject(); + project.getBuildersList().add(Functions.isWindows() ? new BatchFile("echo hello") : new Shell("echo hello")); + + FreeStyleBuild b = project.scheduleBuild2(0).get(); + + assert b.getWorkspace().exists() : "Workspace should exist by now"; + + // make sure that the action link is protected + JenkinsRule.WebClient wc = j.createWebClient(); + try { + wc.getPage(new WebRequest(new URL(wc.getContextPath() + project.getUrl() + "doWipeOutWorkspace"), HttpMethod.POST)); + fail("Expected HTTP status code 403"); + } catch (FailingHttpStatusCodeException e) { + assertEquals(HttpURLConnection.HTTP_FORBIDDEN, e.getStatusCode()); + } + } + + /** + * Makes sure that the workspace deletion link is not provided when the user + * doesn't have an access. + */ + @Test + @PresetData(DataSet.ANONYMOUS_READONLY) + public void wipeWorkspaceProtected2() throws Exception { + ((GlobalMatrixAuthorizationStrategy) j.jenkins.getAuthorizationStrategy()).add(AbstractProject.WORKSPACE, "anonymous"); + + // make sure that the deletion is protected in the same way + wipeWorkspaceProtected(); + + // there shouldn't be any "wipe out workspace" link for anonymous user + JenkinsRule.WebClient webClient = j.createWebClient(); + HtmlPage page = webClient.getPage(j.jenkins.getItem("test0")); + + page = (HtmlPage) page.getAnchorByText("Workspace").click(); + try { + String wipeOutLabel = ResourceBundle.getBundle("hudson/model/AbstractProject/sidepanel").getString("Wipe Out Workspace"); + page.getAnchorByText(wipeOutLabel); + fail("shouldn't find a link"); + } catch (ElementNotFoundException e) { + // OK + } + } + + /** + * Tests the <optionalBlock @field> round trip behavior by using + * {@link AbstractProject#concurrentBuild} + */ + @Test + public void optionalBlockDataBindingRoundtrip() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + for (boolean b : new boolean[] {true, false}) { + p.setConcurrentBuild(b); + j.submit(j.createWebClient().getPage(p, "configure").getFormByName("config")); + assertEquals(b, p.isConcurrentBuild()); + } + } + + /** + * Tests round trip configuration of the blockBuildWhenUpstreamBuilding + * field + */ + @Test + @Issue("JENKINS-4423") + public void configuringBlockBuildWhenUpstreamBuildingRoundtrip() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + p.setBlockBuildWhenUpstreamBuilding(false); + + HtmlForm form = j.createWebClient().getPage(p, "configure").getFormByName("config"); + HtmlInput input = form.getInputByName("blockBuildWhenUpstreamBuilding"); + assertFalse("blockBuildWhenUpstreamBuilding check box is checked.", input.isChecked()); + + input.setChecked(true); + j.submit(form); + assertTrue("blockBuildWhenUpstreamBuilding was not updated from configuration form", p.blockBuildWhenUpstreamBuilding()); + + form = j.createWebClient().getPage(p, "configure").getFormByName("config"); + input = form.getInputByName("blockBuildWhenUpstreamBuilding"); + assert input.isChecked() : "blockBuildWhenUpstreamBuilding check box is not checked."; + } + + /** + * Unless the concurrent build option is enabled, polling and build should + * be mutually exclusive to avoid allocating unnecessary workspaces. + */ + @Test + @Issue("JENKINS-4202") + public void pollingAndBuildExclusion() throws Exception { + final OneShotEvent sync = new OneShotEvent(); + + final FreeStyleProject p = j.createFreeStyleProject(); + FreeStyleBuild b1 = j.buildAndAssertSuccess(p); + + p.setScm(new NullSCM() { + @Override + public boolean pollChanges(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener) { + try { + sync.block(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return true; + } + + /** + * Don't write 'this', so that subtypes can be implemented as + * anonymous class. + */ + private Object writeReplace() { + return new Object(); + } + + @Override + public boolean requiresWorkspaceForPolling() { + return true; + } + @Override + public SCMDescriptor getDescriptor() { + return new SCMDescriptor(null) { + }; + } + }); + Thread t = new Thread() { + @Override + public void run() { + p.pollSCMChanges(StreamTaskListener.fromStdout()); + } + }; + try { + t.start(); + Future f = p.scheduleBuild2(0); + + // add a bit of delay to make sure that the blockage is happening + Thread.sleep(3000); + + // release the polling + sync.signal(); + + FreeStyleBuild b2 = j.assertBuildStatusSuccess(f); + + // they should have used the same workspace. + assertEquals(b1.getWorkspace(), b2.getWorkspace()); + } finally { + t.interrupt(); + } + } + + @Test + @Issue("JENKINS-1986") + public void buildSymlinks() throws Exception { + Assume.assumeFalse("If we're on Windows, don't bother doing this", Functions.isWindows()); + + FreeStyleProject job = j.createFreeStyleProject(); + job.getBuildersList().add(new Shell("echo \"Build #$BUILD_NUMBER\"\n")); + FreeStyleBuild build = job.scheduleBuild2(0, new Cause.UserCause()).get(); + File lastSuccessful = new File(job.getRootDir(), "lastSuccessful"), + lastStable = new File(job.getRootDir(), "lastStable"); + // First build creates links + assertSymlinkForBuild(lastSuccessful, 1); + assertSymlinkForBuild(lastStable, 1); + FreeStyleBuild build2 = job.scheduleBuild2(0, new Cause.UserCause()).get(); + // Another build updates links + assertSymlinkForBuild(lastSuccessful, 2); + assertSymlinkForBuild(lastStable, 2); + // Delete latest build should update links + build2.delete(); + assertSymlinkForBuild(lastSuccessful, 1); + assertSymlinkForBuild(lastStable, 1); + // Delete all builds should remove links + build.delete(); + assertFalse("lastSuccessful link should be removed", lastSuccessful.exists()); + assertFalse("lastStable link should be removed", lastStable.exists()); + } + + private static void assertSymlinkForBuild(File file, int buildNumber) + throws IOException, InterruptedException { + assert file.exists() : "should exist and point to something that exists"; + assert Util.isSymlink(file) : "should be symlink"; + String s = FileUtils.readFileToString(new File(file, "log")); + assert s.contains("Build #" + buildNumber + "\n") : "link should point to build #$buildNumber, but link was: ${Util.resolveSymlink(file, TaskListener.NULL)}\nand log was:\n$s"; + } + + @Test + @Issue("JENKINS-2543") + public void symlinkForPostBuildFailure() throws Exception { + Assume.assumeFalse("If we're on Windows, don't bother doing this", Functions.isWindows()); + + // Links should be updated after post-build actions when final build result is known + FreeStyleProject job = j.createFreeStyleProject(); + job.getBuildersList().add(new Shell("echo \"Build #$BUILD_NUMBER\"\n")); + FreeStyleBuild build = job.scheduleBuild2(0, new Cause.UserCause()).get(); + assert Result.SUCCESS == build.getResult(); + File lastSuccessful = new File(job.getRootDir(), "lastSuccessful"), + lastStable = new File(job.getRootDir(), "lastStable"); + // First build creates links + assertSymlinkForBuild(lastSuccessful, 1); + assertSymlinkForBuild(lastStable, 1); + // Archive artifacts that don't exist to create failure in post-build action + job.getPublishersList().add(new ArtifactArchiver("*.foo", "", false, false)); + build = job.scheduleBuild2(0, new Cause.UserCause()).get(); + assert Result.FAILURE == build.getResult(); + // Links should not be updated since build failed + assertSymlinkForBuild(lastSuccessful, 1); + assertSymlinkForBuild(lastStable, 1); + } + + /* TODO too slow, seems capable of causing testWorkspaceLock to time out: + @Test + @Issue("JENKINS-15156") + public void testGetBuildAfterGC() { + FreeStyleProject job = j.createFreeStyleProject(); + job.scheduleBuild2(0, new Cause.UserIdCause()).get(); + j.jenkins.queue.clearLeftItems(); + MemoryAssert.assertGC(new WeakReference(job.getLastBuild())); + assert job.lastBuild != null; + } + */ + @Test + @Issue("JENKINS-17137") + public void externalBuildDirectorySymlinks() throws Exception { + Assume.assumeFalse(Functions.isWindows()); // symlinks may not be available + HtmlForm form = j.createWebClient().goTo("configure").getFormByName("config"); + File builds = j.createTmpDir(); + form.getInputByName("_.rawBuildsDir").setValueAttribute(builds.toString() + "/${ITEM_FULL_NAME}"); + j.submit(form); + assertEquals(builds.toString() + "/${ITEM_FULL_NAME}", j.jenkins.getRawBuildsDir()); + FreeStyleProject p = j.jenkins.createProject(MockFolder.class, "d").createProject(FreeStyleProject.class, "p"); + FreeStyleBuild b1 = p.scheduleBuild2(0).get(); + File link = new File(p.getRootDir(), "lastStable"); + assertTrue(link.exists()); + assertEquals(resolveAll(link).getAbsolutePath(), b1.getRootDir().getAbsolutePath()); + FreeStyleBuild b2 = p.scheduleBuild2(0).get(); + assertTrue(link.exists()); + assertEquals(resolveAll(link).getAbsolutePath(), b2.getRootDir().getAbsolutePath()); + b2.delete(); + assertTrue(link.exists()); + assertEquals(resolveAll(link).getAbsolutePath(), b1.getRootDir().getAbsolutePath()); + b1.delete(); + assertFalse(link.exists()); + } + + private File resolveAll(File link) throws InterruptedException, IOException { + while (true) { + File f = Util.resolveSymlinkToFile(link); + if (f == null) { + return link; + } + link = f; + } + } + + @Test + @Issue("JENKINS-17138") + public void externalBuildDirectoryRenameDelete() throws Exception { + HtmlForm form = j.createWebClient().goTo("configure").getFormByName("config"); + File builds = j.createTmpDir(); + form.getInputByName("_.rawBuildsDir").setValueAttribute(builds.toString() + "/${ITEM_FULL_NAME}"); + j.submit(form); + assertEquals(builds.toString() + "/${ITEM_FULL_NAME}", j.jenkins.getRawBuildsDir()); + FreeStyleProject p = j.jenkins.createProject(MockFolder.class, "d").createProject(FreeStyleProject.class, "prj"); + FreeStyleBuild b = p.scheduleBuild2(0).get(); + File oldBuildDir = new File(builds, "d/prj"); + assertEquals(new File(oldBuildDir, b.getId()), b.getRootDir()); + assertTrue(b.getRootDir().isDirectory()); + p.renameTo("proj"); + File newBuildDir = new File(builds, "d/proj"); + assertEquals(new File(newBuildDir, b.getId()), b.getRootDir()); + assertTrue(b.getRootDir().isDirectory()); + p.delete(); + assertFalse(b.getRootDir().isDirectory()); + } + + @Test + @Issue("JENKINS-18678") + public void renameJobLostBuilds() throws Exception { + FreeStyleProject p = j.createFreeStyleProject("initial"); + j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + assertEquals(1, p.getBuilds().size()); + p.renameTo("edited"); + p._getRuns().purgeCache(); + assertEquals(1, p.getBuilds().size()); + MockFolder d = j.jenkins.createProject(MockFolder.class, "d"); + Items.move(p, d); + assertEquals(p, j.jenkins.getItemByFullName("d/edited")); + p._getRuns().purgeCache(); + assertEquals(1, p.getBuilds().size()); + d.renameTo("d2"); + p = j.jenkins.getItemByFullName("d2/edited", FreeStyleProject.class); + p._getRuns().purgeCache(); + assertEquals(1, p.getBuilds().size()); + } + + @Test + @Issue("JENKINS-17575") + public void deleteRedirect() throws Exception { + j.createFreeStyleProject("j1"); + assertEquals("", deleteRedirectTarget("job/j1")); + j.createFreeStyleProject("j2"); + Jenkins.getInstance().addView(new AllView("v1")); + assertEquals("view/v1/", deleteRedirectTarget("view/v1/job/j2")); + MockFolder d = Jenkins.getInstance().createProject(MockFolder.class, "d"); + d.addView(new AllView("v2")); + for (String n : new String[] {"j3", "j4", "j5"}) { + d.createProject(FreeStyleProject.class, n); + } + assertEquals("job/d/", deleteRedirectTarget("job/d/job/j3")); + assertEquals("job/d/view/v2/", deleteRedirectTarget("job/d/view/v2/job/j4")); + assertEquals("view/v1/job/d/", deleteRedirectTarget("view/v1/job/d/job/j5")); + assertEquals("view/v1/", deleteRedirectTarget("view/v1/job/d")); // JENKINS-23375 + } + + private String deleteRedirectTarget(String job) throws Exception { + JenkinsRule.WebClient wc = j.createWebClient(); + String base = wc.getContextPath(); + String loc = wc.getPage(wc.addCrumb(new WebRequest(new URL(base + job + "/doDelete"), HttpMethod.POST))).getUrl().toString(); + assert loc.startsWith(base) : loc; + return loc.substring(base.length()); + } + + @Test + @Issue("JENKINS-18407") + public void queueSuccessBehavior() throws Exception { + // prevent any builds to test the behaviour + j.jenkins.setNumExecutors(0); + + FreeStyleProject p = j.createFreeStyleProject(); + Future f = p.scheduleBuild2(0); + assertNotNull(f); + Future g = p.scheduleBuild2(0); + assertEquals(f, g); + + p.makeDisabled(true); + assertNull(p.scheduleBuild2(0)); + } + + /** + * Do the same as {@link #testQueueSuccessBehavior()} but over HTTP + */ + @Test + @Issue("JENKINS-18407") + public void queueSuccessBehaviorOverHTTP() throws Exception { + // prevent any builds to test the behaviour + j.jenkins.setNumExecutors(0); + + FreeStyleProject p = j.createFreeStyleProject(); + JenkinsRule.WebClient wc = j.createWebClient(); + + WebResponse rsp = wc.getPage(j.getURL() + p.getUrl() + "build").getWebResponse(); + assertEquals(201, rsp.getStatusCode()); + assertNotNull(rsp.getResponseHeaderValue("Location")); + + WebResponse rsp2 = wc.getPage(j.getURL() + p.getUrl() + "build").getWebResponse(); + assertEquals(201, rsp2.getStatusCode()); + assertEquals(rsp.getResponseHeaderValue("Location"), rsp2.getResponseHeaderValue("Location")); + + p.makeDisabled(true); + + try { + wc.getPage(j.getURL() + p.getUrl() + "build"); + fail(); + } catch (FailingHttpStatusCodeException e) { + // request should fail + } + } + + /** + * We used to store {@link AbstractProject#triggers} as {@link Vector}, so + * make sure we can still read back the configuration from that. + */ + @Test + public void vectorTriggers() throws Exception { + AbstractProject p = (AbstractProject) j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")); + assertEquals(1, p.triggers().size()); + Trigger t = p.triggers().get(0); + assertEquals(SCMTrigger.class, t.getClass()); + assertEquals("*/10 * * * *", t.getSpec()); + } + + @Test + @Issue("JENKINS-18813") + public void removeTrigger() throws Exception { + AbstractProject p = (AbstractProject) j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")); + + TriggerDescriptor SCM_TRIGGER_DESCRIPTOR = (TriggerDescriptor) j.jenkins.getDescriptorOrDie(SCMTrigger.class); + p.removeTrigger(SCM_TRIGGER_DESCRIPTOR); + assertEquals(0, p.triggers().size()); + } + + @Test + @Issue("JENKINS-18813") + public void addTriggerSameType() throws Exception { + AbstractProject p = (AbstractProject) j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")); + + SCMTrigger newTrigger = new SCMTrigger("H/5 * * * *"); + p.addTrigger(newTrigger); + + assertEquals(1, p.triggers().size()); + Trigger t = p.triggers().get(0); + assertEquals(SCMTrigger.class, t.getClass()); + assertEquals("H/5 * * * *", t.getSpec()); + } + + @Test + @Issue("JENKINS-18813") + public void addTriggerDifferentType() throws Exception { + AbstractProject p = (AbstractProject) j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")); + + TimerTrigger newTrigger = new TimerTrigger("20 * * * *"); + p.addTrigger(newTrigger); + + assertEquals(2, p.triggers().size()); + Trigger t = p.triggers().get(1); + assertEquals(newTrigger, t); + } + + /** + * Trying to POST to config.xml by a different job type should fail. + */ + @Test + public void configDotXmlSubmissionToDifferentType() throws Exception { + TestPluginManager tpm = (TestPluginManager) j.jenkins.pluginManager; + tpm.installDetachedPlugin("javadoc"); + tpm.installDetachedPlugin("junit"); + tpm.installDetachedPlugin("display-url-api"); + tpm.installDetachedPlugin("mailer"); + tpm.installDetachedPlugin("maven-plugin"); + + j.jenkins.setCrumbIssuer(null); + FreeStyleProject p = j.createFreeStyleProject(); + + HttpURLConnection con = postConfigDotXml(p, ""); + + // this should fail with a type mismatch error + // the error message should report both what was submitted and what was expected + assertEquals(500, con.getResponseCode()); + String msg = IOUtils.toString(con.getErrorStream()); + System.out.println(msg); + assertThat(msg, allOf(containsString(FreeStyleProject.class.getName()), containsString(MavenModuleSet.class.getName()))); + + // control. this should work + con = postConfigDotXml(p, ""); + assertEquals(200, con.getResponseCode()); + } + + private HttpURLConnection postConfigDotXml(FreeStyleProject p, String xml) throws Exception { + HttpURLConnection con = (HttpURLConnection) new URL(j.getURL(), "job/" + p.getName() + "/config.xml").openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty("Content-Type", "application/xml"); + con.setDoOutput(true); + try (OutputStream s = con.getOutputStream()) { + s.write(xml.getBytes()); + } + return con; + } + + @Test + @Issue("JENKINS-27549") + public void loadingWithNPEOnTriggerStart() throws Exception { + AbstractProject project = (AbstractProject) j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/npeTrigger.xml")); + + assertEquals(1, project.triggers().size()); + } + + @Test + @Issue("JENKINS-30742") + public void resolveForCLI() throws Exception { + try { + AbstractProject not_found = AbstractProject.resolveForCLI("never_created"); + fail("Exception should occur before!"); + } catch (CmdLineException e) { + assertEquals("No such job \u2018never_created\u2019 exists.", e.getMessage()); + } + + AbstractProject project = j.jenkins.createProject(FreeStyleProject.class, "never_created"); + try { + AbstractProject not_found = AbstractProject.resolveForCLI("never_created1"); + fail("Exception should occur before!"); + } catch (CmdLineException e) { + assertEquals("No such job \u2018never_created1\u2019 exists. Perhaps you meant \u2018never_created\u2019?", e.getMessage()); + } + + } + + public static class MockBuildTriggerThrowsNPEOnStart extends Trigger { + @Override + public void start(hudson.model.Item project, boolean newInstance) { + throw new NullPointerException(); + } + + @TestExtension("loadingWithNPEOnTriggerStart") + public static class DescriptorImpl extends TriggerDescriptor { + + @Override + public boolean isApplicable(hudson.model.Item item) { + return false; + } + } + + } + + @Issue("SECURITY-617") + @Test + public void upstreamDownstreamExportApi() throws Exception { + FreeStyleProject us = j.createFreeStyleProject("upstream-project"); + FreeStyleProject ds = j.createFreeStyleProject("downstream-project"); + us.getPublishersList().add(new BuildTrigger(Collections.singleton(ds), Result.SUCCESS)); + j.jenkins.rebuildDependencyGraph(); + assertEquals(Collections.singletonList(ds), us.getDownstreamProjects()); + assertEquals(Collections.singletonList(us), ds.getUpstreamProjects()); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy(). + grant(Jenkins.READ).everywhere().toEveryone(). + grant(Item.READ).everywhere().to("alice"). + grant(Item.READ).onItems(us).to("bob"). + grant(Item.READ).onItems(ds).to("charlie")); + String api = j.createWebClient().login("alice").goTo(us.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); + System.out.println(api); + assertThat(api, containsString("downstream-project")); + api = j.createWebClient().login("alice").goTo(ds.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); + System.out.println(api); + assertThat(api, containsString("upstream-project")); + api = j.createWebClient().login("bob").goTo(us.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); + System.out.println(api); + assertThat(api, not(containsString("downstream-project"))); + api = j.createWebClient().login("charlie").goTo(ds.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); + System.out.println(api); + assertThat(api, not(containsString("upstream-project"))); + } + +} From b81a8ec736931d6346fea31a40d4dc11550dc01a Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 17:41:42 -0500 Subject: [PATCH 40/85] EnvironmentContributorTest --- .../model/EnvironmentContributorTest.groovy | 41 ------------------- .../model/EnvironmentContributorTest.java | 40 ++++++++++++++++++ 2 files changed, 40 insertions(+), 41 deletions(-) delete mode 100644 test/src/test/groovy/hudson/model/EnvironmentContributorTest.groovy create mode 100644 test/src/test/java/hudson/model/EnvironmentContributorTest.java diff --git a/test/src/test/groovy/hudson/model/EnvironmentContributorTest.groovy b/test/src/test/groovy/hudson/model/EnvironmentContributorTest.groovy deleted file mode 100644 index fab4c865f6..0000000000 --- a/test/src/test/groovy/hudson/model/EnvironmentContributorTest.groovy +++ /dev/null @@ -1,41 +0,0 @@ -package hudson.model - -import hudson.EnvVars -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.CaptureEnvironmentBuilder -import org.jvnet.hudson.test.JenkinsRule -import org.jvnet.hudson.test.TestExtension - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class EnvironmentContributorTest { - @Rule - public JenkinsRule j = new JenkinsRule() - - /** - * Makes sure that the project-scoped environment variables are getting consulted. - */ - @Test - public void testProjectScoped() { - def p = j.createFreeStyleProject() - def c = new CaptureEnvironmentBuilder() - p.buildersList.add(c) - p.description = "Issac Newton"; - j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - - assert c.envVars["ABC"]=="Issac Newton"; - assert c.envVars["NODE_NAME"]=="master"; - } - - @TestExtension("testProjectScoped") - public static class JobScopedInjection extends EnvironmentContributor { - @Override - void buildEnvironmentFor(Job j, EnvVars envs, TaskListener listener) { - envs.put("ABC",j.description) - } - } -} diff --git a/test/src/test/java/hudson/model/EnvironmentContributorTest.java b/test/src/test/java/hudson/model/EnvironmentContributorTest.java new file mode 100644 index 0000000000..f7a99814e8 --- /dev/null +++ b/test/src/test/java/hudson/model/EnvironmentContributorTest.java @@ -0,0 +1,40 @@ +package hudson.model; + +import hudson.EnvVars; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.CaptureEnvironmentBuilder; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; + +public class EnvironmentContributorTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + /** + * Makes sure that the project-scoped environment variables are getting + * consulted. + */ + @Test + public void projectScoped() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + CaptureEnvironmentBuilder c = new CaptureEnvironmentBuilder(); + p.getBuildersList().add(c); + p.setDescription("Issac Newton"); + j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + + assertEquals("Issac Newton", c.getEnvVars().get("ABC")); + assertEquals("master", c.getEnvVars().get("NODE_NAME")); + } + + @TestExtension("projectScoped") + public static class JobScopedInjection extends EnvironmentContributor { + @Override + public void buildEnvironmentFor(Job j, EnvVars envs, TaskListener listener) { + envs.put("ABC", j.getDescription()); + } + } + +} From 54a44b7d587affb86e9f881b76afa70fe317eb6b Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 17:52:43 -0500 Subject: [PATCH 41/85] SlaveTest --- .../test/groovy/hudson/model/SlaveTest.groovy | 104 --------------- .../model/{Slave2Test.java => SlaveTest.java} | 123 +++++++++++++----- 2 files changed, 90 insertions(+), 137 deletions(-) delete mode 100644 test/src/test/groovy/hudson/model/SlaveTest.groovy rename test/src/test/java/hudson/model/{Slave2Test.java => SlaveTest.java} (69%) diff --git a/test/src/test/groovy/hudson/model/SlaveTest.groovy b/test/src/test/groovy/hudson/model/SlaveTest.groovy deleted file mode 100644 index 35299a7e4f..0000000000 --- a/test/src/test/groovy/hudson/model/SlaveTest.groovy +++ /dev/null @@ -1,104 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc. - * - * 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.model - -import static hudson.util.FormValidation.Kind.ERROR -import static hudson.util.FormValidation.Kind.WARNING -import static org.junit.Assert.assertNotNull -import static org.junit.Assert.assertEquals - -import hudson.security.csrf.CrumbIssuer -import hudson.slaves.DumbSlave -import hudson.slaves.JNLPLauncher -import hudson.util.FormValidation -import org.junit.Rule -import org.junit.Test -import org.apache.commons.io.IOUtils -import org.jvnet.hudson.test.GroovyJenkinsRule - -/** - * @author Kohsuke Kawaguchi - */ -class SlaveTest { - - @Rule - public GroovyJenkinsRule j = new GroovyJenkinsRule() - - /** - * Makes sure that a form validation method gets inherited. - */ - @Test - void formValidation() { - j.executeOnServer { - assertNotNull(j.jenkins.getDescriptor(DumbSlave).getCheckUrl("remoteFS")) - } - } - - /** - * Programmatic config.xml submission. - */ - @Test - void slaveConfigDotXml() { - DumbSlave s = j.createSlave() - def wc = j.createWebClient() - def p = wc.goTo("computer/${s.name}/config.xml", "application/xml") - def xml = p.webResponse.contentAsString - new XmlSlurper().parseText(xml) // verify that it is XML - - // make sure it survives the roundtrip - post("computer/${s.name}/config.xml",xml); - - assertNotNull(j.jenkins.getNode(s.name)) - - xml = IOUtils.toString(getClass().getResource("SlaveTest/slave.xml").openStream()); - xml = xml.replace("NAME",s.name) - post("computer/${s.name}/config.xml",xml); - - s = j.jenkins.getNode(s.name) - assertNotNull(s) - assertEquals("some text",s.nodeDescription) - assertEquals(JNLPLauncher.class,s.launcher.class) - } - - def post(url,String xml) { - HttpURLConnection con = new URL(j.getURL(),url).openConnection(); - con.requestMethod = "POST" - con.setRequestProperty("Content-Type","application/xml;charset=UTF-8") - con.setRequestProperty(CrumbIssuer.DEFAULT_CRUMB_NAME,"test") - con.doOutput = true; - con.outputStream.write(xml.getBytes("UTF-8")) - con.outputStream.close(); - IOUtils.copy(con.inputStream,System.out) - } - - @Test - void remoteFsCheck() { - def d = j.jenkins.getDescriptorByType(DumbSlave.DescriptorImpl.class) - assert d.doCheckRemoteFS("c:\\")==FormValidation.ok(); - assert d.doCheckRemoteFS("/tmp")==FormValidation.ok(); - assert d.doCheckRemoteFS("relative/path").kind==WARNING; - assert d.doCheckRemoteFS("/net/foo/bar/zot").kind==WARNING; - assert d.doCheckRemoteFS("\\\\machine\\folder\\foo").kind==WARNING; - } -} diff --git a/test/src/test/java/hudson/model/Slave2Test.java b/test/src/test/java/hudson/model/SlaveTest.java similarity index 69% rename from test/src/test/java/hudson/model/Slave2Test.java rename to test/src/test/java/hudson/model/SlaveTest.java index 6ab40e3eed..76823820c3 100644 --- a/test/src/test/java/hudson/model/Slave2Test.java +++ b/test/src/test/java/hudson/model/SlaveTest.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2015 CloudBees, Inc. + * Copyright (c) 2004-2009, Sun Microsystems, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,59 +23,117 @@ */ package hudson.model; +import com.gargoylesoftware.htmlunit.Page; +import groovy.util.XmlSlurper; import hudson.DescriptorExtensionList; import hudson.ExtensionList; +import hudson.security.csrf.CrumbIssuer; import hudson.slaves.ComputerLauncher; import hudson.slaves.DumbSlave; +import hudson.slaves.JNLPLauncher; import hudson.slaves.NodeProperty; import hudson.slaves.NodePropertyDescriptor; import hudson.slaves.RetentionStrategy; +import hudson.util.FormValidation; +import static hudson.util.FormValidation.Kind.WARNING; +import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.URL; import java.net.URLEncoder; import java.util.HashSet; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; -import org.junit.Rule; -import org.junit.Test; -import org.jvnet.hudson.test.JenkinsRule; - +import org.apache.commons.io.IOUtils; import static org.hamcrest.Matchers.anyOf; -import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import static org.junit.Assume.assumeThat; - +import org.junit.Rule; +import org.junit.Test; import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TestExtension; -/** - * Tests for the {@link Slave} class. - * There is also a Groovy implementation of such test file, hence the class name - * has an index. - * @author Oleg Nenashev - */ -public class Slave2Test { - +public class SlaveTest { + @Rule - public JenkinsRule rule = new JenkinsRule(); - + public JenkinsRule j = new JenkinsRule(); + + /** + * Makes sure that a form validation method gets inherited. + */ + @Test + public void formValidation() throws Exception { + j.executeOnServer(() -> { + assertNotNull(j.jenkins.getDescriptor(DumbSlave.class).getCheckUrl("remoteFS")); + return null; + }); + } + + /** + * Programmatic config.xml submission. + */ + @Test + public void slaveConfigDotXml() throws Exception { + DumbSlave s = j.createSlave(); + JenkinsRule.WebClient wc = j.createWebClient(); + Page p = wc.goTo("computer/" + s.getNodeName() + "/config.xml", "application/xml"); + String xml = p.getWebResponse().getContentAsString(); + new XmlSlurper().parseText(xml); // verify that it is XML + + // make sure it survives the roundtrip + post("computer/" + s.getNodeName() + "/config.xml", xml); + + assertNotNull(j.jenkins.getNode(s.getNodeName())); + + xml = IOUtils.toString(getClass().getResource("SlaveTest/slave.xml").openStream()); + xml = xml.replace("NAME", s.getNodeName()); + post("computer/" + s.getNodeName() + "/config.xml", xml); + + s = (DumbSlave) j.jenkins.getNode(s.getNodeName()); + assertNotNull(s); + assertEquals("some text", s.getNodeDescription()); + assertEquals(JNLPLauncher.class, s.getLauncher().getClass()); + } + + private void post(String url, String xml) throws Exception { + HttpURLConnection con = (HttpURLConnection) new URL(j.getURL(), url).openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty("Content-Type", "application/xml;charset=UTF-8"); + con.setRequestProperty(CrumbIssuer.DEFAULT_CRUMB_NAME, "test"); + con.setDoOutput(true); + con.getOutputStream().write(xml.getBytes("UTF-8")); + con.getOutputStream().close(); + IOUtils.copy(con.getInputStream(), System.out); + } + + @Test + public void remoteFsCheck() throws Exception { + DumbSlave.DescriptorImpl d = j.jenkins.getDescriptorByType(DumbSlave.DescriptorImpl.class); + assertEquals(FormValidation.ok(), d.doCheckRemoteFS("c:\\")); + assertEquals(FormValidation.ok(), d.doCheckRemoteFS("/tmp")); + assertEquals(WARNING, d.doCheckRemoteFS("relative/path").kind); + assertEquals(WARNING, d.doCheckRemoteFS("/net/foo/bar/zot").kind); + assertEquals(WARNING, d.doCheckRemoteFS("\\\\machine\\folder\\foo").kind); + } + @Test @Issue("SECURITY-195") public void shouldNotEscapeJnlpSlavesResources() throws Exception { - Slave slave = rule.createSlave(); - + Slave slave = j.createSlave(); + // Spot-check correct requests assertJnlpJarUrlIsAllowed(slave, "agent.jar"); assertJnlpJarUrlIsAllowed(slave, "slave.jar"); assertJnlpJarUrlIsAllowed(slave, "remoting.jar"); assertJnlpJarUrlIsAllowed(slave, "jenkins-cli.jar"); assertJnlpJarUrlIsAllowed(slave, "hudson-cli.jar"); - + // Check that requests to other WEB-INF contents fail assertJnlpJarUrlFails(slave, "web.xml"); assertJnlpJarUrlFails(slave, "web.xml"); @@ -83,7 +141,7 @@ public class Slave2Test { assertJnlpJarUrlFails(slave, "classes/dependencies.txt"); assertJnlpJarUrlFails(slave, "plugins/ant.hpi"); assertJnlpJarUrlFails(slave, "nonexistentfolder/something.txt"); - + // Try various kinds of folder escaping (SECURITY-195) assertJnlpJarUrlFails(slave, "../"); assertJnlpJarUrlFails(slave, ".."); @@ -93,7 +151,7 @@ public class Slave2Test { assertJnlpJarUrlFails(slave, "foo/../../bar"); assertJnlpJarUrlFails(slave, "./../foo/bar"); } - + private void assertJnlpJarUrlFails(@Nonnull Slave slave, @Nonnull String url) throws Exception { // Raw access to API Slave.JnlpJar jnlpJar = slave.getComputer().getJnlpJars(url); @@ -105,15 +163,15 @@ public class Slave2Test { } fail("Expected the MalformedURLException for " + url); } - + private void assertJnlpJarUrlIsAllowed(@Nonnull Slave slave, @Nonnull String url) throws Exception { // Raw access to API Slave.JnlpJar jnlpJar = slave.getComputer().getJnlpJars(url); assertNotNull(jnlpJar.getURL()); - - + + // Access from a Web client - JenkinsRule.WebClient client = rule.createWebClient(); + JenkinsRule.WebClient client = j.createWebClient(); assertEquals(200, client.getPage(client.getContextPath() + "jnlpJars/" + URLEncoder.encode(url, "UTF-8")).getWebResponse().getStatusCode()); assertEquals(200, client.getPage(jnlpJar.getURL()).getWebResponse().getStatusCode()); } @@ -122,9 +180,9 @@ public class Slave2Test { @Issue("JENKINS-36280") public void launcherFiltering() throws Exception { DumbSlave.DescriptorImpl descriptor = - rule.getInstance().getDescriptorByType(DumbSlave.DescriptorImpl.class); + j.getInstance().getDescriptorByType(DumbSlave.DescriptorImpl.class); DescriptorExtensionList> descriptors = - rule.getInstance().getDescriptorList(ComputerLauncher.class); + j.getInstance().getDescriptorList(ComputerLauncher.class); assumeThat("we need at least two launchers to test this", descriptors.size(), not(anyOf(is(0), is(1)))); assertThat(descriptor.computerLauncherDescriptors(null), containsInAnyOrder(descriptors.toArray(new Descriptor[descriptors.size()]))); @@ -140,7 +198,7 @@ public class Slave2Test { @Issue("JENKINS-36280") public void retentionFiltering() throws Exception { DumbSlave.DescriptorImpl descriptor = - rule.getInstance().getDescriptorByType(DumbSlave.DescriptorImpl.class); + j.getInstance().getDescriptorByType(DumbSlave.DescriptorImpl.class); DescriptorExtensionList, Descriptor>> descriptors = RetentionStrategy.all(); assumeThat("we need at least two retention strategies to test this", descriptors.size(), not(anyOf(is(0), is(1)))); assertThat(descriptor.retentionStrategyDescriptors(null), containsInAnyOrder(descriptors.toArray(new Descriptor[descriptors.size()]))); @@ -157,7 +215,7 @@ public class Slave2Test { @Issue("JENKINS-36280") public void propertyFiltering() throws Exception { DumbSlave.DescriptorImpl descriptor = - rule.getInstance().getDescriptorByType(DumbSlave.DescriptorImpl.class); + j.getInstance().getDescriptorByType(DumbSlave.DescriptorImpl.class); DescriptorExtensionList, NodePropertyDescriptor> descriptors = NodeProperty.all(); assumeThat("we need at least two node properties to test this", descriptors.size(), not(anyOf(is(0), is(1)))); assertThat(descriptor.nodePropertyDescriptors(null), containsInAnyOrder(descriptors.toArray(new Descriptor[descriptors.size()]))); @@ -170,8 +228,6 @@ public class Slave2Test { assertThat(descriptor.nodePropertyDescriptors(null), hasItem(victim)); } - - @TestExtension public static class DynamicFilter extends DescriptorVisibilityFilter { @@ -191,4 +247,5 @@ public class Slave2Test { return !descriptors.contains(descriptor); } } + } From ef8568c40ad84ec43ee2b77285eefd7db0cb79b0 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 18:02:08 -0500 Subject: [PATCH 42/85] TokenBasedRememberMeServices2Test --- .../TokenBasedRememberMeServices2Test.groovy | 129 ----------------- .../TokenBasedRememberMeServices2Test.java | 130 ++++++++++++++++++ 2 files changed, 130 insertions(+), 129 deletions(-) delete mode 100644 test/src/test/groovy/hudson/security/TokenBasedRememberMeServices2Test.groovy create mode 100644 test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test.java diff --git a/test/src/test/groovy/hudson/security/TokenBasedRememberMeServices2Test.groovy b/test/src/test/groovy/hudson/security/TokenBasedRememberMeServices2Test.groovy deleted file mode 100644 index d0c6ae2880..0000000000 --- a/test/src/test/groovy/hudson/security/TokenBasedRememberMeServices2Test.groovy +++ /dev/null @@ -1,129 +0,0 @@ -package hudson.security - -import jenkins.model.Jenkins -import org.acegisecurity.AuthenticationException -import org.acegisecurity.BadCredentialsException -import org.acegisecurity.GrantedAuthority -import org.acegisecurity.GrantedAuthorityImpl -import org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices -import org.acegisecurity.userdetails.User -import org.acegisecurity.userdetails.UserDetails -import org.acegisecurity.userdetails.UsernameNotFoundException -import org.junit.Before -import com.gargoylesoftware.htmlunit.util.Cookie -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.JenkinsRule -import org.jvnet.hudson.test.LoggerRule -import org.springframework.dao.DataAccessException - -import java.util.logging.Handler -import java.util.logging.LogRecord -import java.util.logging.Logger - -import static java.util.logging.Level.FINEST - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class TokenBasedRememberMeServices2Test { - @Rule - public JenkinsRule j = new JenkinsRule(); - @Rule - public LoggerRule logging = new LoggerRule() - - private static boolean failureInduced; - - @Before - public void resetFailureInduced() {failureInduced = false} - - @Test - public void rememberMeAutoLoginFailure() { - j.jenkins.securityRealm = new InvalidUserWhenLoggingBackInRealm() - - def wc = j.createWebClient() - wc.login("alice","alice",true) - - // we should see a remember me cookie - def c = getRememberMeCookie(wc) - assert c!=null - - // start a new session and attempt to access Jenkins, - // which should cause autoLogin failures - wc = j.createWebClient() - wc.cookieManager.addCookie(c); - - // even if SecurityRealm chokes, it shouldn't kill the page - logging.capture(1000).record(TokenBasedRememberMeServices.class, FINEST) - wc.goTo("") - - // make sure that the server recorded this failure - assert failureInduced - assert logging.messages.find { it.contains("contained username 'alice' but was not found")}!=null - // and the problematic cookie should have been removed - assert getRememberMeCookie(wc)==null - } - - private Cookie getRememberMeCookie(JenkinsRule.WebClient wc) { - wc.cookieManager.getCookie(TokenBasedRememberMeServices2.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY) - } - - private static class InvalidUserWhenLoggingBackInRealm extends AbstractPasswordBasedSecurityRealm { - @Override - protected UserDetails authenticate(String username, String password) throws AuthenticationException { - if (username==password) - return new User(username,password,true,[new GrantedAuthorityImpl("myteam")] as GrantedAuthority[]) - throw new BadCredentialsException(username); - } - - @Override - GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException { - throw new UnsupportedOperationException() - } - - @Override - UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { - failureInduced = true - throw new UsernameNotFoundException("intentionally not working"); - } - } - - - @Test - public void basicFlow() { - j.jenkins.securityRealm = new StupidRealm() - - def wc = j.createWebClient() - wc.login("bob","bob",true) - - // we should see a remember me cookie - def c = getRememberMeCookie(wc) - assert c!=null - - // start a new session and attempt to access Jenkins, - wc = j.createWebClient() - wc.cookieManager.addCookie(c); - - // this will trigger remember me - wc.goTo("") - - // make sure that our security realm failed to report the info correctly - assert failureInduced - // but we should have logged in - wc.executeOnServer { - def a = Jenkins.getAuthentication() - assert a.name=="bob" - assert a.authorities*.authority.join(":")=="authenticated:myteam" - } - } - - private static class StupidRealm extends InvalidUserWhenLoggingBackInRealm { - @Override - UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { - failureInduced = true - throw new UserMayOrMayNotExistException("I cannot tell"); - } - } -} diff --git a/test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test.java b/test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test.java new file mode 100644 index 0000000000..35bea92686 --- /dev/null +++ b/test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test.java @@ -0,0 +1,130 @@ +package hudson.security; + +import com.gargoylesoftware.htmlunit.util.Cookie; +import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import static java.util.logging.Level.FINEST; +import java.util.stream.Collectors; +import jenkins.model.Jenkins; +import org.acegisecurity.Authentication; +import org.acegisecurity.AuthenticationException; +import org.acegisecurity.BadCredentialsException; +import org.acegisecurity.GrantedAuthority; +import org.acegisecurity.GrantedAuthorityImpl; +import org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices; +import org.acegisecurity.userdetails.User; +import org.acegisecurity.userdetails.UserDetails; +import org.acegisecurity.userdetails.UsernameNotFoundException; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.LoggerRule; +import org.springframework.dao.DataAccessException; + +public class TokenBasedRememberMeServices2Test { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Rule + public LoggerRule logging = new LoggerRule(); + + private static boolean failureInduced; + + @Before + public void resetFailureInduced() { + failureInduced = false; + } + + @Test + public void rememberMeAutoLoginFailure() throws Exception { + j.jenkins.setSecurityRealm(new InvalidUserWhenLoggingBackInRealm()); + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.login("alice", "alice", true); + + // we should see a remember me cookie + Cookie c = getRememberMeCookie(wc); + assertNotNull(c); + + // start a new session and attempt to access Jenkins, + // which should cause autoLogin failures + wc = j.createWebClient(); + wc.getCookieManager().addCookie(c); + + // even if SecurityRealm chokes, it shouldn't kill the page + logging.capture(1000).record(TokenBasedRememberMeServices.class, FINEST); + wc.goTo(""); + + // make sure that the server recorded this failure + assertTrue(failureInduced); + assertTrue(logging.getMessages().stream().anyMatch(m -> m.contains("contained username 'alice' but was not found"))); + // and the problematic cookie should have been removed + assertNull(getRememberMeCookie(wc)); + } + + private Cookie getRememberMeCookie(JenkinsRule.WebClient wc) { + return wc.getCookieManager().getCookie(TokenBasedRememberMeServices2.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY); + } + + private static class InvalidUserWhenLoggingBackInRealm extends AbstractPasswordBasedSecurityRealm { + @Override + protected UserDetails authenticate(String username, String password) throws AuthenticationException { + if (username.equals(password)) { + return new User(username, password, true, new GrantedAuthority[] {new GrantedAuthorityImpl("myteam")}); + } + throw new BadCredentialsException(username); + } + + @Override + public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException { + throw new UnsupportedOperationException(); + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { + failureInduced = true; + throw new UsernameNotFoundException("intentionally not working"); + } + } + + @Test + public void basicFlow() throws Exception { + j.jenkins.setSecurityRealm(new StupidRealm()); + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.login("bob", "bob", true); + + // we should see a remember me cookie + Cookie c = getRememberMeCookie(wc); + assertNotNull(c); + + // start a new session and attempt to access Jenkins, + wc = j.createWebClient(); + wc.getCookieManager().addCookie(c); + + // this will trigger remember me + wc.goTo(""); + + // make sure that our security realm failed to report the info correctly + assertTrue(failureInduced); + // but we should have logged in + wc.executeOnServer(() -> { + Authentication a = Jenkins.getAuthentication(); + assertEquals("bob", a.getName()); + assertEquals(ImmutableList.of("authenticated", "myteam"), Arrays.asList(a.getAuthorities()).stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())); + return null; + }); + } + + private static class StupidRealm extends InvalidUserWhenLoggingBackInRealm { + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { + failureInduced = true; + throw new UserMayOrMayNotExistException("I cannot tell"); + } + } + +} From d6dfd42a47cba0aea783c4020a62f516d3b6ce9d Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 18:15:28 -0500 Subject: [PATCH 43/85] TextFileTest --- .../test/java/hudson/util/TextFileTest.java | 97 +++++++++++++++++ .../src/test/resources/hudson/util/ascii.txt | 0 .../groovy/hudson/util/TextFileTest.groovy | 102 ------------------ 3 files changed, 97 insertions(+), 102 deletions(-) create mode 100644 core/src/test/java/hudson/util/TextFileTest.java rename {test => core}/src/test/resources/hudson/util/ascii.txt (100%) delete mode 100644 test/src/test/groovy/hudson/util/TextFileTest.groovy diff --git a/core/src/test/java/hudson/util/TextFileTest.java b/core/src/test/java/hudson/util/TextFileTest.java new file mode 100644 index 0000000000..cb689a080f --- /dev/null +++ b/core/src/test/java/hudson/util/TextFileTest.java @@ -0,0 +1,97 @@ +package hudson.util; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class TextFileTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Test + public void head() throws Exception { + File f = tmp.newFile(); + FileUtils.copyURLToFile(getClass().getResource("ascii.txt"), f); + + TextFile t = new TextFile(f); + String first35 = "Lorem ipsum dolor sit amet, consect"; + assertEquals(35, first35.length()); + assertEquals(first35, t.head(35)); + } + + @Test + public void shortHead() throws Exception { + File f = tmp.newFile(); + FileUtils.write(f, "hello"); + + TextFile t = new TextFile(f); + assertEquals("hello", t.head(35)); + } + + @Test + public void tail() throws Exception { + File f = tmp.newFile(); + FileUtils.copyURLToFile(getClass().getResource("ascii.txt"), f); + + TextFile t = new TextFile(f); + String tailStr = "la, vitae interdum quam rutrum id." + System.lineSeparator(); + assertEquals(34 + System.lineSeparator().length(), tailStr.length()); + assertEquals(tailStr, t.fastTail(tailStr.length())); + } + + @Test + public void shortTail() throws Exception { + File f = tmp.newFile(); + FileUtils.write(f, "hello"); + + TextFile t = new TextFile(f); + assertEquals("hello", t.fastTail(35)); + } + + /** + * Shift JIS is a multi-byte character encoding. + * + * In it, 0x82 0x83 is \u30e2, and 0x83 0x82 is \uFF43. So if aren't + * careful, we'll parse the text incorrectly. + */ + @Test + public void tailShiftJIS() throws Exception { + File f = tmp.newFile(); + + TextFile t = new TextFile(f); + + try (OutputStream o = new FileOutputStream(f)) { + for (int i = 0; i < 80; i++) { + for (int j = 0; j < 40; j++) { + o.write(0x83); + o.write(0x82); + } + o.write(0x0A); + } + } + + String tail = t.fastTail(35, Charset.forName("Shift_JIS")); + assertEquals(StringUtils.repeat("\u30e2", 34) + "\n", tail); + assertEquals(35, tail.length()); + + // add one more byte to force fastTail to read from one byte ahead + // between this and the previous case, it should start parsing text incorrectly, until it hits NL + // where it comes back in sync + try (OutputStream o = new FileOutputStream(f, true)) { + o.write(0x0A); + } + + tail = t.fastTail(35, Charset.forName("Shift_JIS")); + assertEquals(StringUtils.repeat("\u30e2", 33) + "\n\n", tail); + assertEquals(35, tail.length()); + } + +} diff --git a/test/src/test/resources/hudson/util/ascii.txt b/core/src/test/resources/hudson/util/ascii.txt similarity index 100% rename from test/src/test/resources/hudson/util/ascii.txt rename to core/src/test/resources/hudson/util/ascii.txt diff --git a/test/src/test/groovy/hudson/util/TextFileTest.groovy b/test/src/test/groovy/hudson/util/TextFileTest.groovy deleted file mode 100644 index 0299132fc8..0000000000 --- a/test/src/test/groovy/hudson/util/TextFileTest.groovy +++ /dev/null @@ -1,102 +0,0 @@ -package hudson.util - -import org.junit.After -import org.junit.Test - -import java.nio.charset.Charset - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class TextFileTest { - List files = []; - - @After - void tearDown() { - files*.delete() - } - - @Test - public void head() { - def f = newFile() - f.text = getClass().getResource("ascii.txt").text - - def t = new TextFile(f) - def first35 = "Lorem ipsum dolor sit amet, consect" - assert t.head(35).equals(first35) - assert first35.length()==35 - } - - @Test - public void shortHead() { - def f = newFile() - f.text = "hello" - - def t = new TextFile(f) - assert t.head(35).equals("hello") - } - - @Test - public void tail() { - def f = newFile() - f.text = getClass().getResource("ascii.txt").text - - def t = new TextFile(f) - def tailStr = "la, vitae interdum quam rutrum id." + System.lineSeparator() - assert t.fastTail(tailStr.length()).equals(tailStr) - assert tailStr.length()==(34 + System.lineSeparator().length()) - } - - @Test - public void shortTail() { - def f = newFile() - f.text = "hello" - - def t = new TextFile(f) - assert t.fastTail(35).equals("hello") - } - - /** - * Shift JIS is a multi-byte character encoding. - * - * In it, 0x82 0x83 is \u30e2, and 0x83 0x82 is \uFF43. - * So if aren't careful, we'll parse the text incorrectly. - */ - @Test - public void tailShiftJIS() { - def f = newFile() - - def t = new TextFile(f) - - f.withOutputStream { o -> - (1..80).each { - (1..40).each { - o.write(0x83) - o.write(0x82) - } - o.write(0x0A); - } - } - - def tail = t.fastTail(35, Charset.forName("Shift_JIS")) - assert tail.equals("\u30e2"*34+"\n") - assert tail.length()==35 - - // add one more byte to force fastTail to read from one byte ahead - // between this and the previous case, it should start parsing text incorrectly, until it hits NL - // where it comes back in sync - f.append([0x0A] as byte[]) - - tail = t.fastTail(35, Charset.forName("Shift_JIS")) - assert tail.equals("\u30e2"*33+"\n\n") - assert tail.length()==35 - } - - def newFile() { - def f = File.createTempFile("foo", "txt") - files.add(f) - return f - } -} From 6a32a20fcd71576f850110182ed0c06d9305b43d Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 18:17:58 -0500 Subject: [PATCH 44/85] HistoryWidgetTest --- .../hudson/widgets/HistoryWidgetTest.groovy | 28 ------------------- .../hudson/widgets/HistoryWidgetTest.java | 27 ++++++++++++++++++ 2 files changed, 27 insertions(+), 28 deletions(-) delete mode 100644 test/src/test/groovy/hudson/widgets/HistoryWidgetTest.groovy create mode 100644 test/src/test/java/hudson/widgets/HistoryWidgetTest.java diff --git a/test/src/test/groovy/hudson/widgets/HistoryWidgetTest.groovy b/test/src/test/groovy/hudson/widgets/HistoryWidgetTest.groovy deleted file mode 100644 index 9d32acfa6b..0000000000 --- a/test/src/test/groovy/hudson/widgets/HistoryWidgetTest.groovy +++ /dev/null @@ -1,28 +0,0 @@ -package hudson.widgets - -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.JenkinsRule - -/** - * @author Kohsuke Kawaguchi - */ -class HistoryWidgetTest { - - @Rule - public JenkinsRule j = new JenkinsRule() - - @Test - @Issue("JENKINS-15499") - void moreLink() { - def p = j.createFreeStyleProject() - for (x in 1..3) { - j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - } - - def wc = j.createWebClient() - wc.javaScriptEnabled = false - wc.goTo("job/${p.name}/buildHistory/all") - } -} diff --git a/test/src/test/java/hudson/widgets/HistoryWidgetTest.java b/test/src/test/java/hudson/widgets/HistoryWidgetTest.java new file mode 100644 index 0000000000..fd28e63f19 --- /dev/null +++ b/test/src/test/java/hudson/widgets/HistoryWidgetTest.java @@ -0,0 +1,27 @@ +package hudson.widgets; + +import hudson.model.FreeStyleProject; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +public class HistoryWidgetTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + @Issue("JENKINS-15499") + public void moreLink() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + for (int x = 0; x < 3; x++) { + j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + } + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.setJavaScriptEnabled(false); + wc.goTo("job/" + p.getName() + "/buildHistory/all"); + } + +} From 7bcb30cb8e85af0e66a7f632cbb3a1314caf09d4 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 18:31:02 -0500 Subject: [PATCH 45/85] Jenkins19124Test --- .../jenkins/bugs/Jenkins19124Test.groovy | 89 ------------------ .../java/jenkins/bugs/Jenkins19124Test.java | 94 +++++++++++++++++++ 2 files changed, 94 insertions(+), 89 deletions(-) delete mode 100644 test/src/test/groovy/jenkins/bugs/Jenkins19124Test.groovy create mode 100644 test/src/test/java/jenkins/bugs/Jenkins19124Test.java diff --git a/test/src/test/groovy/jenkins/bugs/Jenkins19124Test.groovy b/test/src/test/groovy/jenkins/bugs/Jenkins19124Test.groovy deleted file mode 100644 index 352b3144f0..0000000000 --- a/test/src/test/groovy/jenkins/bugs/Jenkins19124Test.groovy +++ /dev/null @@ -1,89 +0,0 @@ -package jenkins.bugs - -import com.gargoylesoftware.htmlunit.WebClientUtil -import hudson.Launcher -import hudson.model.AbstractBuild -import hudson.model.AbstractProject -import hudson.model.BuildListener -import hudson.tasks.BuildStepDescriptor -import hudson.tasks.Builder -import hudson.util.FormValidation -import hudson.util.ListBoxModel -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.JenkinsRule -import org.jvnet.hudson.test.TestExtension -import org.kohsuke.stapler.QueryParameter - -import javax.inject.Inject - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class Jenkins19124Test { - @Rule - public JenkinsRule j = new JenkinsRule(); - - @Inject - DescriptorImpl d; - - @Issue("JENKINS-19124") - @Test - public void interrelatedFormValidation() { - j.jenkins.injector.injectMembers(this); - - def p = j.createFreeStyleProject(); - p.buildersList.add(new Foo()); - - def wc = j.createWebClient(); - def c = wc.getPage(p, "configure"); - c.getElementByName("_.alpha").valueAttribute = "hello"; - WebClientUtil.waitForJSExec(wc); - assert d.alpha=="hello"; - assert d.bravo=="2"; - - c.getElementByName("_.bravo").setSelectedAttribute("1",true); - WebClientUtil.waitForJSExec(wc); - assert d.alpha=="hello"; - assert d.bravo=="1"; - } - - public static class Foo extends Builder { - String getAlpha() { return "alpha"; } - String getBravo() { return "2"; } - - @Override - boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { - return true; - } - - private Object writeReplace() { return new Object(); } - } - - @TestExtension - public static class DescriptorImpl extends BuildStepDescriptor { - String alpha,bravo; - - public DescriptorImpl() { - super(Foo.class) - } - - FormValidation doCheckAlpha(@QueryParameter String value, @QueryParameter String bravo) { - this.alpha = value; - this.bravo = bravo; - return FormValidation.ok(); - } - - ListBoxModel doFillBravoItems() { - return new ListBoxModel().add("1").add("2").add("3"); - } - - @Override - boolean isApplicable(Class jobType) { - return true; - } - } -} diff --git a/test/src/test/java/jenkins/bugs/Jenkins19124Test.java b/test/src/test/java/jenkins/bugs/Jenkins19124Test.java new file mode 100644 index 0000000000..4f976f85ba --- /dev/null +++ b/test/src/test/java/jenkins/bugs/Jenkins19124Test.java @@ -0,0 +1,94 @@ +package jenkins.bugs; + +import com.gargoylesoftware.htmlunit.WebClientUtil; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.html.HtmlSelect; +import com.gargoylesoftware.htmlunit.html.HtmlTextInput; +import hudson.Launcher; +import hudson.model.AbstractBuild; +import hudson.model.AbstractProject; +import hudson.model.BuildListener; +import hudson.model.FreeStyleProject; +import hudson.tasks.BuildStepDescriptor; +import hudson.tasks.Builder; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; +import java.io.IOException; +import javax.inject.Inject; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.QueryParameter; + +public class Jenkins19124Test { + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Inject + public Foo.DescriptorImpl d; + + @Issue("JENKINS-19124") + @Test + public void interrelatedFormValidation() throws Exception { + j.jenkins.getInjector().injectMembers(this); + + FreeStyleProject p = j.createFreeStyleProject(); + p.getBuildersList().add(new Foo()); + + JenkinsRule.WebClient wc = j.createWebClient(); + HtmlPage c = wc.getPage(p, "configure"); + HtmlTextInput alpha = c.getElementByName("_.alpha"); + alpha.setValueAttribute("hello"); + WebClientUtil.waitForJSExec(wc); + assertEquals("hello", d.alpha); + assertEquals("2", d.bravo); + + HtmlSelect bravo = c.getElementByName("_.bravo"); + bravo.setSelectedAttribute("1", true); + WebClientUtil.waitForJSExec(wc); + assertEquals("hello", d.alpha); + assertEquals("1", d.bravo); + } + + public static class Foo extends Builder { + + public String getAlpha() { + return "alpha"; + } + public String getBravo() { + return "2"; + } + + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + return true; + } + + @TestExtension + public static class DescriptorImpl extends BuildStepDescriptor { + + String alpha, bravo; + + public FormValidation doCheckAlpha(@QueryParameter String value, @QueryParameter String bravo) { + this.alpha = value; + this.bravo = bravo; + return FormValidation.ok(); + } + + public ListBoxModel doFillBravoItems() { + return new ListBoxModel().add("1").add("2").add("3"); + } + + @Override + public boolean isApplicable(Class jobType) { + return true; + } + + } + + } + +} From fcedf647d7c439e08f7ffb975f8af6d822034c6c Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 18:37:28 -0500 Subject: [PATCH 46/85] PeepholePermalinkTest --- .../model/PeepholePermalinkTest.groovy | 100 ------------------ .../jenkins/model/PeepholePermalinkTest.java | 99 +++++++++++++++++ 2 files changed, 99 insertions(+), 100 deletions(-) delete mode 100644 test/src/test/groovy/jenkins/model/PeepholePermalinkTest.groovy create mode 100644 test/src/test/java/jenkins/model/PeepholePermalinkTest.java diff --git a/test/src/test/groovy/jenkins/model/PeepholePermalinkTest.groovy b/test/src/test/groovy/jenkins/model/PeepholePermalinkTest.groovy deleted file mode 100644 index a500677a9f..0000000000 --- a/test/src/test/groovy/jenkins/model/PeepholePermalinkTest.groovy +++ /dev/null @@ -1,100 +0,0 @@ -package jenkins.model - -import static org.junit.Assert.assertTrue - -import org.junit.Assume; - -import hudson.Functions -import hudson.Util -import hudson.model.Run -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.FailureBuilder -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.JenkinsRule - -/** - * @author Kohsuke Kawaguchi - */ -class PeepholePermalinkTest { - - @Rule - public JenkinsRule j = new JenkinsRule() - - /** - * Basic operation of the permalink generation. - */ - @Test - void basics() { - Assume.assumeFalse("can't run on windows because we rely on symlinks", Functions.isWindows()); - - def p = j.createFreeStyleProject() - def b1 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - - def lsb = new File(p.buildDir, "lastSuccessfulBuild") - def lfb = new File(p.buildDir, "lastFailedBuild") - - assertLink(lsb,b1) - - // now another build that fails - p.buildersList.add(new FailureBuilder()) - def b2 = p.scheduleBuild2(0).get() - - assertLink(lsb,b1) - assertLink(lfb,b2) - - // one more build and this time it succeeds - p.buildersList.clear() - def b3 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - - assertLink(lsb,b3) - assertLink(lfb,b2) - - // delete b3 and symlinks should update properly - b3.delete() - assertLink(lsb,b1) - assertLink(lfb,b2) - - b1.delete() - assertLink(lsb,null) - assertLink(lfb,b2) - - b2.delete() - assertLink(lsb,null) - assertLink(lfb,null) - } - - def assertLink(File symlink, Run build) { - assert Util.resolveSymlink(symlink)==(build==null ? "-1" : build.number as String); - } - - /** - * job/JOBNAME/lastStable and job/JOBNAME/lastSuccessful symlinks that we used to generate should still work - */ - @Test - void legacyCompatibility() { - Assume.assumeFalse("can't run on windows because we rely on symlinks", Functions.isWindows()); - - def p = j.createFreeStyleProject() - def b1 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - - ["lastStable","lastSuccessful"].each { n -> - // test if they both point to b1 - assert new File(p.rootDir,"$n/build.xml").length() == new File(b1.rootDir,"build.xml").length() - } - } - - @Test - @Issue("JENKINS-19034") - void rebuildBuildNumberPermalinks() { - def p = j.createFreeStyleProject() - def b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - File f = new File(p.getBuildDir(), "1") - // assertTrue(Util.isSymlink(f)) - f.delete() - PeepholePermalink link = p.getPermalinks().find({l -> l instanceof PeepholePermalink}) - println(link) - link.updateCache(p, b) - assertTrue("build symlink hasn't been restored", f.exists()) - } -} diff --git a/test/src/test/java/jenkins/model/PeepholePermalinkTest.java b/test/src/test/java/jenkins/model/PeepholePermalinkTest.java new file mode 100644 index 0000000000..c25e276b4e --- /dev/null +++ b/test/src/test/java/jenkins/model/PeepholePermalinkTest.java @@ -0,0 +1,99 @@ +package jenkins.model; + +import hudson.Functions; +import hudson.Util; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.Run; +import java.io.File; +import static org.junit.Assert.*; +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.FailureBuilder; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +public class PeepholePermalinkTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + /** + * Basic operation of the permalink generation. + */ + @Test + public void basics() throws Exception { + Assume.assumeFalse("can't run on windows because we rely on symlinks", Functions.isWindows()); + + FreeStyleProject p = j.createFreeStyleProject(); + FreeStyleBuild b1 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + + File lsb = new File(p.getBuildDir(), "lastSuccessfulBuild"); + File lfb = new File(p.getBuildDir(), "lastFailedBuild"); + + assertLink(lsb, b1); + + // now another build that fails + p.getBuildersList().add(new FailureBuilder()); + FreeStyleBuild b2 = p.scheduleBuild2(0).get(); + + assertLink(lsb, b1); + assertLink(lfb, b2); + + // one more build and this time it succeeds + p.getBuildersList().clear(); + FreeStyleBuild b3 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + + assertLink(lsb, b3); + assertLink(lfb, b2); + + // delete b3 and symlinks should update properly + b3.delete(); + assertLink(lsb, b1); + assertLink(lfb, b2); + + b1.delete(); + assertLink(lsb, null); + assertLink(lfb, b2); + + b2.delete(); + assertLink(lsb, null); + assertLink(lfb, null); + } + + private void assertLink(File symlink, Run build) throws Exception { + assertEquals(build == null ? "-1" : Integer.toString(build.getNumber()), Util.resolveSymlink(symlink)); + } + + /** + * job/JOBNAME/lastStable and job/JOBNAME/lastSuccessful symlinks that we + * used to generate should still work + */ + @Test + public void legacyCompatibility() throws Exception { + Assume.assumeFalse("can't run on windows because we rely on symlinks", Functions.isWindows()); + + FreeStyleProject p = j.createFreeStyleProject(); + FreeStyleBuild b1 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + + for (String n : new String[] {"lastStable", "lastSuccessful"}) { + // test if they both point to b1 + assertEquals(new File(p.getRootDir(), n + "/build.xml").length(), new File(b1.getRootDir(), "build.xml").length()); + } + } + + @Test + @Issue("JENKINS-19034") + public void rebuildBuildNumberPermalinks() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + FreeStyleBuild b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + File f = new File(p.getBuildDir(), "1"); + // assertTrue(Util.isSymlink(f)) + f.delete(); + PeepholePermalink link = (PeepholePermalink) p.getPermalinks().stream().filter(l -> l instanceof PeepholePermalink).findAny().get(); + link.updateCache(p, b); + assertTrue("build symlink hasn't been restored", f.exists()); + } + +} From 26e2001cf2d92cd54425c89e1e400990e61a7f32 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 18:40:13 -0500 Subject: [PATCH 47/85] ApplyButtonTest --- .../groovy/lib/form/ApplyButtonTest.groovy | 37 ---------------- .../test/java/lib/form/ApplyButtonTest.java | 42 +++++++++++++++++++ 2 files changed, 42 insertions(+), 37 deletions(-) delete mode 100644 test/src/test/groovy/lib/form/ApplyButtonTest.groovy create mode 100644 test/src/test/java/lib/form/ApplyButtonTest.java diff --git a/test/src/test/groovy/lib/form/ApplyButtonTest.groovy b/test/src/test/groovy/lib/form/ApplyButtonTest.groovy deleted file mode 100644 index ceae5ed940..0000000000 --- a/test/src/test/groovy/lib/form/ApplyButtonTest.groovy +++ /dev/null @@ -1,37 +0,0 @@ -package lib.form - -import hudson.markup.RawHtmlMarkupFormatter -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.JenkinsRule - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class ApplyButtonTest { - @Rule public JenkinsRule j = new JenkinsRule(); - - /** - * Editing code mirror should still gets reflected when you click apply. - */ - @Test @Issue("JENKINS-18436") - public void editDescription() { - j.jenkins.markupFormatter = RawHtmlMarkupFormatter.INSTANCE // need something using CodeMirror - def p = j.createFreeStyleProject() - def b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - - def config = j.createWebClient().getPage(b, "configure") - def form = config.getFormByName("config") - // HtmlUnit doesn't have JSON, so we need to emulate one - config.executeJavaScript(getClass().getResource("JSON.js").text) - // it's hard to emulate the keytyping, so we just set the value into codemirror and test if this gets - // reflected back into TEXTAREA - config.executeJavaScript("document.getElementsByTagName('TEXTAREA')[0].codemirrorObject.setLine(0,'foobar')") - j.getButtonByCaption(form,"Apply").click() - - assert "foobar"==b.description - } -} diff --git a/test/src/test/java/lib/form/ApplyButtonTest.java b/test/src/test/java/lib/form/ApplyButtonTest.java new file mode 100644 index 0000000000..028e135da0 --- /dev/null +++ b/test/src/test/java/lib/form/ApplyButtonTest.java @@ -0,0 +1,42 @@ +package lib.form; + +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import hudson.markup.RawHtmlMarkupFormatter; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import org.apache.commons.io.IOUtils; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +public class ApplyButtonTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + /** + * Editing code mirror should still gets reflected when you click apply. + */ + @Test + @Issue("JENKINS-18436") + public void editDescription() throws Exception { + j.jenkins.setMarkupFormatter(RawHtmlMarkupFormatter.INSTANCE); // need something using CodeMirror + FreeStyleProject p = j.createFreeStyleProject(); + FreeStyleBuild b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + + HtmlPage config = j.createWebClient().getPage(b, "configure"); + HtmlForm form = config.getFormByName("config"); + // HtmlUnit doesn't have JSON, so we need to emulate one + config.executeJavaScript(IOUtils.toString(ApplyButtonTest.class.getResource("JSON.js"))); + // it's hard to emulate the keytyping, so we just set the value into codemirror and test if this gets + // reflected back into TEXTAREA + config.executeJavaScript("document.getElementsByTagName('TEXTAREA')[0].codemirrorObject.setLine(0,'foobar')"); + j.getButtonByCaption(form, "Apply").click(); + + assertEquals("foobar", b.getDescription()); + } + +} From 0bc7b46055e7e6d0201b902c440b12afc293045f Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Dec 2017 18:49:22 -0500 Subject: [PATCH 48/85] Last trace of gmaven gone! --- test/pom.xml | 49 ------- .../test/groovy/lib/form/TextAreaTest.groovy | 128 ----------------- .../jenkins/bugs/Jenkins41511Test.java | 0 test/src/test/java/lib/form/TextAreaTest.java | 129 ++++++++++++++++++ 4 files changed, 129 insertions(+), 177 deletions(-) delete mode 100644 test/src/test/groovy/lib/form/TextAreaTest.groovy rename test/src/test/{groovy => java}/jenkins/bugs/Jenkins41511Test.java (100%) create mode 100644 test/src/test/java/lib/form/TextAreaTest.java diff --git a/test/pom.xml b/test/pom.xml index 6d70dd8805..22df2c180e 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -241,55 +241,6 @@ THE SOFTWARE. ${concurrency} - - org.codehaus.gmaven - gmaven-plugin - 1.5-jenkins-3 - - - default - - - generateTestStubs - testCompile - - - - - - org.apache.ant - ant - 1.8.0 - - - - org.apache.ant - ant-launcher - 1.8.0 - - - org.apache.ant - ant-junit - 1.7.0 - - - - javax.servlet - javax.servlet-api - 3.1.0 - - - org.codehaus.gmaven.runtime - gmaven-runtime-2.0 - 1.5-jenkins-3 - - - - - 2.0 - - maven-deploy-plugin diff --git a/test/src/test/groovy/lib/form/TextAreaTest.groovy b/test/src/test/groovy/lib/form/TextAreaTest.groovy deleted file mode 100644 index f5edc5cac3..0000000000 --- a/test/src/test/groovy/lib/form/TextAreaTest.groovy +++ /dev/null @@ -1,128 +0,0 @@ -package lib.form - -import hudson.model.AbstractProject -import hudson.tasks.BuildStepDescriptor -import hudson.tasks.Builder -import hudson.util.FormValidation -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.JenkinsRule -import org.jvnet.hudson.test.TestExtension -import org.kohsuke.stapler.DataBoundConstructor -import org.kohsuke.stapler.QueryParameter - -import javax.inject.Inject - -import static org.junit.Assert.* - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class TextAreaTest { - @Rule - public JenkinsRule j = new JenkinsRule(); - - @Inject - TestBuilder.DescriptorImpl d; - - @Test @Issue("JENKINS-19457") - public void validation() { - j.jenkins.injector.injectMembers(this) - def p = j.createFreeStyleProject() - p.buildersList.add(new TestBuilder()) - j.configRoundtrip(p) - assert d.text1=="This is text1" - assert d.text2=="Received This is text1" - } - - public static class TestBuilder extends Builder { - @DataBoundConstructor - TestBuilder() { - } - - public String getText1() { return "This is text1" } - public String getText2() { return "This is text2" } - - @TestExtension - public static class DescriptorImpl extends BuildStepDescriptor { - def text1,text2; - - @Override - boolean isApplicable(Class jobType) { - return true; - } - - FormValidation doCheckText1(@QueryParameter String value) { - this.text1 = value; - return FormValidation.ok(); - } - - FormValidation doCheckText2(@QueryParameter String text1) { - this.text2 = "Received "+text1; - return FormValidation.ok(); - } - } - - } - - @Issue("JENKINS-27505") - @Test - public void testText() { - T1:{ - def TEXT_TO_TEST = "some\nvalue\n"; - def p = j.createFreeStyleProject(); - def target = new TextareaTestBuilder(TEXT_TO_TEST); - p.buildersList.add(target); - j.configRoundtrip(p); - j.assertEqualDataBoundBeans(target, p.getBuildersList().get(TextareaTestBuilder.class)); - } - - // test for a textarea beginning with a empty line. - T2:{ - def TEXT_TO_TEST = "\nbegin\n\nwith\nempty\nline\n\n"; - def p = j.createFreeStyleProject(); - def target = new TextareaTestBuilder(TEXT_TO_TEST); - p.buildersList.add(target); - j.configRoundtrip(p); - j.assertEqualDataBoundBeans(target, p.getBuildersList().get(TextareaTestBuilder.class)); - } - - // test for a textarea beginning with two empty lines. - T3:{ - def TEXT_TO_TEST = "\n\nbegin\n\nwith\ntwo\nempty\nline\n\n"; - def p = j.createFreeStyleProject(); - def target = new TextareaTestBuilder(TEXT_TO_TEST); - p.buildersList.add(target); - j.configRoundtrip(p); - j.assertEqualDataBoundBeans(target, p.getBuildersList().get(TextareaTestBuilder.class)); - } - } - - public static class TextareaTestBuilder extends Builder { - private text; - - @DataBoundConstructor - TextareaTestBuilder(String text) { - this.text = text; - } - - public String getText() { return text; } - - @TestExtension - public static class DescriptorImpl extends BuildStepDescriptor { - @Override - boolean isApplicable(Class jobType) { - return true; - } - - @Override - String getDisplayName() { - return this.class.name; - } - } - - } -} diff --git a/test/src/test/groovy/jenkins/bugs/Jenkins41511Test.java b/test/src/test/java/jenkins/bugs/Jenkins41511Test.java similarity index 100% rename from test/src/test/groovy/jenkins/bugs/Jenkins41511Test.java rename to test/src/test/java/jenkins/bugs/Jenkins41511Test.java diff --git a/test/src/test/java/lib/form/TextAreaTest.java b/test/src/test/java/lib/form/TextAreaTest.java new file mode 100644 index 0000000000..9324554af0 --- /dev/null +++ b/test/src/test/java/lib/form/TextAreaTest.java @@ -0,0 +1,129 @@ +package lib.form; + +import hudson.model.AbstractProject; +import hudson.model.FreeStyleProject; +import hudson.tasks.BuildStepDescriptor; +import hudson.tasks.Builder; +import hudson.util.FormValidation; +import javax.inject.Inject; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; + +public class TextAreaTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Inject + public TestBuilder.DescriptorImpl d; + + @Test + @Issue("JENKINS-19457") + public void validation() throws Exception { + j.jenkins.getInjector().injectMembers(this); + FreeStyleProject p = j.createFreeStyleProject(); + p.getBuildersList().add(new TestBuilder()); + j.configRoundtrip(p); + assertEquals("This is text1", d.text1); + assertEquals("Received This is text1", d.text2); + } + + public static class TestBuilder extends Builder { + + @DataBoundConstructor + public TestBuilder() {} + + public String getText1() { + return "This is text1"; + } + public String getText2() { + return "This is text2"; + } + + @TestExtension + public static class DescriptorImpl extends BuildStepDescriptor { + + String text1, text2; + + @Override + public boolean isApplicable(Class jobType) { + return true; + } + + public FormValidation doCheckText1(@QueryParameter String value) { + this.text1 = value; + return FormValidation.ok(); + } + + public FormValidation doCheckText2(@QueryParameter String text1) { + this.text2 = "Received " + text1; + return FormValidation.ok(); + } + } + + } + + @Issue("JENKINS-27505") + @Test + public void text() throws Exception { + T1: { + String TEXT_TO_TEST = "some\nvalue\n"; + FreeStyleProject p = j.createFreeStyleProject(); + TextareaTestBuilder target = new TextareaTestBuilder(TEXT_TO_TEST); + p.getBuildersList().add(target); + j.configRoundtrip(p); + j.assertEqualDataBoundBeans(target, p.getBuildersList().get(TextareaTestBuilder.class)); + } + + // test for a textarea beginning with a empty line. + T2: { + String TEXT_TO_TEST = "\nbegin\n\nwith\nempty\nline\n\n"; + FreeStyleProject p = j.createFreeStyleProject(); + TextareaTestBuilder target = new TextareaTestBuilder(TEXT_TO_TEST); + p.getBuildersList().add(target); + j.configRoundtrip(p); + j.assertEqualDataBoundBeans(target, p.getBuildersList().get(TextareaTestBuilder.class)); + } + + // test for a textarea beginning with two empty lines. + T3: { + String TEXT_TO_TEST = "\n\nbegin\n\nwith\ntwo\nempty\nline\n\n"; + FreeStyleProject p = j.createFreeStyleProject(); + TextareaTestBuilder target = new TextareaTestBuilder(TEXT_TO_TEST); + p.getBuildersList().add(target); + j.configRoundtrip(p); + j.assertEqualDataBoundBeans(target, p.getBuildersList().get(TextareaTestBuilder.class)); + } + } + + public static class TextareaTestBuilder extends Builder { + + private String text; + + @DataBoundConstructor + public TextareaTestBuilder(String text) { + this.text = text; + } + + public String getText() { + return text; + } + + @TestExtension + public static class DescriptorImpl extends BuildStepDescriptor { + @Override + public boolean isApplicable(Class jobType) { + return true; + } + + } + + } + +} From e20b0496149669f3a0f05cabd1a06eb3a469e935 Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Sun, 10 Dec 2017 02:42:20 +0100 Subject: [PATCH 49/85] [JENKINS-34254] Fix RequirePOST form --- core/pom.xml | 2 +- .../hudson/security/csrf/CrumbFilter.java | 8 ++++ .../security/csrf/CrumbFilter/retry.jelly | 42 +++++++++++++++++++ .../csrf/CrumbFilter/retry.properties | 4 ++ 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly create mode 100644 core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.properties diff --git a/core/pom.xml b/core/pom.xml index a4b053eee5..2a06499268 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -39,7 +39,7 @@ THE SOFTWARE. true - 1.253 + 1.254-SNAPSHOT 2.5.6.SEC03 2.4.11 diff --git a/core/src/main/java/hudson/security/csrf/CrumbFilter.java b/core/src/main/java/hudson/security/csrf/CrumbFilter.java index e2b2790ef0..bcaca93132 100644 --- a/core/src/main/java/hudson/security/csrf/CrumbFilter.java +++ b/core/src/main/java/hudson/security/csrf/CrumbFilter.java @@ -8,6 +8,8 @@ package hudson.security.csrf; import hudson.util.MultipartFormDataParser; import jenkins.model.Jenkins; import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; +import org.kohsuke.stapler.ForwardToView; +import org.kohsuke.stapler.interceptor.RequirePOST; import java.io.IOException; import java.util.Enumeration; @@ -42,6 +44,12 @@ public class CrumbFilter implements Filter { } public void init(FilterConfig filterConfig) throws ServletException { + RequirePOST.Processor.registerErrorHandler(new RequirePOST.ErrorHandler() { + @Override + public ForwardToView getForwardView() { + return new ForwardToView(CrumbFilter.this, "retry"); + } + }); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { diff --git a/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly b/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly new file mode 100644 index 0000000000..bcd3e5a61c --- /dev/null +++ b/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly @@ -0,0 +1,42 @@ + + + + + + + + +

${%This URL requires POST}

+

+ ${%blurb} +

+

${requestURL}

+

${%warning}

+ + + + + + diff --git a/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.properties b/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.properties new file mode 100644 index 0000000000..0e47fdc8d5 --- /dev/null +++ b/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.properties @@ -0,0 +1,4 @@ +blurb = The URL you're trying to access requires that requests be sent using POST (like a form submission). \ + The button below allows you to retry accessing this URL using POST. \ + URL being accessed: +warning = If you were sent here from an untrusted source, please proceed with caution. \ No newline at end of file From 30ab4481f286a5c33499489dfcb9b3df6587ff38 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Sun, 10 Dec 2017 05:27:36 +0100 Subject: [PATCH 50/85] [FIXES JENKINS-47439] Setup wizard does not resume after restart (#3166) * [JENKINS-47439] Add a failing test On first startup, the setup wizard goes into state NEW and the filter to force display the setup wizard is installed. On second startup, the setup wizard goes into state RESTART (which assumes the setup wizard is done), and the setup wizard is skipped completely. This test expects that state NEW is retained upon restart when nothing is done. * [JENKINS-47439] Persist InstallState In some cases, the heuristics to determine the current setup wizard state are fragile. It is safer to persist the install state so that upon restart, the setup wizard can resume where it was left off. * Missing javadoc and since for new public methods * s/XXX/FIXME/ * Missed that one * Setup wizard filter should be removed when entering a state where setup is complete * Use parameterized logging * Improvements over previous impl * Removed static isUsingSecurityToken. Now only determined from install state. * Call onInstallStateUpdate before InstallState#initializeState as the latter can update state. * Triggering a new build --- .../java/hudson/util/PluginServletFilter.java | 19 +++++ .../java/jenkins/install/InstallState.java | 13 ++- .../java/jenkins/install/InstallUtil.java | 1 - .../java/jenkins/install/SetupWizard.java | 81 ++++++++++++++----- core/src/main/java/jenkins/model/Jenkins.java | 9 ++- .../install/SetupWizardRestartTest.java | 50 ++++++++++++ 6 files changed, 143 insertions(+), 30 deletions(-) create mode 100644 test/src/test/java/jenkins/install/SetupWizardRestartTest.java diff --git a/core/src/main/java/hudson/util/PluginServletFilter.java b/core/src/main/java/hudson/util/PluginServletFilter.java index 9b311aa790..aa2c53973e 100644 --- a/core/src/main/java/hudson/util/PluginServletFilter.java +++ b/core/src/main/java/hudson/util/PluginServletFilter.java @@ -113,6 +113,25 @@ public class PluginServletFilter implements Filter, ExtensionPoint { } } + /** + * Checks whether the given filter is already registered in the chain. + * @param filter the filter to check. + * @return true if the filter is already registered in the chain. + * @since FIXME + */ + public static boolean hasFilter(Filter filter) { + Jenkins j = Jenkins.getInstanceOrNull(); + PluginServletFilter container = null; + if(j != null) { + container = getInstance(j.servletContext); + } + if (j == null || container == null) { + return LEGACY.contains(filter); + } else { + return container.list.contains(filter); + } + } + public static void removeFilter(Filter filter) throws ServletException { Jenkins j = Jenkins.getInstanceOrNull(); if (j==null || getInstance(j.servletContext) == null) { diff --git a/core/src/main/java/jenkins/install/InstallState.java b/core/src/main/java/jenkins/install/InstallState.java index 71790d3c54..939c45f828 100644 --- a/core/src/main/java/jenkins/install/InstallState.java +++ b/core/src/main/java/jenkins/install/InstallState.java @@ -49,7 +49,12 @@ public class InstallState implements ExtensionPoint { * Need InstallState != NEW for tests by default */ @Extension - public static final InstallState UNKNOWN = new InstallState("UNKNOWN", true); + public static final InstallState UNKNOWN = new InstallState("UNKNOWN", true) { + @Override + public void initializeState() { + InstallUtil.proceedToNextStateFrom(this); + } + }; /** * After any setup / restart / etc. hooks are done, states should be running @@ -94,7 +99,7 @@ public class InstallState implements ExtensionPoint { */ @Extension public static final InstallState INITIAL_PLUGINS_INSTALLING = new InstallState("INITIAL_PLUGINS_INSTALLING", false); - + /** * Security setup for a new Jenkins install. */ @@ -106,7 +111,7 @@ public class InstallState implements ExtensionPoint { } catch (Exception e) { throw new RuntimeException(e); } - + InstallUtil.proceedToNextStateFrom(INITIAL_SECURITY_SETUP); } }; @@ -116,7 +121,7 @@ public class InstallState implements ExtensionPoint { */ @Extension public static final InstallState NEW = new InstallState("NEW", false); - + /** * Restart of an existing Jenkins install. */ diff --git a/core/src/main/java/jenkins/install/InstallUtil.java b/core/src/main/java/jenkins/install/InstallUtil.java index e4868e95c1..20d9a68c76 100644 --- a/core/src/main/java/jenkins/install/InstallUtil.java +++ b/core/src/main/java/jenkins/install/InstallUtil.java @@ -92,7 +92,6 @@ public class InstallUtil { */ public static void proceedToNextStateFrom(InstallState prior) { InstallState next = getNextInstallState(prior); - if (Main.isDevelopmentMode) LOGGER.info("Install state transitioning from: " + prior + " to: " + next); if (next != null) { Jenkins.getInstance().setInstallState(next); } diff --git a/core/src/main/java/jenkins/install/SetupWizard.java b/core/src/main/java/jenkins/install/SetupWizard.java index 0f04f903db..f15b468eed 100644 --- a/core/src/main/java/jenkins/install/SetupWizard.java +++ b/core/src/main/java/jenkins/install/SetupWizard.java @@ -64,7 +64,6 @@ import net.sf.json.JSONArray; import net.sf.json.JSONObject; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; -import org.jenkinsci.remoting.engine.JnlpProtocol4Handler; import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.stapler.interceptor.RequirePOST; @@ -77,6 +76,10 @@ import org.kohsuke.stapler.interceptor.RequirePOST; @Restricted(NoExternalUse.class) @Extension public class SetupWizard extends PageDecorator { + public SetupWizard() { + checkFilter(); + } + /** * The security token parameter name */ @@ -84,11 +87,6 @@ public class SetupWizard extends PageDecorator { private static final Logger LOGGER = Logger.getLogger(SetupWizard.class.getName()); - /** - * Used to determine if this was a new install (vs. an upgrade, restart, or otherwise) - */ - private static boolean isUsingSecurityToken = false; - /** * Initialize the setup wizard, this will process any current state initializations */ @@ -168,17 +166,8 @@ public class SetupWizard extends PageDecorator { + "*************************************************************" + ls + "*************************************************************" + ls); } - - try { - PluginServletFilter.addFilter(FORCE_SETUP_WIZARD_FILTER); - // if we're not using security defaults, we should not show the security token screen - // users will likely be sent to a login screen instead - isUsingSecurityToken = isUsingSecurityDefaults(); - } catch (ServletException e) { - throw new RuntimeException("Unable to add PluginServletFilter for the SetupWizard", e); - } } - + try { // Make sure plugin metadata is up to date UpdateCenter.updateDefaultSite(); @@ -186,14 +175,34 @@ public class SetupWizard extends PageDecorator { LOGGER.log(Level.WARNING, e.getMessage(), e); } } - + + private void setUpFilter() { + try { + if (!PluginServletFilter.hasFilter(FORCE_SETUP_WIZARD_FILTER)) { + PluginServletFilter.addFilter(FORCE_SETUP_WIZARD_FILTER); + } + } catch (ServletException e) { + throw new RuntimeException("Unable to add PluginServletFilter for the SetupWizard", e); + } + } + + private void tearDownFilter() { + try { + if (PluginServletFilter.hasFilter(FORCE_SETUP_WIZARD_FILTER)) { + PluginServletFilter.removeFilter(FORCE_SETUP_WIZARD_FILTER); + } + } catch (ServletException e) { + throw new RuntimeException("Unable to remove PluginServletFilter for the SetupWizard", e); + } + } + /** * Indicates a generated password should be used - e.g. this is a new install, no security realm set up */ + @SuppressWarnings("unused") // used by jelly public boolean isUsingSecurityToken() { try { - return isUsingSecurityToken // only ever show the unlock page if using the security token - && !Jenkins.getInstance().getInstallState().isSetupComplete() + return !Jenkins.getInstance().getInstallState().isSetupComplete() && isUsingSecurityDefaults(); } catch (Exception e) { // ignore @@ -487,8 +496,6 @@ public class SetupWizard extends PageDecorator { Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); InstallUtil.saveLastExecVersion(); setCurrentLevel(Jenkins.getVersion()); - PluginServletFilter.removeFilter(FORCE_SETUP_WIZARD_FILTER); - isUsingSecurityToken = false; // this should not be considered new anymore InstallUtil.proceedToNextStateFrom(InstallState.INITIAL_SETUP_COMPLETED); } @@ -508,7 +515,28 @@ public class SetupWizard extends PageDecorator { } return InstallState.valueOf(name); } - + + /** + * Called upon install state update. + * @param state the new install state. + * @since FIXME + */ + public void onInstallStateUpdate(InstallState state) { + if (state.isSetupComplete()) { + tearDownFilter(); + } else { + setUpFilter(); + } + } + + /** + * Returns whether the setup wizard filter is currently registered. + * @since FIXME + */ + public boolean hasSetupWizardFilter() { + return PluginServletFilter.hasFilter(FORCE_SETUP_WIZARD_FILTER); + } + /** * This filter will validate that the security token is provided */ @@ -544,4 +572,13 @@ public class SetupWizard extends PageDecorator { public void destroy() { } }; + + /** + * Sets up the Setup Wizard filter if the current state requires it. + */ + private void checkFilter() { + if (!Jenkins.getInstance().getInstallState().isSetupComplete()) { + setUpFilter(); + } + } } diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 1e61a80d0a..f14ff58b7e 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -328,7 +328,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve /** * The Jenkins instance startup type i.e. NEW, UPGRADE etc */ - private transient InstallState installState = InstallState.UNKNOWN; + private InstallState installState; /** * If we're in the process of an initial setup, @@ -924,7 +924,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve System.exit(0); setupWizard = new SetupWizard(); - InstallUtil.proceedToNextStateFrom(InstallState.UNKNOWN); + getInstallState().initializeState(); launchTcpSlaveAgentListener(); @@ -1033,9 +1033,12 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve public void setInstallState(@Nonnull InstallState newState) { InstallState prior = installState; installState = newState; - if (!prior.equals(newState)) { + LOGGER.log(Main.isDevelopmentMode ? Level.INFO : Level.FINE, "Install state transitioning from: {0} to : {1}", new Object[] { prior, installState }); + if (!newState.equals(prior)) { + getSetupWizard().onInstallStateUpdate(newState); newState.initializeState(); } + saveQuietly(); } /** diff --git a/test/src/test/java/jenkins/install/SetupWizardRestartTest.java b/test/src/test/java/jenkins/install/SetupWizardRestartTest.java new file mode 100644 index 0000000000..474c1772f9 --- /dev/null +++ b/test/src/test/java/jenkins/install/SetupWizardRestartTest.java @@ -0,0 +1,50 @@ +package jenkins.install; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runners.model.Statement; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +import hudson.Main; +import jenkins.model.Jenkins; + +public class SetupWizardRestartTest { + @Rule + public RestartableJenkinsRule rr = new RestartableJenkinsRule(); + + @Issue("JENKINS-47439") + @Test + public void restartKeepsSetupWizardState() { + rr.addStep(new Statement() { + @Override + public void evaluate() throws IOException { + // Modify state so that we get into the same conditions as a real start + Main.isUnitTest = false; + FileUtils.write(InstallUtil.getLastExecVersionFile(), ""); + Jenkins j = rr.j.getInstance(); + // Re-evaluate current state based on the new context + InstallUtil.proceedToNextStateFrom(InstallState.UNKNOWN); + assertEquals("Unexpected install state", InstallState.NEW, j.getInstallState()); + assertTrue("Expecting setup wizard filter to be up", j.getSetupWizard().hasSetupWizardFilter()); + InstallUtil.saveLastExecVersion(); + } + }); + // Check that the state is retained after a restart + rr.addStep(new Statement() { + @Override + public void evaluate() { + Jenkins j = rr.j.getInstance(); + assertEquals("Unexpected install state", InstallState.NEW, j.getInstallState()); + assertTrue("Expecting setup wizard filter to be up after restart", j.getSetupWizard().hasSetupWizardFilter()); + } + }); + } + +} From b27bb928a28ee578122eb4b076c8a3d8d68d878c Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Sun, 10 Dec 2017 10:35:17 +0100 Subject: [PATCH 51/85] [JENKINS-34254] Set HTTP status code for view --- .../main/resources/hudson/security/csrf/CrumbFilter/retry.jelly | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly b/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly index bcd3e5a61c..163e015556 100644 --- a/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly +++ b/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly @@ -24,7 +24,7 @@ THE SOFTWARE. - + From a4608a21ced998f1da374961bc3d7537c9c35b0b Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 11 Dec 2017 13:34:19 -0800 Subject: [PATCH 52/85] [maven-release-plugin] prepare release jenkins-2.94 --- cli/pom.xml | 2 +- core/pom.xml | 2 +- pom.xml | 4 ++-- test/pom.xml | 2 +- war/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index b6771c7daf..905cad44b5 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 2.94-SNAPSHOT + 2.94 cli diff --git a/core/pom.xml b/core/pom.xml index fbc89c67de..1a7057dfd4 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.94-SNAPSHOT + 2.94 jenkins-core diff --git a/pom.xml b/pom.xml index 06e7f7efef..e62d01371e 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.94-SNAPSHOT + 2.94 pom Jenkins main module @@ -58,7 +58,7 @@ THE SOFTWARE. scm:git:git://github.com/jenkinsci/jenkins.git scm:git:ssh://git@github.com/jenkinsci/jenkins.git https://github.com/jenkinsci/jenkins - HEAD + jenkins-2.94 diff --git a/test/pom.xml b/test/pom.xml index 3d0f103d4a..a5c6b5fc22 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.94-SNAPSHOT + 2.94 test diff --git a/war/pom.xml b/war/pom.xml index effbb6ae30..208c8a92b6 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.94-SNAPSHOT + 2.94 jenkins-war From 78adf02addc47ccb2629c9bb9101a8dd630b22a5 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 11 Dec 2017 13:34:20 -0800 Subject: [PATCH 53/85] [maven-release-plugin] prepare for next development iteration --- cli/pom.xml | 2 +- core/pom.xml | 2 +- pom.xml | 4 ++-- test/pom.xml | 2 +- war/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 905cad44b5..0fca80941a 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 2.94 + 2.95-SNAPSHOT cli diff --git a/core/pom.xml b/core/pom.xml index 1a7057dfd4..a3a7bc6bff 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.94 + 2.95-SNAPSHOT jenkins-core diff --git a/pom.xml b/pom.xml index e62d01371e..498b67a88e 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.94 + 2.95-SNAPSHOT pom Jenkins main module @@ -58,7 +58,7 @@ THE SOFTWARE. scm:git:git://github.com/jenkinsci/jenkins.git scm:git:ssh://git@github.com/jenkinsci/jenkins.git https://github.com/jenkinsci/jenkins - jenkins-2.94 + HEAD diff --git a/test/pom.xml b/test/pom.xml index a5c6b5fc22..f38b1d2a09 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.94 + 2.95-SNAPSHOT test diff --git a/war/pom.xml b/war/pom.xml index 208c8a92b6..f9bb67064b 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.94 + 2.95-SNAPSHOT jenkins-war From b6088dc456e88a369a617fd90b267d15a1a9fe6b Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 11 Dec 2017 17:44:55 -0500 Subject: [PATCH 54/85] Deprecating Service in favor of ServiceLoader. --- core/src/main/java/hudson/PluginManager.java | 4 +++- core/src/main/java/hudson/init/InitStrategy.java | 14 ++++++++------ core/src/main/java/hudson/util/Service.java | 6 +++--- core/src/main/java/jenkins/InitReactorRunner.java | 5 +++-- .../java/jenkins/security/ConfidentialStore.java | 14 ++++++++------ 5 files changed, 25 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index c816805ba6..87153180f6 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -120,7 +120,6 @@ import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; -import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.TreeMap; @@ -144,6 +143,7 @@ import hudson.util.FormValidation; import java.io.ByteArrayInputStream; import java.net.JarURLConnection; import java.net.URLConnection; +import java.util.ServiceLoader; import java.util.jar.JarEntry; import static java.util.logging.Level.FINE; @@ -1195,7 +1195,9 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas /** * Discover all the service provider implementations of the given class, * via META-INF/services. + * @deprecated Use {@link ServiceLoader} instead, or (more commonly) {@link ExtensionList}. */ + @Deprecated public Collection> discover( Class spi ) { Set> result = new HashSet>(); diff --git a/core/src/main/java/hudson/init/InitStrategy.java b/core/src/main/java/hudson/init/InitStrategy.java index 0d1e037e6a..9d7d287c43 100644 --- a/core/src/main/java/hudson/init/InitStrategy.java +++ b/core/src/main/java/hudson/init/InitStrategy.java @@ -17,7 +17,8 @@ import hudson.PluginManager; import jenkins.util.SystemProperties; import hudson.util.DirScanner; import hudson.util.FileVisitor; -import hudson.util.Service; +import java.util.Iterator; +import java.util.ServiceLoader; /** * Strategy pattern of the various key decision making during the Jenkins initialization. @@ -113,11 +114,12 @@ public class InitStrategy { * Obtains the instance to be used. */ public static InitStrategy get(ClassLoader cl) throws IOException { - List r = Service.loadInstances(cl, InitStrategy.class); - if (r.isEmpty()) return new InitStrategy(); // default - - InitStrategy s = r.get(0); - LOGGER.fine("Using "+s+" as InitStrategy"); + Iterator it = ServiceLoader.load(InitStrategy.class, cl).iterator(); + if (!it.hasNext()) { + return new InitStrategy(); // default + } + InitStrategy s = it.next(); + LOGGER.log(Level.FINE, "Using {0} as InitStrategy", s); return s; } diff --git a/core/src/main/java/hudson/util/Service.java b/core/src/main/java/hudson/util/Service.java index 6275b07e3f..a979324f33 100644 --- a/core/src/main/java/hudson/util/Service.java +++ b/core/src/main/java/hudson/util/Service.java @@ -31,6 +31,7 @@ import java.util.Collection; import java.util.Enumeration; import java.util.List; import java.util.ArrayList; +import java.util.ServiceLoader; import java.util.logging.Level; import java.util.logging.Logger; import static java.util.logging.Level.WARNING; @@ -39,11 +40,10 @@ import static java.util.logging.Level.WARNING; * Load classes by looking up META-INF/services. * * @author Kohsuke Kawaguchi + * @deprecated use {@link ServiceLoader} instead. */ +@Deprecated public class Service { - /** - * Poorman's clone of JDK6 ServiceLoader. - */ public static List loadInstances(ClassLoader classLoader, Class type) throws IOException { List result = new ArrayList(); diff --git a/core/src/main/java/jenkins/InitReactorRunner.java b/core/src/main/java/jenkins/InitReactorRunner.java index c5c2bfb844..aa6ef83ab7 100644 --- a/core/src/main/java/jenkins/InitReactorRunner.java +++ b/core/src/main/java/jenkins/InitReactorRunner.java @@ -1,11 +1,11 @@ package jenkins; +import com.google.common.collect.Lists; import jenkins.util.SystemProperties; import hudson.init.InitMilestone; import hudson.init.InitReactorListener; import hudson.util.DaemonThreadFactory; import hudson.util.NamingThreadFactory; -import hudson.util.Service; import jenkins.model.Configuration; import jenkins.model.Jenkins; import org.jvnet.hudson.reactor.Milestone; @@ -16,6 +16,7 @@ import org.jvnet.hudson.reactor.Task; import java.io.IOException; import java.util.List; +import java.util.ServiceLoader; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; @@ -59,7 +60,7 @@ public class InitReactorRunner { * As such there's no way for plugins to participate into this process. */ private ReactorListener buildReactorListener() throws IOException { - List r = (List) Service.loadInstances(Thread.currentThread().getContextClassLoader(), InitReactorListener.class); + List r = Lists.newArrayList(ServiceLoader.load(InitReactorListener.class, Thread.currentThread().getContextClassLoader())); r.add(new ReactorListener() { final Level level = Level.parse( Configuration.getStringConfigParameter("initLogLevel", "FINE") ); public void onTaskStarted(Task t) { diff --git a/core/src/main/java/jenkins/security/ConfidentialStore.java b/core/src/main/java/jenkins/security/ConfidentialStore.java index 6e79d3d70a..1a8a152f82 100644 --- a/core/src/main/java/jenkins/security/ConfidentialStore.java +++ b/core/src/main/java/jenkins/security/ConfidentialStore.java @@ -4,7 +4,6 @@ import hudson.Extension; import hudson.Lookup; import hudson.init.InitMilestone; import hudson.util.Secret; -import hudson.util.Service; import jenkins.model.Jenkins; import org.kohsuke.MetaInfServices; @@ -12,7 +11,9 @@ import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import java.io.IOException; import java.security.SecureRandom; -import java.util.List; +import java.util.Iterator; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; import java.util.logging.Level; import java.util.logging.Logger; @@ -68,10 +69,11 @@ public abstract class ConfidentialStore { ConfidentialStore cs = lookup.get(ConfidentialStore.class); if (cs==null) { try { - List r = (List) Service.loadInstances(ConfidentialStore.class.getClassLoader(), ConfidentialStore.class); - if (!r.isEmpty()) - cs = r.get(0); - } catch (IOException e) { + Iterator it = ServiceLoader.load(ConfidentialStore.class, ConfidentialStore.class.getClassLoader()).iterator(); + if (it.hasNext()) { + cs = it.next(); + } + } catch (ServiceConfigurationError e) { LOGGER.log(Level.WARNING, "Failed to list up ConfidentialStore implementations",e); // fall through } From 2f45a2332b96a133ef269e2b621617016c98fdfa Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Tue, 12 Dec 2017 00:12:23 +0100 Subject: [PATCH 55/85] [JENKINS-34254] Adapt to upstream change using ServiceLoader --- .../java/hudson/security/csrf/CrumbFilter.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/hudson/security/csrf/CrumbFilter.java b/core/src/main/java/hudson/security/csrf/CrumbFilter.java index bcaca93132..437d220952 100644 --- a/core/src/main/java/hudson/security/csrf/CrumbFilter.java +++ b/core/src/main/java/hudson/security/csrf/CrumbFilter.java @@ -8,6 +8,9 @@ package hudson.security.csrf; import hudson.util.MultipartFormDataParser; import jenkins.model.Jenkins; import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; +import org.kohsuke.MetaInfServices; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.ForwardToView; import org.kohsuke.stapler.interceptor.RequirePOST; @@ -43,13 +46,16 @@ public class CrumbFilter implements Filter { return h.getCrumbIssuer(); } + @Restricted(NoExternalUse.class) + @MetaInfServices + public static class ErrorCustomizer implements RequirePOST.ErrorCustomizer { + @Override + public ForwardToView getForwardView() { + return new ForwardToView(CrumbFilter.class, "retry"); + } + } + public void init(FilterConfig filterConfig) throws ServletException { - RequirePOST.Processor.registerErrorHandler(new RequirePOST.ErrorHandler() { - @Override - public ForwardToView getForwardView() { - return new ForwardToView(CrumbFilter.this, "retry"); - } - }); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { From 0c59df3d46fa5c4ac018ad6c26bc38cf5a58d98d Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Tue, 12 Dec 2017 04:03:22 -0500 Subject: [PATCH 56/85] [JENKINS-48480] Deleting DeprecatedAgentProtocolMonitor.initializerCheck. (#3190) --- .../DeprecatedAgentProtocolMonitor.java | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/core/src/main/java/jenkins/slaves/DeprecatedAgentProtocolMonitor.java b/core/src/main/java/jenkins/slaves/DeprecatedAgentProtocolMonitor.java index 096468e7b7..f519e4ecb6 100644 --- a/core/src/main/java/jenkins/slaves/DeprecatedAgentProtocolMonitor.java +++ b/core/src/main/java/jenkins/slaves/DeprecatedAgentProtocolMonitor.java @@ -24,14 +24,10 @@ package jenkins.slaves; import hudson.Extension; -import hudson.init.InitMilestone; -import hudson.init.Initializer; import hudson.model.AdministrativeMonitor; import java.util.ArrayList; import java.util.List; import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.annotation.CheckForNull; import jenkins.AgentProtocol; import jenkins.model.Jenkins; @@ -53,8 +49,6 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; @Restricted(NoExternalUse.class) public class DeprecatedAgentProtocolMonitor extends AdministrativeMonitor { - private static final Logger LOGGER = Logger.getLogger(DeprecatedAgentProtocolMonitor.class.getName()); - public DeprecatedAgentProtocolMonitor() { super(); } @@ -97,17 +91,4 @@ public class DeprecatedAgentProtocolMonitor extends AdministrativeMonitor { } return StringUtils.join(deprecatedProtocols, ','); } - - @Initializer(after = InitMilestone.JOB_LOADED) - @Restricted(NoExternalUse.class) - public static void initializerCheck() { - String protocols = getDeprecatedProtocolsString(); - if(protocols != null) { - LOGGER.log(Level.WARNING, "This Jenkins instance uses deprecated Remoting protocols: {0}" - + "It may impact stability of the instance. " - + "If newer protocol versions are supported by all system components " - + "(agents, CLI and other clients), " - + "it is highly recommended to disable the deprecated protocols.", protocols); - } - } } From 6bf700827a12bb1348c95944cfc52d47a74371d2 Mon Sep 17 00:00:00 2001 From: Devin Nusbaum Date: Tue, 12 Dec 2017 10:38:20 -0500 Subject: [PATCH 57/85] Do not use InstallState in PluginManager when loading detached plugins --- core/src/main/java/hudson/PluginManager.java | 5 ++--- core/src/main/java/jenkins/install/InstallUtil.java | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index c816805ba6..7fb2ed8712 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -679,9 +679,8 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas * */ protected void loadDetachedPlugins() { - InstallState installState = Jenkins.getActiveInstance().getInstallState(); - if (InstallState.UPGRADE.equals(installState)) { - VersionNumber lastExecVersion = new VersionNumber(InstallUtil.getLastExecVersion()); + VersionNumber lastExecVersion = new VersionNumber(InstallUtil.getLastExecVersion()); + if (lastExecVersion.isNewerThan(InstallUtil.NEW_INSTALL_VERSION) && lastExecVersion.isOlderThan(Jenkins.getVersion())) { LOGGER.log(INFO, "Upgrading Jenkins. The last running version was {0}. This Jenkins is version {1}.", new Object[] {lastExecVersion, Jenkins.VERSION}); diff --git a/core/src/main/java/jenkins/install/InstallUtil.java b/core/src/main/java/jenkins/install/InstallUtil.java index e4868e95c1..d89cac2df5 100644 --- a/core/src/main/java/jenkins/install/InstallUtil.java +++ b/core/src/main/java/jenkins/install/InstallUtil.java @@ -70,7 +70,7 @@ public class InstallUtil { private static final Logger LOGGER = Logger.getLogger(InstallUtil.class.getName()); // tests need this to be 1.0 - private static final VersionNumber NEW_INSTALL_VERSION = new VersionNumber("1.0"); + public static final VersionNumber NEW_INSTALL_VERSION = new VersionNumber("1.0"); private static final VersionNumber FORCE_NEW_INSTALL_VERSION = new VersionNumber("0.0"); /** From 0518a290596fdb9fca5b63e5782a6d38c6182d5d Mon Sep 17 00:00:00 2001 From: Devin Nusbaum Date: Tue, 12 Dec 2017 10:44:56 -0500 Subject: [PATCH 58/85] Add test for installing detached plugins after upgrading Jenkins --- .../test/java/hudson/PluginManagerUtil.java | 11 +++ .../install/LoadDetachedPluginsTest.java | 86 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java diff --git a/test/src/test/java/hudson/PluginManagerUtil.java b/test/src/test/java/hudson/PluginManagerUtil.java index 2263622b4c..423213b7d6 100644 --- a/test/src/test/java/hudson/PluginManagerUtil.java +++ b/test/src/test/java/hudson/PluginManagerUtil.java @@ -26,6 +26,8 @@ package hudson; import jenkins.RestartRequiredException; import jenkins.model.Jenkins; import org.apache.commons.io.FileUtils; +import org.junit.runner.Description; +import org.jvnet.hudson.test.RestartableJenkinsRule; import org.jvnet.hudson.test.JenkinsRule; import java.io.File; @@ -47,6 +49,15 @@ public class PluginManagerUtil { }; } + public static RestartableJenkinsRule newRestartableJenkinsRule() { + return new RestartableJenkinsRule() { + @Override + public JenkinsRule createJenkinsRule(Description description) { + return newJenkinsRule(); + } + }; + } + public static void dynamicLoad(String plugin, Jenkins jenkins) throws IOException, InterruptedException, RestartRequiredException { dynamicLoad(plugin, jenkins, false); } diff --git a/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java new file mode 100644 index 0000000000..74caf55781 --- /dev/null +++ b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java @@ -0,0 +1,86 @@ +/* + * The MIT License + * + * Copyright 2017 CloudBees, Inc. + * + * 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 jenkins.install; + +import hudson.ClassicPluginStrategy; +import hudson.ClassicPluginStrategy.DetachedPlugin; +import hudson.PluginManager; +import hudson.PluginManagerUtil; +import hudson.PluginWrapper; +import hudson.util.VersionNumber; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +public class LoadDetachedPluginsTest { + + @Rule public RestartableJenkinsRule rr = PluginManagerUtil.newRestartableJenkinsRule(); + + @Issue("JENKINS-48365") + @Test + public void detachedPluginsInstalledAfterUpgrade() throws IOException { + VersionNumber since = new VersionNumber("1.1"); + rr.then(r -> { + List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since); + assertFalse("Many plugins have been detached since the given version", detachedPlugins.isEmpty()); + assertTrue("Detached plugins should not be installed on a new instance", + getInstalledDetachedPlugins(r, detachedPlugins).isEmpty()); + // Trick PluginManager into thinking an upgrade happened when Jenkins restarts. + InstallUtil.saveLastExecVersion(since.toString()); + }); + rr.then(r -> { + List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since); + assertThat("Plugins have been detached since the pre-upgrade version", + detachedPlugins.size(), greaterThan(15)); + assertThat("Plugins detached between the pre-upgrade version and the current version should be installed", + getInstalledDetachedPlugins(r, detachedPlugins).size(), equalTo(detachedPlugins.size())); + }); + } + + private List getInstalledDetachedPlugins(JenkinsRule r, List detachedPlugins) { + PluginManager pluginManager = r.jenkins.getPluginManager(); + List installedPlugins = new ArrayList<>(); + for (DetachedPlugin plugin : detachedPlugins) { + PluginWrapper wrapper = pluginManager.getPlugin(plugin.getShortName()); + if (wrapper != null) { + installedPlugins.add(wrapper); + assertTrue(wrapper.isActive()); + } + } + return installedPlugins; + } + +} From efd8b7153b8b7b29b8af6b73236e61d041b251cc Mon Sep 17 00:00:00 2001 From: Devin Nusbaum Date: Tue, 12 Dec 2017 17:16:30 -0500 Subject: [PATCH 59/85] Use LocalData to test upgrades from before and after 2.0 --- .../install/LoadDetachedPluginsTest.java | 49 ++++++++++++++----- .../upgradeFromJenkins1/config.xml | 34 +++++++++++++ ...enkins.install.InstallUtil.lastExecVersion | 1 + 3 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins1/config.xml create mode 100644 test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2/jenkins.install.InstallUtil.lastExecVersion diff --git a/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java index 74caf55781..f70b4d3a93 100644 --- a/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java +++ b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java @@ -38,10 +38,12 @@ import org.junit.Test; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.RestartableJenkinsRule; +import org.jvnet.hudson.test.recipes.LocalData; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; -import static org.junit.Assert.assertFalse; +import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -51,25 +53,48 @@ public class LoadDetachedPluginsTest { @Issue("JENKINS-48365") @Test - public void detachedPluginsInstalledAfterUpgrade() throws IOException { - VersionNumber since = new VersionNumber("1.1"); - rr.then(r -> { - List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since); - assertFalse("Many plugins have been detached since the given version", detachedPlugins.isEmpty()); - assertTrue("Detached plugins should not be installed on a new instance", - getInstalledDetachedPlugins(r, detachedPlugins).isEmpty()); - // Trick PluginManager into thinking an upgrade happened when Jenkins restarts. - InstallUtil.saveLastExecVersion(since.toString()); - }); + @LocalData + public void upgradeFromJenkins1() throws IOException { + VersionNumber since = new VersionNumber("1.550"); rr.then(r -> { List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since); assertThat("Plugins have been detached since the pre-upgrade version", - detachedPlugins.size(), greaterThan(15)); + detachedPlugins.size(), greaterThan(4)); assertThat("Plugins detached between the pre-upgrade version and the current version should be installed", getInstalledDetachedPlugins(r, detachedPlugins).size(), equalTo(detachedPlugins.size())); }); } + @Issue("JENKINS-48365") + @Test + @LocalData + public void upgradeFromJenkins2() { + VersionNumber since = new VersionNumber("2.0"); + rr.then(r -> { + List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since); + assertThat("Plugins have been detached since the pre-upgrade version", + detachedPlugins.size(), greaterThan(1)); + assertThat("Plugins detached between the pre-upgrade version and the current version should be installed", + getInstalledDetachedPlugins(r, detachedPlugins).size(), equalTo(detachedPlugins.size())); + }); + } + + @Test + public void newInstallation() { + rr.then(r -> { + List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(); + assertThat("Detached plugins should exist", detachedPlugins, not(empty())); + assertThat("Detached plugins should not be installed on a new instance", + getInstalledDetachedPlugins(r, detachedPlugins), empty()); + }); + rr.then(r -> { + List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(); + assertThat("Detached plugins should exist", detachedPlugins, not(empty())); + assertThat("Detached plugins should not be installed after restarting", + getInstalledDetachedPlugins(r, detachedPlugins), empty()); + }); + } + private List getInstalledDetachedPlugins(JenkinsRule r, List detachedPlugins) { PluginManager pluginManager = r.jenkins.getPluginManager(); List installedPlugins = new ArrayList<>(); diff --git a/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins1/config.xml b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins1/config.xml new file mode 100644 index 0000000000..6fcea351c2 --- /dev/null +++ b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins1/config.xml @@ -0,0 +1,34 @@ + + + + 1.550 + 2 + NORMAL + true + + + false + + ${JENKINS_HOME}/workspace/${ITEM_FULLNAME} + ${ITEM_ROOTDIR}/builds + + + + + 5 + 0 + + + + All + false + false + + + + All + 0 + + + + \ No newline at end of file diff --git a/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2/jenkins.install.InstallUtil.lastExecVersion b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2/jenkins.install.InstallUtil.lastExecVersion new file mode 100644 index 0000000000..415b19fc36 --- /dev/null +++ b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2/jenkins.install.InstallUtil.lastExecVersion @@ -0,0 +1 @@ +2.0 \ No newline at end of file From cfbc34ed729e0564f75c1497f49cdffcefaeebe3 Mon Sep 17 00:00:00 2001 From: Devin Nusbaum Date: Tue, 12 Dec 2017 17:37:25 -0500 Subject: [PATCH 60/85] Assert that detached plugins and their dependencies load successfully --- .../jenkins/install/LoadDetachedPluginsTest.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java index f70b4d3a93..eecc1e2b49 100644 --- a/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java +++ b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java @@ -62,6 +62,7 @@ public class LoadDetachedPluginsTest { detachedPlugins.size(), greaterThan(4)); assertThat("Plugins detached between the pre-upgrade version and the current version should be installed", getInstalledDetachedPlugins(r, detachedPlugins).size(), equalTo(detachedPlugins.size())); + assertNoFailedPlugins(r); }); } @@ -76,6 +77,7 @@ public class LoadDetachedPluginsTest { detachedPlugins.size(), greaterThan(1)); assertThat("Plugins detached between the pre-upgrade version and the current version should be installed", getInstalledDetachedPlugins(r, detachedPlugins).size(), equalTo(detachedPlugins.size())); + assertNoFailedPlugins(r); }); } @@ -86,12 +88,14 @@ public class LoadDetachedPluginsTest { assertThat("Detached plugins should exist", detachedPlugins, not(empty())); assertThat("Detached plugins should not be installed on a new instance", getInstalledDetachedPlugins(r, detachedPlugins), empty()); + assertNoFailedPlugins(r); }); rr.then(r -> { List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(); assertThat("Detached plugins should exist", detachedPlugins, not(empty())); assertThat("Detached plugins should not be installed after restarting", getInstalledDetachedPlugins(r, detachedPlugins), empty()); + assertNoFailedPlugins(r); }); } @@ -102,10 +106,16 @@ public class LoadDetachedPluginsTest { PluginWrapper wrapper = pluginManager.getPlugin(plugin.getShortName()); if (wrapper != null) { installedPlugins.add(wrapper); - assertTrue(wrapper.isActive()); + assertTrue("Detached plugins should be active if installed", wrapper.isActive()); + assertThat("Detached plugins should not have dependency errors", wrapper.getDependencyErrors(), empty()); } } return installedPlugins; } + private void assertNoFailedPlugins(JenkinsRule r) { + assertThat("Detached plugins and their dependencies should not fail to install", + r.jenkins.getPluginManager().getFailedPlugins(), empty()); + } + } From bf4c8e6fdfdd7ff0ba514877422f55622f15c15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20F=C3=BCrbacher?= Date: Wed, 13 Dec 2017 15:06:53 +0100 Subject: [PATCH 61/85] Fixed typo in German translation. --- core/src/main/resources/lib/hudson/scriptConsole_de.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/lib/hudson/scriptConsole_de.properties b/core/src/main/resources/lib/hudson/scriptConsole_de.properties index 4db58a7382..6505dca93e 100644 --- a/core/src/main/resources/lib/hudson/scriptConsole_de.properties +++ b/core/src/main/resources/lib/hudson/scriptConsole_de.properties @@ -26,7 +26,7 @@ Run=Ausf\u00FChren description=Geben Sie ein beliebiges Groovy-Skript \ ein und f\u00FChren Sie dieses auf dem Server aus. Dies ist n\u00FCtzlich bei der Fehlersuche und zur Diagnostik. \ Verwenden Sie den println-Befehl, um Ausgaben sichtbar zu machen (wenn Sie System.out \ - verwenden, gehen die Ausgaben auf die Standardausgabe (STDOUT) des Servers, die schwieriger + verwenden, gehen die Ausgaben auf die Standardausgabe (STDOUT) des Servers, die schwieriger \ einzusehen ist). Beispiel: description2=Alle Klassen aller Plugins sind verf\u00FCgbar. jenkins.*, jenkins.model.*, hudson.* sowie hudson.model.* werden standardm\u00E4\u00DFig importiert. It\ is\ not\ possible\ to\ run\ scripts\ when\ agent\ is\ offline.=Es ist nicht m\u00F6glich, Skripte auf einem Agenten, der offline ist, auszuf\u00FChren From ccc374a7176d7704941fb494589790b7673efe2e Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Wed, 13 Dec 2017 12:59:04 -0500 Subject: [PATCH 62/85] [SECURITY-667] Ensure all tasks have completed before we attain COMPLETED. --- core/src/main/java/jenkins/model/Jenkins.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 8d16eaed99..b1f64dd9c5 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -3097,7 +3097,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve }); } - g.requires(JOB_LOADED).add("Cleaning up obsolete items deleted from the disk", new Executable() { + g.requires(JOB_LOADED).attains(COMPLETED).add("Cleaning up obsolete items deleted from the disk", new Executable() { public void run(Reactor reactor) throws Exception { // anything we didn't load from disk, throw them away. // doing this after loading from disk allows newly loaded items @@ -3112,7 +3112,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } }); - g.requires(JOB_LOADED).add("Finalizing set up",new Executable() { + g.requires(JOB_LOADED).attains(COMPLETED).add("Finalizing set up",new Executable() { public void run(Reactor session) throws Exception { rebuildDependencyGraph(); From 0f029f39e6edaa18a26f9244c1db42e801e90c92 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Wed, 13 Dec 2017 17:58:00 -0800 Subject: [PATCH 63/85] [maven-release-plugin] prepare release jenkins-2.95 --- cli/pom.xml | 2 +- core/pom.xml | 2 +- pom.xml | 4 ++-- test/pom.xml | 2 +- war/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 0fca80941a..fda944b19a 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 2.95-SNAPSHOT + 2.95 cli diff --git a/core/pom.xml b/core/pom.xml index a3a7bc6bff..731f6265b8 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.95-SNAPSHOT + 2.95 jenkins-core diff --git a/pom.xml b/pom.xml index 498b67a88e..f8505941fb 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.95-SNAPSHOT + 2.95 pom Jenkins main module @@ -58,7 +58,7 @@ THE SOFTWARE. scm:git:git://github.com/jenkinsci/jenkins.git scm:git:ssh://git@github.com/jenkinsci/jenkins.git https://github.com/jenkinsci/jenkins - HEAD + jenkins-2.95 diff --git a/test/pom.xml b/test/pom.xml index f38b1d2a09..7d714bd526 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.95-SNAPSHOT + 2.95 test diff --git a/war/pom.xml b/war/pom.xml index f9bb67064b..277328007a 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.95-SNAPSHOT + 2.95 jenkins-war From 4c75250ad4b0d99496950ae83f4ba6aa2255c1c6 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Wed, 13 Dec 2017 17:58:00 -0800 Subject: [PATCH 64/85] [maven-release-plugin] prepare for next development iteration --- cli/pom.xml | 2 +- core/pom.xml | 2 +- pom.xml | 4 ++-- test/pom.xml | 2 +- war/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index fda944b19a..1cb344f3bf 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 2.95 + 2.96-SNAPSHOT cli diff --git a/core/pom.xml b/core/pom.xml index 731f6265b8..0db9130a23 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.95 + 2.96-SNAPSHOT jenkins-core diff --git a/pom.xml b/pom.xml index f8505941fb..9bde65a91d 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.95 + 2.96-SNAPSHOT pom Jenkins main module @@ -58,7 +58,7 @@ THE SOFTWARE. scm:git:git://github.com/jenkinsci/jenkins.git scm:git:ssh://git@github.com/jenkinsci/jenkins.git https://github.com/jenkinsci/jenkins - jenkins-2.95 + HEAD diff --git a/test/pom.xml b/test/pom.xml index 7d714bd526..578d451007 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.95 + 2.96-SNAPSHOT test diff --git a/war/pom.xml b/war/pom.xml index 277328007a..13086ab727 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.95 + 2.96-SNAPSHOT jenkins-war From 67476c7920c9c004bd97b9d12beae8c4f9f406cd Mon Sep 17 00:00:00 2001 From: Nicolas De loof Date: Mon, 11 Dec 2017 14:34:42 +0100 Subject: [PATCH 65/85] upgrade access-modifier to 1.12 with 1.12 @Restricted annotation are kept for runtime, which is a requirement for [configuration-as-code](https://github.com/jenkinsci/configuration-as-code-plugin) to detect technical attribute which should not be exposed (typically : `Jenkins#setInstallState()`) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9bde65a91d..61644dbd00 100644 --- a/pom.xml +++ b/pom.xml @@ -94,7 +94,7 @@ THE SOFTWARE. ${skipTests} 3.0.4 true - 1.11 + 1.12 ${access-modifier.version} ${access-modifier.version} From b7f42b2e59b2081782d6e51da18b0c93808d98da Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Thu, 14 Dec 2017 20:44:54 +0100 Subject: [PATCH 66/85] [JENKINS-27027] Notify the SecurityListener on authentication (#3074) * [JENKINS-27026] Notify the SecurityListener in case of Token based authentication success - due the current version of the method, the UserDetails required for the event was not accessible. In order to stay with the same API in SecurityListener, two "protected" methods were created to split the job and let the UserDetails accessible * - add test to ensure the SecurityListener is called for REST Token but also for regular basic auth * - remove the comment about the split, will be put in GitHub comment instead * - add check for anonymous call instead of just putting a comment - remove the constructor in the dummy - add link to PR from Daniel to simplify a call * - separate the before/after to save one clear and be more explicit - put more meaning in the assertLastEventIs method by explicitly say we will remove the last event * - add comment about why we do not fire the "failedToAuthenticated" in the case of an invalid token (tips: it's because it could be a valid password) * - also add the authenticated trigger on legacy filter as pointed by Ivan * - add support of event on CLI remoting authentication - adjust tests by moving the helper class used to spy on events * - as mentioned Yvan, the code had some problems with null checking, so the approach is changed in order to encapsulate all that internal mechanism * - add javadoc - open the getUserDetailsForImpersonation from the User (will let the SSHD module to retrieve UserDetails from that) * - remove single quote in log messages * - basic corrections requested by Jesse * - just another typo * - adjust the javadoc for SecurityListener events * - add the link to Jenkins#Anonymous * - add link (not using see) * - update comment on the isAnonymous as we (me + Oleg) do not find a best place at the moment * - put the new method isAnonymous in ACL instead of Functions * - little typo - add requirement about the SecurityContext authentication --- core/src/main/java/hudson/Functions.java | 9 +- core/src/main/java/hudson/cli/CLICommand.java | 28 ++++- .../hudson/cli/ClientAuthenticationCache.java | 5 +- .../main/java/hudson/cli/LoginCommand.java | 3 + .../main/java/hudson/cli/LogoutCommand.java | 9 ++ core/src/main/java/hudson/model/User.java | 61 ++++++++-- core/src/main/java/hudson/security/ACL.java | 10 ++ .../security/BasicAuthenticationFilter.java | 10 +- .../BasicHeaderApiTokenAuthenticator.java | 12 +- .../jenkins/security/SecurityListener.java | 17 +-- .../security/CliAuthenticationTest.java | 112 ++++++++++++++++- .../security/BasicHeaderProcessorTest.java | 52 ++++++-- .../jenkins/security/SpySecurityListener.java | 115 ++++++++++++++++++ 13 files changed, 403 insertions(+), 40 deletions(-) create mode 100644 test/src/test/java/jenkins/security/SpySecurityListener.java diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index c9c5e8545b..bf8aca4e47 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -26,6 +26,7 @@ package hudson; import hudson.model.Slave; +import hudson.security.*; import jenkins.util.SystemProperties; import hudson.cli.CLICommand; import hudson.console.ConsoleAnnotationDescriptor; @@ -56,11 +57,6 @@ import hudson.model.View; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; import hudson.search.SearchableModelObject; -import hudson.security.AccessControlled; -import hudson.security.AuthorizationStrategy; -import hudson.security.GlobalSecurityConfiguration; -import hudson.security.Permission; -import hudson.security.SecurityRealm; import hudson.security.captcha.CaptchaSupport; import hudson.security.csrf.CrumbIssuer; import hudson.slaves.Cloud; @@ -137,6 +133,7 @@ import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithChildren; import jenkins.model.ModelObjectWithContextMenu; +import org.acegisecurity.Authentication; import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; import org.apache.commons.jelly.JellyContext; import org.apache.commons.jelly.JellyTagException; @@ -1582,7 +1579,7 @@ public class Functions { * Checks if the current user is anonymous. */ public static boolean isAnonymous() { - return Jenkins.getAuthentication() instanceof AnonymousAuthenticationToken; + return ACL.isAnonymous(Jenkins.getAuthentication()); } /** diff --git a/core/src/main/java/hudson/cli/CLICommand.java b/core/src/main/java/hudson/cli/CLICommand.java index d507662037..5efbaf3a33 100644 --- a/core/src/main/java/hudson/cli/CLICommand.java +++ b/core/src/main/java/hudson/cli/CLICommand.java @@ -30,6 +30,8 @@ import hudson.ExtensionPoint; import hudson.cli.declarative.CLIMethod; import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson; import hudson.Functions; +import hudson.security.ACL; +import jenkins.security.SecurityListener; import jenkins.util.SystemProperties; import hudson.cli.declarative.OptionHandlerExtension; import jenkins.model.Jenkins; @@ -44,6 +46,8 @@ import org.acegisecurity.Authentication; import org.acegisecurity.BadCredentialsException; import org.acegisecurity.context.SecurityContext; import org.acegisecurity.context.SecurityContextHolder; +import org.acegisecurity.userdetails.User; +import org.acegisecurity.userdetails.UserDetails; import org.apache.commons.discovery.ResourceClassIterator; import org.apache.commons.discovery.ResourceNameIterator; import org.apache.commons.discovery.resource.ClassLoaders; @@ -344,8 +348,16 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { @Deprecated protected Authentication loadStoredAuthentication() throws InterruptedException { try { - if (channel!=null) - return new ClientAuthenticationCache(channel).get(); + if (channel!=null){ + Authentication authLoadedFromCache = new ClientAuthenticationCache(channel).get(); + + if(!ACL.isAnonymous(authLoadedFromCache)){ + UserDetails userDetails = new CLIUserDetails(authLoadedFromCache); + SecurityListener.fireAuthenticated(userDetails); + } + + return authLoadedFromCache; + } } catch (IOException e) { stderr.println("Failed to access the stored credential"); Functions.printStackTrace(e, stderr); // recover @@ -642,4 +654,16 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { } } } + + /** + * User details loaded from the CLI {@link ClientAuthenticationCache} + * The user is never anonymous since it must be authenticated to be stored in the cache + */ + @Deprecated + @Restricted(NoExternalUse.class) + private static class CLIUserDetails extends User { + private CLIUserDetails(Authentication auth) { + super(auth.getName(), "", true, true, true, true, auth.getAuthorities()); + } + } } diff --git a/core/src/main/java/hudson/cli/ClientAuthenticationCache.java b/core/src/main/java/hudson/cli/ClientAuthenticationCache.java index cf5b5c113c..48b96f4686 100644 --- a/core/src/main/java/hudson/cli/ClientAuthenticationCache.java +++ b/core/src/main/java/hudson/cli/ClientAuthenticationCache.java @@ -22,6 +22,8 @@ import java.util.logging.Level; import java.util.logging.Logger; import jenkins.security.HMACConfidentialKey; +import javax.annotation.Nonnull; + /** * Represents the authentication credential store of the CLI client. * @@ -73,7 +75,7 @@ public class ClientAuthenticationCache implements Serializable { * * @return {@link jenkins.model.Jenkins#ANONYMOUS} if no such credential is found, or if the stored credential is invalid. */ - public Authentication get() { + public @Nonnull Authentication get() { Jenkins h = Jenkins.getActiveInstance(); String val = props.getProperty(getPropertyKey()); if (val == null) { @@ -100,6 +102,7 @@ public class ClientAuthenticationCache implements Serializable { LOGGER.log(Level.FINER, "Loaded stored CLI authentication for {0}", username); return new UsernamePasswordAuthenticationToken(u.getUsername(), "", u.getAuthorities()); } catch (AuthenticationException | DataAccessException e) { + //TODO there is no check to be consistent with User.ALLOW_NON_EXISTENT_USER_TO_LOGIN LOGGER.log(Level.FINE, "Stored CLI authentication did not correspond to a valid user: " + username, e); return Jenkins.ANONYMOUS; } diff --git a/core/src/main/java/hudson/cli/LoginCommand.java b/core/src/main/java/hudson/cli/LoginCommand.java index 8920ad4111..55efd9dbcb 100644 --- a/core/src/main/java/hudson/cli/LoginCommand.java +++ b/core/src/main/java/hudson/cli/LoginCommand.java @@ -3,6 +3,7 @@ package hudson.cli; import hudson.Extension; import java.io.PrintStream; import jenkins.model.Jenkins; +import jenkins.security.SecurityListener; import org.acegisecurity.Authentication; import org.kohsuke.args4j.CmdLineException; @@ -45,6 +46,8 @@ public class LoginCommand extends CLICommand { ClientAuthenticationCache store = new ClientAuthenticationCache(checkChannel()); store.set(a); + SecurityListener.fireLoggedIn(a.getName()); + return 0; } diff --git a/core/src/main/java/hudson/cli/LogoutCommand.java b/core/src/main/java/hudson/cli/LogoutCommand.java index 822c99d84e..8578a6eb04 100644 --- a/core/src/main/java/hudson/cli/LogoutCommand.java +++ b/core/src/main/java/hudson/cli/LogoutCommand.java @@ -1,6 +1,9 @@ package hudson.cli; import hudson.Extension; +import jenkins.security.SecurityListener; +import org.acegisecurity.Authentication; + import java.io.PrintStream; /** @@ -27,7 +30,13 @@ public class LogoutCommand extends CLICommand { @Override protected int run() throws Exception { ClientAuthenticationCache store = new ClientAuthenticationCache(checkChannel()); + + Authentication auth = store.get(); + store.remove(); + + SecurityListener.fireLoggedOut(auth.getName()); + return 0; } } diff --git a/core/src/main/java/hudson/model/User.java b/core/src/main/java/hudson/model/User.java index d75ec1b227..418e49aa0d 100644 --- a/core/src/main/java/hudson/model/User.java +++ b/core/src/main/java/hudson/model/User.java @@ -326,23 +326,70 @@ public class User extends AbstractModelObject implements AccessControlled, Descr * @since 1.419 */ public @Nonnull Authentication impersonate() throws UsernameNotFoundException { + return this.impersonate(this.getUserDetailsForImpersonation()); + } + + /** + * This method checks with {@link SecurityRealm} if the user is a valid user that can login to the security realm. + * If {@link SecurityRealm} is a kind that does not support querying information about other users, this will + * use {@link LastGrantedAuthoritiesProperty} to pick up the granted authorities as of the last time the user has + * logged in. + * + * @return userDetails for the user, in case he's not found but seems legitimate, we provide a userDetails with minimum access + * + * @throws UsernameNotFoundException + * If this user is not a valid user in the backend {@link SecurityRealm}. + */ + public @Nonnull UserDetails getUserDetailsForImpersonation() throws UsernameNotFoundException { + ImpersonatingUserDetailsService userDetailsService = new ImpersonatingUserDetailsService( + Jenkins.getInstance().getSecurityRealm().getSecurityComponents().userDetails + ); + try { - UserDetails u = new ImpersonatingUserDetailsService( - Jenkins.getInstance().getSecurityRealm().getSecurityComponents().userDetails).loadUserByUsername(id); - return new UsernamePasswordAuthenticationToken(u.getUsername(), "", u.getAuthorities()); + UserDetails userDetails = userDetailsService.loadUserByUsername(id); + LOGGER.log(Level.FINE, "Impersonation of the user {0} was a success", new Object[]{ id }); + return userDetails; } catch (UserMayOrMayNotExistException e) { + LOGGER.log(Level.FINE, "The user {0} may or may not exist in the SecurityRealm, so we provide minimum access", new Object[]{ id }); // backend can't load information about other users. so use the stored information if available } catch (UsernameNotFoundException e) { // if the user no longer exists in the backend, we need to refuse impersonating this user - if (!ALLOW_NON_EXISTENT_USER_TO_LOGIN) + if(ALLOW_NON_EXISTENT_USER_TO_LOGIN){ + LOGGER.log(Level.FINE, "The user {0} was not found in the SecurityRealm but we are required to let it pass, due to ALLOW_NON_EXISTENT_USER_TO_LOGIN", new Object[]{ id }); + }else{ + LOGGER.log(Level.FINE, "The user {0} was not found in the SecurityRealm", new Object[]{ id }); throw e; + } } catch (DataAccessException e) { // seems like it's in the same boat as UserMayOrMayNotExistException + LOGGER.log(Level.FINE, "The user {0} retrieval just threw a DataAccess exception with msg = {1}, so we provide minimum access", new Object[]{ id, e.getMessage() }); } + + return new LegitimateButUnknownUserDetails(id); + } - // seems like a legitimate user we have no idea about. proceed with minimum access - return new UsernamePasswordAuthenticationToken(id, "", - new GrantedAuthority[]{SecurityRealm.AUTHENTICATED_AUTHORITY}); + /** + * Only used for a legitimate user we have no idea about. We give it only minimum access + */ + private static class LegitimateButUnknownUserDetails extends org.acegisecurity.userdetails.User{ + private LegitimateButUnknownUserDetails(String username) throws IllegalArgumentException { + super( + username, "", + true, true, true, true, + new GrantedAuthority[]{SecurityRealm.AUTHENTICATED_AUTHORITY} + ); + } + } + + /** + * Creates an {@link Authentication} object that represents this user using the given userDetails + * + * @param userDetails Provided by {@link #getUserDetailsForImpersonation()}. + * @see #getUserDetailsForImpersonation() + */ + @Restricted(NoExternalUse.class) + public @Nonnull Authentication impersonate(@Nonnull UserDetails userDetails) { + return new UsernamePasswordAuthenticationToken(userDetails.getUsername(), "", userDetails.getAuthorities()); } /** diff --git a/core/src/main/java/hudson/security/ACL.java b/core/src/main/java/hudson/security/ACL.java index 0efe770050..41f8b5a9bc 100644 --- a/core/src/main/java/hudson/security/ACL.java +++ b/core/src/main/java/hudson/security/ACL.java @@ -44,6 +44,7 @@ import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; import org.acegisecurity.acls.sid.PrincipalSid; import org.acegisecurity.acls.sid.Sid; +import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -319,4 +320,13 @@ public abstract class ACL { return as(user == null ? Jenkins.ANONYMOUS : user.impersonate()); } + /** + * Checks if the given authentication is anonymous by checking its class. + * @see Jenkins#ANONYMOUS + * @see AnonymousAuthenticationToken + */ + public static boolean isAnonymous(@Nonnull Authentication authentication) { + //TODO use AuthenticationTrustResolver instead to be consistent through the application + return authentication instanceof AnonymousAuthenticationToken; + } } diff --git a/core/src/main/java/hudson/security/BasicAuthenticationFilter.java b/core/src/main/java/hudson/security/BasicAuthenticationFilter.java index 11709bcf58..1ec0be214f 100644 --- a/core/src/main/java/hudson/security/BasicAuthenticationFilter.java +++ b/core/src/main/java/hudson/security/BasicAuthenticationFilter.java @@ -27,7 +27,10 @@ import hudson.model.User; import jenkins.model.Jenkins; import hudson.util.Scrambler; import jenkins.security.ApiTokenProperty; +import jenkins.security.SecurityListener; +import org.acegisecurity.Authentication; import org.acegisecurity.context.SecurityContextHolder; +import org.acegisecurity.userdetails.UserDetails; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -138,7 +141,12 @@ public class BasicAuthenticationFilter implements Filter { User u = User.getById(username, true); ApiTokenProperty t = u.getProperty(ApiTokenProperty.class); if (t!=null && t.matchesPassword(password)) { - SecurityContextHolder.getContext().setAuthentication(u.impersonate()); + UserDetails userDetails = u.getUserDetailsForImpersonation(); + Authentication auth = u.impersonate(userDetails); + + SecurityListener.fireAuthenticated(userDetails); + + SecurityContextHolder.getContext().setAuthentication(auth); try { chain.doFilter(request,response); } finally { diff --git a/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java b/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java index a192dddc5f..f0f6491e22 100644 --- a/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java +++ b/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java @@ -3,6 +3,7 @@ package jenkins.security; import hudson.Extension; import hudson.model.User; import org.acegisecurity.Authentication; +import org.acegisecurity.userdetails.UserDetails; import org.acegisecurity.userdetails.UsernameNotFoundException; import org.springframework.dao.DataAccessException; @@ -21,6 +22,10 @@ import static java.util.logging.Level.*; */ @Extension public class BasicHeaderApiTokenAuthenticator extends BasicHeaderAuthenticator { + /** + * Note: if the token does not exist or does not match, we do not use {@link SecurityListener#fireFailedToAuthenticate(String)} + * because it will be done in the {@link BasicHeaderRealPasswordAuthenticator} in the case the password is not valid either + */ @Override public Authentication authenticate(HttpServletRequest req, HttpServletResponse rsp, String username, String password) throws ServletException { // attempt to authenticate as API token @@ -28,7 +33,12 @@ public class BasicHeaderApiTokenAuthenticator extends BasicHeaderAuthenticator { ApiTokenProperty t = u.getProperty(ApiTokenProperty.class); if (t!=null && t.matchesPassword(password)) { try { - return u.impersonate(); + UserDetails userDetails = u.getUserDetailsForImpersonation(); + Authentication auth = u.impersonate(userDetails); + + SecurityListener.fireAuthenticated(userDetails); + + return auth; } catch (UsernameNotFoundException x) { // The token was valid, but the impersonation failed. This token is clearly not his real password, // so there's no point in continuing the request processing. Report this error and abort. diff --git a/core/src/main/java/jenkins/security/SecurityListener.java b/core/src/main/java/jenkins/security/SecurityListener.java index f8c8ca6604..134e992e7f 100644 --- a/core/src/main/java/jenkins/security/SecurityListener.java +++ b/core/src/main/java/jenkins/security/SecurityListener.java @@ -45,29 +45,32 @@ public abstract class SecurityListener implements ExtensionPoint { private static final Logger LOGGER = Logger.getLogger(SecurityListener.class.getName()); /** - * Fired when a user was successfully authenticated by password. - * This might be via the web UI, or via REST (not with an API token) or CLI (not with an SSH key). - * Only {@link AbstractPasswordBasedSecurityRealm}s are considered. - * @param details details of the newly authenticated user, such as name and groups + * Fired when a user was successfully authenticated using credentials. It could be password or any other credentials. + * This might be via the web UI, or via REST (using API token or Basic), or CLI (remoting, auth, ssh) + * or any other way plugins can propose. + * @param details details of the newly authenticated user, such as name and groups. */ protected void authenticated(@Nonnull UserDetails details){} /** - * Fired when a user tried to authenticate by password but failed. + * Fired when a user tried to authenticate but failed. + * In case the authentication method uses multiple layers to validate the credentials, + * we do fire this event only when even the last layer failed to authenticate. * @param username the user * @see #authenticated */ protected void failedToAuthenticate(@Nonnull String username){} /** - * Fired when a user has logged in via the web UI. + * Fired when a user has logged in. Compared to authenticated, there is a notion of storage / cache. * Would be called after {@link #authenticated}. + * It should be called after the {@link org.acegisecurity.context.SecurityContextHolder#getContext()}'s authentication is set. * @param username the user */ protected void loggedIn(@Nonnull String username){} /** - * Fired when a user has failed to log in via the web UI. + * Fired when a user has failed to log in. * Would be called after {@link #failedToAuthenticate}. * @param username the user */ diff --git a/test/src/test/java/hudson/security/CliAuthenticationTest.java b/test/src/test/java/hudson/security/CliAuthenticationTest.java index bdddf14407..c02d085586 100644 --- a/test/src/test/java/hudson/security/CliAuthenticationTest.java +++ b/test/src/test/java/hudson/security/CliAuthenticationTest.java @@ -1,24 +1,34 @@ package hudson.security; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - +import hudson.ExtensionList; import hudson.cli.CLI; import hudson.cli.CLICommand; import jenkins.model.Jenkins; +import jenkins.security.SecurityListener; +import jenkins.security.SpySecurityListener; import org.acegisecurity.Authentication; +import org.acegisecurity.AuthenticationException; +import org.acegisecurity.BadCredentialsException; +import org.acegisecurity.GrantedAuthority; +import org.acegisecurity.userdetails.User; +import org.acegisecurity.userdetails.UserDetails; +import org.acegisecurity.userdetails.UsernameNotFoundException; import org.apache.commons.io.input.NullInputStream; +import org.junit.After; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.For; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TestExtension; +import org.springframework.dao.DataAccessException; import java.io.ByteArrayOutputStream; import java.util.Arrays; +import static org.junit.Assert.*; + /** * @author Kohsuke Kawaguchi */ @@ -27,6 +37,14 @@ public class CliAuthenticationTest { @Rule public JenkinsRule j = new JenkinsRule(); + + private SpySecurityListener spySecurityListener; + + @Before + public void prepareListeners(){ + //TODO simplify using #3021 into ExtensionList.lookupSingleton(SpySecurityListener.class) + this.spySecurityListener = ExtensionList.lookup(SecurityListener.class).get(SpySecurityListenerImpl.class); + } @Test public void test() throws Exception { @@ -40,6 +58,10 @@ public class CliAuthenticationTest { assertEquals(0, command(args)); } + private void unsuccessfulCommand(String... args) throws Exception { + assertNotEquals(0, command(args)); + } + private int command(String... args) throws Exception { try (CLI cli = new CLI(j.getURL())) { return cli.execute(args); @@ -91,9 +113,86 @@ public class CliAuthenticationTest { j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); successfulCommand("login","--username","abc","--password","abc"); + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(userDetails -> userDetails.getUsername().equals("abc")); + spySecurityListener.loggedInCalls.assertLastEventIsAndThenRemoveIt("abc"); + successfulCommand("test"); // now we can run without an explicit credential + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(userDetails -> userDetails.getUsername().equals("abc")); + spySecurityListener.loggedInCalls.assertNoNewEvents(); + successfulCommand("logout"); + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(userDetails -> userDetails.getUsername().equals("abc")); + spySecurityListener.loggedOutCalls.assertLastEventIsAndThenRemoveIt("abc"); + successfulCommand("anonymous"); // now we should run as anonymous + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.failedToAuthenticateCalls.assertNoNewEvents(); + spySecurityListener.loggedInCalls.assertNoNewEvents(); + spySecurityListener.loggedOutCalls.assertNoNewEvents(); + } + + @Test + @Issue("JENKINS-27026") + public void loginAsALegitimateUserButUnknown() throws Exception { + j.jenkins.setSecurityRealm(new MockSecurityRealm()); + + String username = "alice"; + + unsuccessfulCommand("login","--username",username,"--password","badCredentials"); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt(username); + spySecurityListener.failedToLogInCalls.assertNoNewEvents(); + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.loggedInCalls.assertNoNewEvents(); +// spySecurityListener.failedToLogInCalls.assertLastEventIsAndThenRemoveIt(username); + + unsuccessfulCommand("login","--username",username,"--password","usernameNotFound"); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt(username); + spySecurityListener.failedToLogInCalls.assertNoNewEvents(); + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.loggedInCalls.assertNoNewEvents(); +// spySecurityListener.failedToLogInCalls.assertLastEventIsAndThenRemoveIt(username); + + unsuccessfulCommand("login","--username",username,"--password","mayOrMayNotExist"); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt(username); + spySecurityListener.failedToLogInCalls.assertNoNewEvents(); + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.loggedInCalls.assertNoNewEvents(); +// spySecurityListener.failedToLogInCalls.assertLastEventIsAndThenRemoveIt(username); + + // in case of authentication that throw a DataAccessException (see impersonatingUserDetailsService) + // the CLI login command does not work as expected + unsuccessfulCommand("login","--username",username,"--password","dataAccess"); + spySecurityListener.failedToAuthenticateCalls.assertNoNewEvents(); + spySecurityListener.failedToLogInCalls.assertNoNewEvents(); + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.loggedInCalls.assertNoNewEvents(); +// spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt(username); +// spySecurityListener.failedToLogInCalls.assertLastEventIsAndThenRemoveIt(username); + } + + private static class MockSecurityRealm extends AbstractPasswordBasedSecurityRealm { + @Override protected UserDetails authenticate(String username, String password) throws AuthenticationException { + switch(password){ + case "badCredentials": throw new BadCredentialsException("BadCredentials requested"); + case "usernameNotFound": throw new UsernameNotFoundException("UsernameNotFound requested"); + case "mayOrMayNotExist": throw new UserMayOrMayNotExistException("MayOrMayNotExist requested"); + case "dataAccess": throw new DataAccessException("DataAccess requested"){}; + default: + return new User( + username, password, + true, true, true, + new GrantedAuthority[]{SecurityRealm.AUTHENTICATED_AUTHORITY} + ); + } + } + + @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return null; + } + + @Override public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException { + return null; + } } /** @@ -112,4 +211,7 @@ public class CliAuthenticationTest { assertTrue(out1.contains("Bad Credentials. Search the server log for")); assertTrue(out2.contains("Bad Credentials. Search the server log for")); } + + @TestExtension + public static class SpySecurityListenerImpl extends SpySecurityListener {} } diff --git a/test/src/test/java/jenkins/security/BasicHeaderProcessorTest.java b/test/src/test/java/jenkins/security/BasicHeaderProcessorTest.java index 449293ae10..1c7c32d43f 100644 --- a/test/src/test/java/jenkins/security/BasicHeaderProcessorTest.java +++ b/test/src/test/java/jenkins/security/BasicHeaderProcessorTest.java @@ -1,15 +1,15 @@ package jenkins.security; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.WebRequest; +import hudson.ExtensionList; import hudson.model.UnprotectedRootAction; import hudson.model.User; import hudson.util.HttpResponses; import hudson.util.Scrambler; +import org.junit.After; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -21,6 +21,8 @@ import org.xml.sax.SAXException; import java.io.IOException; import java.net.URL; +import static org.junit.Assert.*; + /** * @author Kohsuke Kawaguchi */ @@ -30,6 +32,14 @@ public class BasicHeaderProcessorTest { private WebClient wc; + private SpySecurityListener spySecurityListener; + + @Before + public void prepareListeners(){ + //TODO simplify using #3021 into ExtensionList.lookupSingleton(SpySecurityListener.class) + this.spySecurityListener = ExtensionList.lookup(SecurityListener.class).get(SpySecurityListenerImpl.class); + } + /** * Tests various ways to send the Basic auth. */ @@ -43,39 +53,54 @@ public class BasicHeaderProcessorTest { // call without authentication makeRequestWithAuthAndVerify(null, "anonymous"); + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.failedToAuthenticateCalls.assertNoNewEvents(); // call with API token ApiTokenProperty t = foo.getProperty(ApiTokenProperty.class); final String token = t.getApiToken(); makeRequestWithAuthAndVerify("foo:"+token, "foo"); + //TODO verify why there are two events "authenticated" that are triggered + // the whole authentication process seems to be done twice + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(u -> u.getUsername().equals("foo")); // call with invalid API token makeRequestAndFail("foo:abcd"+token); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt("foo"); // call with password makeRequestWithAuthAndVerify("foo:foo", "foo"); + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(u -> u.getUsername().equals("foo")); // call with incorrect password makeRequestAndFail("foo:bar"); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt("foo"); wc.login("bar"); + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(u -> u.getUsername().equals("bar")); + spySecurityListener.loggedInCalls.assertLastEventIsAndThenRemoveIt("bar"); // if the session cookie is valid, then basic header won't be needed makeRequestWithAuthAndVerify(null, "bar"); + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.failedToAuthenticateCalls.assertNoNewEvents(); // if the session cookie is valid, and basic header is set anyway login should not fail either makeRequestWithAuthAndVerify("bar:bar", "bar"); + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.failedToAuthenticateCalls.assertNoNewEvents(); // but if the password is incorrect, it should fail, instead of silently logging in as the user indicated by session makeRequestAndFail("foo:bar"); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt("foo"); } private void makeRequestAndFail(String userAndPass) throws IOException, SAXException { - makeRequestWithAuthCodeAndFail(encrypt("Basic", userAndPass)); + makeRequestWithAuthCodeAndFail(encode("Basic", userAndPass)); } - private String encrypt(String prefix, String userAndPass) { + private String encode(String prefix, String userAndPass) { if (userAndPass==null) { return null; } @@ -92,7 +117,7 @@ public class BasicHeaderProcessorTest { } private void makeRequestWithAuthAndVerify(String userAndPass, String username) throws IOException, SAXException { - makeRequestWithAuthCodeAndVerify(encrypt("Basic", userAndPass), username); + makeRequestWithAuthCodeAndVerify(encode("Basic", userAndPass), username); } private void makeRequestWithAuthCodeAndVerify(String authCode, String expected) throws IOException, SAXException { @@ -116,20 +141,24 @@ public class BasicHeaderProcessorTest { // call with API token ApiTokenProperty t = foo.getProperty(ApiTokenProperty.class); final String token = t.getApiToken(); - String authCode1 = encrypt(prefix,"foo:"+token); + String authCode1 = encode(prefix,"foo:"+token); makeRequestWithAuthCodeAndVerify(authCode1, "foo"); + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(u -> u.getUsername().equals("foo")); // call with invalid API token - String authCode2 = encrypt(prefix,"foo:abcd"+token); + String authCode2 = encode(prefix,"foo:abcd"+token); makeRequestWithAuthCodeAndFail(authCode2); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt("foo"); // call with password - String authCode3 = encrypt(prefix,"foo:foo"); + String authCode3 = encode(prefix,"foo:foo"); makeRequestWithAuthCodeAndVerify(authCode3, "foo"); + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(u -> u.getUsername().equals("foo")); // call with incorrect password - String authCode4 = encrypt(prefix,"foo:bar"); + String authCode4 = encode(prefix,"foo:bar"); makeRequestWithAuthCodeAndFail(authCode4); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt("foo"); } } @@ -155,4 +184,7 @@ public class BasicHeaderProcessorTest { return HttpResponses.plainText(u!=null ? u.getId() : "anonymous"); } } + + @TestExtension + public static class SpySecurityListenerImpl extends SpySecurityListener {} } diff --git a/test/src/test/java/jenkins/security/SpySecurityListener.java b/test/src/test/java/jenkins/security/SpySecurityListener.java new file mode 100644 index 0000000000..0d9731cc90 --- /dev/null +++ b/test/src/test/java/jenkins/security/SpySecurityListener.java @@ -0,0 +1,115 @@ +/* + * The MIT License + * + * Copyright (c) 2017, CloudBees, Inc. + * + * 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 jenkins.security; + +import org.acegisecurity.userdetails.UserDetails; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +//TODO temporary solution, should be moved to Jenkins Test Harness project +/** + * Mean to be included in test classes to provide a way to spy on the SecurityListener events + * @see jenkins.security.BasicHeaderProcessorTest.SpySecurityListenerImpl + * @see hudson.security.CliAuthenticationTest.SpySecurityListenerImpl + */ +public abstract class SpySecurityListener extends SecurityListener { + public final EventQueue authenticatedCalls = new EventQueue<>(); + public final EventQueue failedToAuthenticateCalls = new EventQueue<>(); + public final EventQueue loggedInCalls = new EventQueue<>(); + public final EventQueue failedToLogInCalls = new EventQueue<>(); + public final EventQueue loggedOutCalls = new EventQueue<>(); + + public void clearPreviousCalls(){ + this.authenticatedCalls.clear(); + this.failedToAuthenticateCalls.clear(); + this.loggedInCalls.clear(); + this.failedToLogInCalls.clear(); + this.loggedOutCalls.clear(); + } + + @Override + protected void authenticated(@Nonnull UserDetails details) { + this.authenticatedCalls.add(details); + } + + @Override + protected void failedToAuthenticate(@Nonnull String username) { + this.failedToAuthenticateCalls.add(username); + } + + @Override + protected void loggedIn(@Nonnull String username) { + this.loggedInCalls.add(username); + } + + @Override + protected void failedToLogIn(@Nonnull String username) { + this.failedToLogInCalls.add(username); + } + + @Override + protected void loggedOut(@Nonnull String username) { + this.loggedOutCalls.add(username); + + } + + public static class EventQueue { + private final List eventList = new ArrayList<>(); + + private EventQueue add(T t){ + eventList.add(t); + return this; + } + + public void assertLastEventIsAndThenRemoveIt(T expected){ + assertLastEventIsAndThenRemoveIt(expected::equals); + } + + public void assertLastEventIsAndThenRemoveIt(Predicate predicate){ + if(eventList.isEmpty()){ + fail("event list is empty"); + } + + T t = eventList.remove(eventList.size() - 1); + assertTrue(predicate.test(t)); + eventList.clear(); + } + + public void assertNoNewEvents(){ + assertEquals("list of event should be empty", eventList.size(), 0); + } + + public void clear(){ + eventList.clear(); + } + } +} + From 14605fea838947546da600faff9483cf2ba3f4eb Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Thu, 14 Dec 2017 20:45:24 +0100 Subject: [PATCH 67/85] Introduce the new UserIdCause constructor, which accepts userId as an argument. (#3162) * Introduce UserIdCause constructor, which accepts userId as an argument. * [JENKINS-48467] - UserIdCuase: Add issue link in the TODO comment --- core/src/main/java/hudson/model/Cause.java | 23 ++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/hudson/model/Cause.java b/core/src/main/java/hudson/model/Cause.java index ebfb0fc310..7b16918f04 100644 --- a/core/src/main/java/hudson/model/Cause.java +++ b/core/src/main/java/hudson/model/Cause.java @@ -402,17 +402,36 @@ public abstract class Cause { */ public static class UserIdCause extends Cause { + @CheckForNull private String userId; + /** + * Constructor, which uses the current {@link User}. + */ public UserIdCause() { User user = User.current(); this.userId = (user == null) ? null : user.getId(); } + /** + * Constructor. + * @param userId User ID. {@code null} if the user is unknown. + * @since TODO + */ + public UserIdCause(@CheckForNull String userId) { + this.userId = userId; + } + @Exported(visibility = 3) + @CheckForNull public String getUserId() { return userId; } + + @Nonnull + private String getUserIdOrUnknown() { + return userId != null ? userId : User.getUnknown().getId(); + } @Exported(visibility = 3) public String getUserName() { @@ -435,8 +454,8 @@ public abstract class Cause { @Override public void print(TaskListener listener) { listener.getLogger().println(Messages.Cause_UserIdCause_ShortDescription( - // TODO better to use ModelHyperlinkNote.encodeTo(User), or User.getUrl, since it handles URL escaping - ModelHyperlinkNote.encodeTo("/user/"+getUserId(), getUserName()))); + // TODO JENKINS-48467 - better to use ModelHyperlinkNote.encodeTo(User), or User.getUrl, since it handles URL escaping + ModelHyperlinkNote.encodeTo("/user/"+getUserIdOrUnknown(), getUserName()))); } @Override From 09bcc5d6538b3cfffbf71228ebd1679e3e20d8b2 Mon Sep 17 00:00:00 2001 From: Krishan Bhasin Date: Thu, 14 Dec 2017 19:52:56 +0000 Subject: [PATCH 68/85] [JENKINS-47324] - Reduce usage of File.mkdirs() in FilePath and IOUtils (#3173) * Move an instance of renameTo() to Files.move() * Replace an instance of File.toURI() with an instance of Path.toUri() * Replace mkdirs() with Files.createDirectories() Replace mkdir() with Files.createTempDirectory() * Undo addition of createTempDirectory() as per review comments * Return to use of FilePath#mkdirs(File) and instead modify it to use the new API. * Undo addition of toPath() in a URI conversion as it brings no benefits. * Replace new uses of toPath() with Util.fileToPath() to pre-handle runtime exceptions * Remove * import. move mkdirs() to using FilePath method instead of File method. * Make IOUtils.mkdirs(File) use Java7 API calls * Add back accidentally removed imports. * Fixed use of wildcard import * Use utility method fileToPath() to handle potential exception thrown --- core/src/main/java/hudson/FilePath.java | 18 +++++++++++------- core/src/main/java/hudson/util/IOUtils.java | 19 ++++++------------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 197a27f60b..8d1233ab04 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -79,6 +79,7 @@ import java.net.URL; import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.InvalidPathException; +import java.nio.file.LinkOption; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Arrays; @@ -125,8 +126,10 @@ import org.kohsuke.stapler.Stapler; import static hudson.FilePath.TarCompression.GZIP; import static hudson.Util.deleteFile; +import static hudson.Util.fileToPath; import static hudson.Util.fixEmpty; import static hudson.Util.isSymlink; + import java.util.Collections; /** @@ -1165,7 +1168,7 @@ public final class FilePath implements Serializable { // following Ant task to avoid possible race condition. Thread.sleep(10); - return f.mkdirs() || f.exists(); + return mkdirs(f) || f.exists(); } })) throw new IOException("Failed to mkdirs: "+remote); @@ -1615,7 +1618,7 @@ public final class FilePath implements Serializable { if (Util.NATIVE_CHMOD_MODE) { PosixAPI.jnr().chmod(f.getAbsolutePath(), mask); } else { - Files.setPosixFilePermissions(Util.fileToPath(f), Util.modeToPermissions(mask)); + Files.setPosixFilePermissions(fileToPath(f), Util.modeToPermissions(mask)); } } @@ -1962,7 +1965,7 @@ public final class FilePath implements Serializable { act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - reading(f).renameTo(creating(new File(target.remote))); + Files.move(fileToPath(reading(f)), fileToPath(creating(new File(target.remote))), LinkOption.NOFOLLOW_LINKS); return null; } }); @@ -2024,8 +2027,8 @@ public final class FilePath implements Serializable { File targetFile = new File(target.remote); File targetDir = targetFile.getParentFile(); filterNonNull().mkdirs(targetDir); - Files.createDirectories(Util.fileToPath(targetDir)); - Files.copy(Util.fileToPath(reading(f)), Util.fileToPath(writing(targetFile)), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + Files.createDirectories(fileToPath(targetDir)); + Files.copy(fileToPath(reading(f)), fileToPath(writing(targetFile)), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); return null; } }); @@ -2957,11 +2960,12 @@ public final class FilePath implements Serializable { return f; } - private boolean mkdirs(File dir) { + private boolean mkdirs(File dir) throws IOException { if (dir.exists()) return false; filterNonNull().mkdirs(dir); - return dir.mkdirs(); + Files.createDirectories(fileToPath(dir)); + return true; } private File mkdirsE(File dir) throws IOException { diff --git a/core/src/main/java/hudson/util/IOUtils.java b/core/src/main/java/hudson/util/IOUtils.java index 4168379405..5022dc021f 100644 --- a/core/src/main/java/hudson/util/IOUtils.java +++ b/core/src/main/java/hudson/util/IOUtils.java @@ -13,6 +13,8 @@ import java.util.Collection; import java.util.List; import java.util.regex.Pattern; +import static hudson.Util.fileToPath; + /** * Adds more to commons-io. * @@ -51,20 +53,11 @@ public class IOUtils { * This method returns the 'dir' parameter so that the method call flows better. */ public static File mkdirs(File dir) throws IOException { - if(dir.mkdirs() || dir.exists()) - return dir; - - // following Ant task to avoid possible race condition. try { - Thread.sleep(10); - } catch (InterruptedException e) { - // ignore + return Files.createDirectories(fileToPath(dir)).toFile(); + } catch (UnsupportedOperationException e) { + throw new IOException(e); } - - if (dir.mkdirs() || dir.exists()) - return dir; - - throw new IOException("Failed to create a directory at "+dir); } /** @@ -134,7 +127,7 @@ public class IOUtils { if (Util.NATIVE_CHMOD_MODE) { return PosixAPI.jnr().stat(f.getPath()).mode(); } else { - return Util.permissionsToMode(Files.getPosixFilePermissions(Util.fileToPath(f))); + return Util.permissionsToMode(Files.getPosixFilePermissions(fileToPath(f))); } } catch (IOException cause) { PosixException e = new PosixException("Unable to get file permissions", null); From dce450e1b4565b682f321329caea080293ef0b9b Mon Sep 17 00:00:00 2001 From: Johno Crawford Date: Fri, 15 Dec 2017 15:04:55 +0100 Subject: [PATCH 69/85] Invoke optimistic before computeIfAbsent to avoid contention. --- core/src/main/java/jenkins/model/Jenkins.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 62e611b441..b69d04b516 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -2595,7 +2595,8 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve */ @SuppressWarnings({"unchecked"}) public ExtensionList getExtensionList(Class extensionType) { - return extensionLists.computeIfAbsent(extensionType, key -> ExtensionList.create(this, key)); + ExtensionList extensionList = extensionLists.get(extensionType); + return extensionList != null ? extensionList : extensionLists.computeIfAbsent(extensionType, key -> ExtensionList.create(this, key)); } /** From 3c695a3ed6836abce19c0c71eeca418f0fe9fd66 Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Fri, 15 Dec 2017 23:11:30 +0100 Subject: [PATCH 70/85] [JENKINS-34254] Use released Stapler 1.254 --- core/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/pom.xml b/core/pom.xml index 2a06499268..ecbdfc4919 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -39,7 +39,7 @@ THE SOFTWARE. true - 1.254-SNAPSHOT + 1.254 2.5.6.SEC03 2.4.11 From f0efdbab087ea26342a034da198d055bd7141b8a Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Fri, 15 Dec 2017 23:43:01 +0100 Subject: [PATCH 71/85] [JENKINS-34254] Add test --- .../security/csrf/CrumbFilter/retry.jelly | 2 +- .../security/csrf/DefaultCrumbIssuerTest.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly b/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly index 163e015556..70e615b78e 100644 --- a/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly +++ b/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly @@ -34,7 +34,7 @@ THE SOFTWARE.

${requestURL}

${%warning}

- +
diff --git a/test/src/test/java/hudson/security/csrf/DefaultCrumbIssuerTest.java b/test/src/test/java/hudson/security/csrf/DefaultCrumbIssuerTest.java index f3fddef1af..52dc455faf 100644 --- a/test/src/test/java/hudson/security/csrf/DefaultCrumbIssuerTest.java +++ b/test/src/test/java/hudson/security/csrf/DefaultCrumbIssuerTest.java @@ -8,6 +8,8 @@ package hudson.security.csrf; import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.html.HtmlPage; +import jenkins.model.Jenkins; +import junit.framework.Assert; import net.sf.json.JSONObject; import org.junit.Before; import org.junit.Rule; @@ -135,4 +137,19 @@ public class DefaultCrumbIssuerTest { wc.assertFails("crumbIssuer/api/json?jsonp=hack", HttpURLConnection.HTTP_FORBIDDEN); } + @Issue("JENKINS-34254") + @Test public void testRequirePostErrorPageCrumb() throws Exception { + Jenkins.getInstance().setCrumbIssuer(new DefaultCrumbIssuer(false)); + WebClient wc = r.createWebClient(); + try { + wc.goTo("quietDown"); + fail("expected failure"); + } catch (FailingHttpStatusCodeException ex) { + Assert.assertEquals("expect HTTP 405 method not allowed", 405, ex.getStatusCode()); + } + HtmlPage retry = (HtmlPage) wc.getCurrentWindow().getEnclosedPage(); + HtmlPage success = r.submit(retry.getFormByName("retry")); + Assert.assertTrue("quieting down", r.jenkins.isQuietingDown()); + } + } From 1270ba3b40ade4b822ee7538e31f0692ffbf64d2 Mon Sep 17 00:00:00 2001 From: Devin Nusbaum Date: Sat, 16 Dec 2017 03:23:41 -0500 Subject: [PATCH 72/85] [JENKINS-48405] Use NIO in tryOnceDeleteFile and makeWritable (#3169) * Use NIO in tryOnceDeleteFile and makeWritable * Don't try to set PosixFileAttributes on Windows * Do not create arbitrary exceptions in makeWritable to fix test failures on Windows * Remove unhelpful layer of exception wrapping * Add test exercising Util#makeWritable in Util#tryOnceDeleteFile * Add test for deleting a non-existant file * Return early if changing permissions with NIO succeeds --- core/src/main/java/hudson/Util.java | 87 +++++++++++-------------- core/src/test/java/hudson/UtilTest.java | 32 +++++++-- 2 files changed, 62 insertions(+), 57 deletions(-) diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index 8ee5c7390a..1e13ea9957 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -28,7 +28,6 @@ import jenkins.util.SystemProperties; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.model.TaskListener; -import hudson.os.PosixAPI; import hudson.util.QuotedStringTokenizer; import hudson.util.VariableResolver; @@ -36,16 +35,12 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang.time.FastDateFormat; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; -import org.apache.tools.ant.taskdefs.Chmod; import org.apache.tools.ant.taskdefs.Copy; import org.apache.tools.ant.types.FileSet; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; -import jnr.posix.FileStat; -import jnr.posix.POSIX; - import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -70,6 +65,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.attribute.DosFileAttributes; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -257,16 +253,16 @@ public class Util { * * @param f * What to delete. If a directory, it'll need to be empty. - * @throws IOException if it exists but could not be successfully deleted + * @throws IOException if it exists but could not be successfully deleted, + * or if it represents an invalid {@link Path}. */ private static void tryOnceDeleteFile(File f) throws IOException { - if (!f.delete()) { - if(!f.exists()) - // we are trying to delete a file that no longer exists, so this is not an error - return; - + Path path = fileToPath(f); + try { + Files.deleteIfExists(path); + } catch (IOException e) { // perhaps this file is read-only? - makeWritable(f); + makeWritable(path); /* on Unix both the file and the directory that contains it has to be writable for a file deletion to be successful. (Confirmed on Solaris 9) @@ -280,56 +276,47 @@ public class Util { $ rm x rm: x not removed: Permission denied */ - - makeWritable(f.getParentFile()); - - if(!f.delete() && f.exists()) { - // trouble-shooting. - try { - Files.deleteIfExists(f.toPath()); - } catch (InvalidPathException e) { - throw new IOException(e); - } - + Path parent = path.getParent(); + if (parent != null) { + makeWritable(parent); + } + try { + Files.deleteIfExists(path); + } catch (IOException e2) { // see https://java.net/projects/hudson/lists/users/archive/2008-05/message/357 // I suspect other processes putting files in this directory File[] files = f.listFiles(); if(files!=null && files.length>0) - throw new IOException("Unable to delete " + f.getPath()+" - files in dir: "+Arrays.asList(files)); - throw new IOException("Unable to delete " + f.getPath()); + throw new IOException("Unable to delete " + f.getPath()+" - files in dir: "+Arrays.asList(files), e2); + throw e2; } } } /** - * Makes the given file writable by any means possible. + * Makes the file at the given path writable by any means possible. */ - private static void makeWritable(@Nonnull File f) { - if (f.setWritable(true)) { - return; - } - // TODO do we still need to try anything else? - - // try chmod. this becomes no-op if this is not Unix. - try { - Chmod chmod = new Chmod(); - chmod.setProject(new Project()); - chmod.setFile(f); - chmod.setPerm("u+w"); - chmod.execute(); - } catch (BuildException e) { - LOGGER.log(Level.INFO,"Failed to chmod "+f,e); - } - - try {// try libc chmod - POSIX posix = PosixAPI.jnr(); - String path = f.getAbsolutePath(); - FileStat stat = posix.stat(path); - posix.chmod(path, stat.mode()|0200); // u+w - } catch (Throwable t) { - LOGGER.log(Level.FINE,"Failed to chmod(2) "+f,t); + private static void makeWritable(@Nonnull Path path) throws IOException { + if (!Functions.isWindows()) { + try { + PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class); + Set newPermissions = ((PosixFileAttributes)attrs).permissions(); + newPermissions.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(path, newPermissions); + return; + } catch (NoSuchFileException e) { + return; + } catch (UnsupportedOperationException e) { + // PosixFileAttributes not supported, fall back to old IO. + } } + /** + * We intentionally do not check the return code of setWritable, because if it + * is false we prefer to rethrow the exception thrown by Files.deleteIfExists, + * which will have a more useful message than something we make up here. + */ + path.toFile().setWritable(true); } /** diff --git a/core/src/test/java/hudson/UtilTest.java b/core/src/test/java/hudson/UtilTest.java index 21a8993de4..f1cf614055 100644 --- a/core/src/test/java/hudson/UtilTest.java +++ b/core/src/test/java/hudson/UtilTest.java @@ -36,6 +36,9 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.FileSystemException; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermissions; import static org.hamcrest.CoreMatchers.allOf; @@ -287,12 +290,6 @@ public class UtilTest { @Test public void testDeleteFile_onWindows() throws Exception { Assume.assumeTrue(Functions.isWindows()); - Class c; - try { - c = Class.forName("java.nio.file.FileSystemException"); - } catch (ClassNotFoundException x) { - throw new AssumptionViolatedException("prior to JDK 7", x); - } final int defaultDeletionMax = Util.DELETION_MAX; try { File f = tmp.newFile(); @@ -304,7 +301,7 @@ public class UtilTest { Util.deleteFile(f); fail("should not have been deletable"); } catch (IOException x) { - assertThat(calcExceptionHierarchy(x), hasItem(c)); + assertThat(calcExceptionHierarchy(x), hasItem(FileSystemException.class)); assertThat(x.getMessage(), containsString(f.getPath())); } } finally { @@ -313,6 +310,27 @@ public class UtilTest { } } + @Test + public void testDeleteFileReadOnly() throws Exception { + // Removing the calls to Util#makeWritable in Util#tryOnceDeleteFile should cause this test to fail. + Path file = tmp.newFolder().toPath().resolve("file.tmp"); + Files.createDirectories(file.getParent()); + Files.createFile(file); + // Using old IO so the test can run on Windows. + file.getParent().toFile().setWritable(false); + file.toFile().setWritable(false); + Util.deleteFile(file.toFile()); + assertFalse(Files.exists(file)); + } + + @Test + public void testDeleteFileDoesNotExist() throws Exception { + Path file = tmp.newFolder().toPath().resolve("file.tmp"); + assertFalse(Files.exists(file)); + // Should not throw an exception. + Util.deleteFile(file.toFile()); + } + @Test public void testDeleteContentsRecursive() throws Exception { final File dir = tmp.newFolder(); From 814d202716a6c61c7d371c6a62755d296fe199a5 Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Sat, 16 Dec 2017 09:25:30 +0100 Subject: [PATCH 73/85] [JENKINS-22474] API Token does not require CSRF token (#3129) * [JENKINS-22474] API Token does not require CSRF token - in order to ease the use of the api, we are not requiring the request to have a crumb - in terms of security it's not a problem normally since the CSRF attacks use the cookies and in case of API Token, it's session-less / cookie-less * - adjust the license header * - add test for basic authentication - add test for login process * - add test for form submission using ui (htmlunit), not just login form * - modification requested by Jesse * - pom.xml update to use the last version of jenkins-test-harness (with the token helper methods) - beginning of the simplification of tests * - using the try-with-resource approach to ease readability * - using closure method now * - add missing login transformation * - add unit test * - use withToken - remove useless crumb for GET method - add fail (otherwise the assert in catch is not as useful as it could be) * another bunch of test cases * - for HudsonTestCase, some additional modifications are required: changing the view / different type of management for the variable inside the views * - small other tests * - last batch for the login method * - crumb is not more required since we are using API Token * - converting auth to ApiToken to avoid crumb method * - converting auth to ApiToken to avoid crumb method (second) * - remove usage of closure aware methods * - update the pom using the snapshot as adviced by Jesse - modifications on other class to adapt to the last modifications in JTH * - modifications requested during code review * - also put back my changes to the conflicted file * - correction of the merge :) --- .../jenkins/security/ApiCrumbExclusion.java | 53 +++++++ .../BasicHeaderApiTokenAuthenticator.java | 8 +- test/pom.xml | 2 +- .../bugs/JnlpAccessWithSecuredHudsonTest.java | 3 +- .../HudsonHomeDiskUsageMonitorTest.java | 19 ++- .../hudson/model/AbstractProjectTest.java | 8 +- .../test/java/hudson/model/ExecutorTest.java | 10 +- .../src/test/java/hudson/model/ItemsTest.java | 20 +-- test/src/test/java/hudson/model/JobTest.java | 11 +- .../PasswordParameterDefinitionTest.java | 15 +- .../test/java/hudson/model/ProjectTest.java | 36 ++++- .../src/test/java/hudson/model/QueueTest.java | 18 ++- test/src/test/java/hudson/model/UserTest.java | 4 +- test/src/test/java/hudson/model/ViewTest.java | 2 +- .../security/ExtendedReadPermissionTest.java | 12 +- .../HudsonPrivateSecurityRealmTest.java | 1 + .../test/java/hudson/security/LoginTest.java | 3 +- .../util/RobustReflectionConverterTest.java | 28 ++-- .../test/java/jenkins/model/JenkinsTest.java | 25 +-- .../security/ApiCrumbExclusionTest.java | 146 ++++++++++++++++++ .../security/ApiTokenPropertyTest.java | 1 - .../security/BasicHeaderProcessorTest.java | 104 +++++++------ .../jenkins/security/Security380Test.java | 1 + test/src/test/java/lib/form/PasswordTest.java | 81 ++++++---- .../index.jelly} | 2 +- 25 files changed, 448 insertions(+), 165 deletions(-) create mode 100644 core/src/main/java/jenkins/security/ApiCrumbExclusion.java create mode 100644 test/src/test/java/jenkins/security/ApiCrumbExclusionTest.java rename test/src/test/resources/lib/form/PasswordTest/{test1.jelly => SecretNotPlainText/index.jelly} (91%) diff --git a/core/src/main/java/jenkins/security/ApiCrumbExclusion.java b/core/src/main/java/jenkins/security/ApiCrumbExclusion.java new file mode 100644 index 0000000000..8eea5cee13 --- /dev/null +++ b/core/src/main/java/jenkins/security/ApiCrumbExclusion.java @@ -0,0 +1,53 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * 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 jenkins.security; + +import hudson.Extension; +import hudson.security.csrf.CrumbExclusion; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * JENKINS-22474: Makes API Token calls bypass CSRF protection to ease usage + */ +@Symbol("apiToken") +@Extension +@Restricted(DoNotUse.class) +public class ApiCrumbExclusion extends CrumbExclusion { + @Override + public boolean process(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + if (Boolean.TRUE.equals(request.getAttribute(BasicHeaderApiTokenAuthenticator.class.getName()))) { + chain.doFilter(request, response); + return true; + } + return false; + } +} diff --git a/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java b/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java index f0f6491e22..345281fdce 100644 --- a/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java +++ b/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java @@ -32,13 +32,12 @@ public class BasicHeaderApiTokenAuthenticator extends BasicHeaderAuthenticator { User u = User.getById(username, true); ApiTokenProperty t = u.getProperty(ApiTokenProperty.class); if (t!=null && t.matchesPassword(password)) { + Authentication auth; try { UserDetails userDetails = u.getUserDetailsForImpersonation(); - Authentication auth = u.impersonate(userDetails); + auth = u.impersonate(userDetails); SecurityListener.fireAuthenticated(userDetails); - - return auth; } catch (UsernameNotFoundException x) { // The token was valid, but the impersonation failed. This token is clearly not his real password, // so there's no point in continuing the request processing. Report this error and abort. @@ -47,6 +46,9 @@ public class BasicHeaderApiTokenAuthenticator extends BasicHeaderAuthenticator { } catch (DataAccessException x) { throw new ServletException(x); } + + req.setAttribute(BasicHeaderApiTokenAuthenticator.class.getName(), true); + return auth; } return null; } diff --git a/test/pom.xml b/test/pom.xml index 578d451007..8745d54534 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -53,7 +53,7 @@ THE SOFTWARE. ${project.groupId} jenkins-test-harness - 2.31 + 2.32 test diff --git a/test/src/test/java/hudson/bugs/JnlpAccessWithSecuredHudsonTest.java b/test/src/test/java/hudson/bugs/JnlpAccessWithSecuredHudsonTest.java index 8c970e8cc9..906adcf6e7 100644 --- a/test/src/test/java/hudson/bugs/JnlpAccessWithSecuredHudsonTest.java +++ b/test/src/test/java/hudson/bugs/JnlpAccessWithSecuredHudsonTest.java @@ -31,6 +31,7 @@ import hudson.Proc; import hudson.cli.util.ScriptLoader; import hudson.model.Node.Mode; import hudson.model.Slave; +import hudson.model.User; import hudson.remoting.Channel; import hudson.slaves.JNLPLauncher; import hudson.slaves.RetentionStrategy; @@ -86,7 +87,7 @@ public class JnlpAccessWithSecuredHudsonTest { public void anonymousCanAlwaysLoadJARs() throws Exception { r.jenkins.setNodes(Collections.singletonList(createNewJnlpSlave("test"))); JenkinsRule.WebClient wc = r.createWebClient(); - HtmlPage p = wc.login("alice").goTo("computer/test/"); + HtmlPage p = wc.withBasicApiToken(User.getById("alice", true)).goTo("computer/test/"); // this fresh WebClient doesn't have a login cookie and represent JNLP launcher JenkinsRule.WebClient jnlpAgent = r.createWebClient(); diff --git a/test/src/test/java/hudson/diagnosis/HudsonHomeDiskUsageMonitorTest.java b/test/src/test/java/hudson/diagnosis/HudsonHomeDiskUsageMonitorTest.java index d416b3e451..3b3da3b177 100644 --- a/test/src/test/java/hudson/diagnosis/HudsonHomeDiskUsageMonitorTest.java +++ b/test/src/test/java/hudson/diagnosis/HudsonHomeDiskUsageMonitorTest.java @@ -11,10 +11,7 @@ import com.gargoylesoftware.htmlunit.WebRequest; import com.gargoylesoftware.htmlunit.util.NameValuePair; import hudson.model.User; import hudson.security.GlobalMatrixAuthorizationStrategy; -import hudson.security.HudsonPrivateSecurityRealm; -import hudson.security.Permission; import jenkins.model.Jenkins; -import org.acegisecurity.context.SecurityContextHolder; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.Issue; @@ -25,6 +22,7 @@ import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.ElementNotFoundException; import java.io.IOException; +import java.net.URL; import java.util.Collections; /** @@ -73,28 +71,33 @@ public class HudsonHomeDiskUsageMonitorTest { auth.add(Jenkins.READ, "users"); j.jenkins.setAuthorizationStrategy(auth); - WebRequest request = new WebRequest(wc.createCrumbedUrl("administrativeMonitor/hudsonHomeIsFull/act"), HttpMethod.POST); + User bob = User.getById("bob", true); + User administrator = User.getById("administrator", true); + + WebRequest request = new WebRequest(new URL(wc.getContextPath() + "administrativeMonitor/hudsonHomeIsFull/act"), HttpMethod.POST); NameValuePair param = new NameValuePair("no", "true"); request.setRequestParameters(Collections.singletonList(param)); HudsonHomeDiskUsageMonitor mon = HudsonHomeDiskUsageMonitor.get(); + wc.withBasicApiToken(bob); try { - wc.login("bob"); wc.getPage(request); + fail(); } catch (FailingHttpStatusCodeException e) { assertEquals(403, e.getStatusCode()); } assertTrue(mon.isEnabled()); + WebRequest requestReadOnly = new WebRequest(new URL(wc.getContextPath() + "administrativeMonitor/hudsonHomeIsFull"), HttpMethod.GET); try { - WebRequest getIndex = new WebRequest(wc.createCrumbedUrl("administrativeMonitor/hudsonHomeIsFull"), HttpMethod.GET); - wc.getPage(getIndex); + wc.getPage(requestReadOnly); + fail(); } catch (FailingHttpStatusCodeException e) { assertEquals(403, e.getStatusCode()); } - wc.login("administrator"); + wc.withBasicApiToken(administrator); wc.getPage(request); assertFalse(mon.isEnabled()); diff --git a/test/src/test/java/hudson/model/AbstractProjectTest.java b/test/src/test/java/hudson/model/AbstractProjectTest.java index 308fc12993..8ba1982b11 100644 --- a/test/src/test/java/hudson/model/AbstractProjectTest.java +++ b/test/src/test/java/hudson/model/AbstractProjectTest.java @@ -629,16 +629,16 @@ public class AbstractProjectTest { grant(Item.READ).everywhere().to("alice"). grant(Item.READ).onItems(us).to("bob"). grant(Item.READ).onItems(ds).to("charlie")); - String api = j.createWebClient().login("alice").goTo(us.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); + String api = j.createWebClient().withBasicCredentials("alice").goTo(us.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); System.out.println(api); assertThat(api, containsString("downstream-project")); - api = j.createWebClient().login("alice").goTo(ds.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); + api = j.createWebClient().withBasicCredentials("alice").goTo(ds.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); System.out.println(api); assertThat(api, containsString("upstream-project")); - api = j.createWebClient().login("bob").goTo(us.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); + api = j.createWebClient().withBasicCredentials("bob").goTo(us.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); System.out.println(api); assertThat(api, not(containsString("downstream-project"))); - api = j.createWebClient().login("charlie").goTo(ds.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); + api = j.createWebClient().withBasicCredentials("charlie").goTo(ds.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); System.out.println(api); assertThat(api, not(containsString("upstream-project"))); } diff --git a/test/src/test/java/hudson/model/ExecutorTest.java b/test/src/test/java/hudson/model/ExecutorTest.java index e13ff1c345..70254456fd 100644 --- a/test/src/test/java/hudson/model/ExecutorTest.java +++ b/test/src/test/java/hudson/model/ExecutorTest.java @@ -136,10 +136,16 @@ public class ExecutorTest { grant(Jenkins.READ).everywhere().toEveryone(). grant(Item.READ).onItems(publicProject).toEveryone(). grant(Item.READ).onItems(secretProject).to("has-security-clearance")); - String api = j.createWebClient().login("has-security-clearance").goTo(slave.toComputer().getUrl() + "api/json?pretty&depth=1", null).getWebResponse().getContentAsString(); + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.withBasicCredentials("has-security-clearance"); + String api = wc.goTo(slave.toComputer().getUrl() + "api/json?pretty&depth=1", null).getWebResponse().getContentAsString(); System.out.println(api); assertThat(api, allOf(containsString("public-project"), containsString("secret-project"))); - api = j.createWebClient().login("regular-joe").goTo(slave.toComputer().getUrl() + "api/json?pretty&depth=1", null).getWebResponse().getContentAsString(); + + wc = j.createWebClient(); + wc.withBasicCredentials("regular-joe"); + api = wc.goTo(slave.toComputer().getUrl() + "api/json?pretty&depth=1", null).getWebResponse().getContentAsString(); System.out.println(api); assertThat(api, allOf(containsString("public-project"), not(containsString("secret-project")))); } diff --git a/test/src/test/java/hudson/model/ItemsTest.java b/test/src/test/java/hudson/model/ItemsTest.java index 32ec8e6e47..40dfd2fa8d 100644 --- a/test/src/test/java/hudson/model/ItemsTest.java +++ b/test/src/test/java/hudson/model/ItemsTest.java @@ -45,6 +45,7 @@ import org.acegisecurity.context.SecurityContext; import org.acegisecurity.context.SecurityContextHolder; import org.apache.commons.httpclient.HttpStatus; +import org.junit.Before; import org.junit.Test; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -123,6 +124,8 @@ public class ItemsTest { // TODO would be more efficient to run these all as a single test case, but after a few Jetty seems to stop serving new content and new requests just hang. private void overwriteTargetSetUp() throws Exception { + User.getById("attacker", true); + // A fully visible item: r.createFreeStyleProject("visible").setDescription("visible"); // An item known to exist but not visible: @@ -185,7 +188,7 @@ public class ItemsTest { JenkinsRule.WebClient wc = wc(r); wc.getOptions().setRedirectEnabled(false); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); // redirect perversely counts as a failure - WebResponse webResponse = wc.getPage(new WebRequest(createCrumbedUrl(r, wc, "createItem?name=" + target + "&mode=hudson.model.FreeStyleProject"), HttpMethod.POST)).getWebResponse(); + WebResponse webResponse = wc.getPage(new WebRequest(new URL(wc.getContextPath() + "createItem?name=" + target + "&mode=hudson.model.FreeStyleProject"), HttpMethod.POST)).getWebResponse(); if (webResponse.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) { throw new FailingHttpStatusCodeException(webResponse); } @@ -198,7 +201,7 @@ public class ItemsTest { JenkinsRule.WebClient wc = wc(r); wc.getOptions().setRedirectEnabled(false); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); - WebResponse webResponse = wc.getPage(new WebRequest(createCrumbedUrl(r, wc, "createItem?name=" + target + "&mode=copy&from=dupe"), HttpMethod.POST)).getWebResponse(); + WebResponse webResponse = wc.getPage(new WebRequest(new URL(wc.getContextPath() + "createItem?name=" + target + "&mode=copy&from=dupe"), HttpMethod.POST)).getWebResponse(); r.jenkins.getItem("dupe").delete(); if (webResponse.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) { throw new FailingHttpStatusCodeException(webResponse); @@ -209,7 +212,7 @@ public class ItemsTest { REST_CREATE { @Override void run(JenkinsRule r, String target) throws Exception { JenkinsRule.WebClient wc = wc(r); - WebRequest req = new WebRequest(createCrumbedUrl(r, wc, "createItem?name=" + target), HttpMethod.POST); + WebRequest req = new WebRequest(new URL(wc.getContextPath() + "createItem?name=" + target), HttpMethod.POST); req.setAdditionalHeader("Content-Type", "application/xml"); req.setRequestBody(""); wc.getPage(req); @@ -222,7 +225,7 @@ public class ItemsTest { JenkinsRule.WebClient wc = wc(r); wc.getOptions().setRedirectEnabled(false); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); - WebResponse webResponse = wc.getPage(new WebRequest(createCrumbedUrl(r, wc, "job/dupe/doRename?newName=" + target), HttpMethod.POST)).getWebResponse(); + WebResponse webResponse = wc.getPage(new WebRequest(new URL(wc.getContextPath() + "job/dupe/doRename?newName=" + target), HttpMethod.POST)).getWebResponse(); if (webResponse.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) { r.jenkins.getItem("dupe").delete(); throw new FailingHttpStatusCodeException(webResponse); @@ -275,14 +278,7 @@ public class ItemsTest { }; abstract void run(JenkinsRule r, String target) throws Exception; private static final JenkinsRule.WebClient wc(JenkinsRule r) throws Exception { - return r.createWebClient().login("attacker"); - } - // TODO replace with standard version once it is fixed to detect an existing query string - private static URL createCrumbedUrl(JenkinsRule r, JenkinsRule.WebClient wc, String relativePath) throws IOException { - CrumbIssuer issuer = r.jenkins.getCrumbIssuer(); - String crumbName = issuer.getDescriptor().getCrumbRequestField(); - String crumb = issuer.getCrumb(null); - return new URL(wc.getContextPath() + relativePath + (relativePath.contains("?") ? "&" : "?") + crumbName + "=" + crumb); + return r.createWebClient().withBasicApiToken("attacker"); } } diff --git a/test/src/test/java/hudson/model/JobTest.java b/test/src/test/java/hudson/model/JobTest.java index d20b9c28a6..58e14cdd33 100644 --- a/test/src/test/java/hudson/model/JobTest.java +++ b/test/src/test/java/hudson/model/JobTest.java @@ -202,7 +202,7 @@ public class JobTest { JenkinsRule.WebClient wc = j.createWebClient(); wc.assertFails("job/testJob/", HttpURLConnection.HTTP_NOT_FOUND); wc.assertFails("jobCaseInsensitive/testJob/", HttpURLConnection.HTTP_NOT_FOUND); - wc.login("joe"); // Has Item.READ permission + wc.withBasicCredentials("joe"); // Has Item.READ permission // Verify we can access both URLs: wc.goTo("job/testJob/"); wc.goTo("jobCaseInsensitive/TESTJOB/"); @@ -216,11 +216,14 @@ public class JobTest { Item.EXTENDED_READ.setEnabled(true); try { wc.assertFails("job/testJob/config.xml", HttpURLConnection.HTTP_FORBIDDEN); - wc.login("alice"); // Has CONFIGURE and EXTENDED_READ permission + + wc.withBasicApiToken(User.getById("alice", true)); // Has CONFIGURE and EXTENDED_READ permission tryConfigDotXml(wc, 500, "Both perms; should get 500"); - wc.login("bob"); // Has only CONFIGURE permission (this should imply EXTENDED_READ) + + wc.withBasicApiToken(User.getById("bob", true)); // Has only CONFIGURE permission (this should imply EXTENDED_READ) tryConfigDotXml(wc, 500, "Config perm should imply EXTENDED_READ"); - wc.login("charlie"); // Has only EXTENDED_READ permission + + wc.withBasicApiToken(User.getById("charlie", true)); // Has only EXTENDED_READ permission tryConfigDotXml(wc, 403, "No permission, should get 403"); } finally { Item.EXTENDED_READ.setEnabled(saveEnabled); diff --git a/test/src/test/java/hudson/model/PasswordParameterDefinitionTest.java b/test/src/test/java/hudson/model/PasswordParameterDefinitionTest.java index 2129c9fce9..4fab18d601 100644 --- a/test/src/test/java/hudson/model/PasswordParameterDefinitionTest.java +++ b/test/src/test/java/hudson/model/PasswordParameterDefinitionTest.java @@ -62,16 +62,21 @@ public class PasswordParameterDefinitionTest { return true; } }); + + User admin = User.getById("admin", true); + User dev = User.getById("dev", true); + JenkinsRule.WebClient wc = j.createWebClient(); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); // ParametersDefinitionProperty/index.jelly sends a 405 but really it is OK // Control case: admin can use default value. - j.submit(wc.login("admin").getPage(p, "build?delay=0sec").getFormByName("parameters")); + j.submit(wc.withBasicApiToken(admin).getPage(p, "build?delay=0sec").getFormByName("parameters")); j.waitUntilNoActivity(); FreeStyleBuild b1 = p.getLastBuild(); assertEquals(1, b1.getNumber()); j.assertLogContains("I heard about a s3cr3t!", j.assertBuildStatusSuccess(b1)); + // Another control case: anyone can enter a different value. - HtmlForm form = wc.login("dev").getPage(p, "build?delay=0sec").getFormByName("parameters"); + HtmlForm form = wc.withBasicApiToken(dev).getPage(p, "build?delay=0sec").getFormByName("parameters"); HtmlPasswordInput input = form.getInputByName("value"); input.setText("rumor"); j.submit(form); @@ -79,14 +84,16 @@ public class PasswordParameterDefinitionTest { FreeStyleBuild b2 = p.getLastBuild(); assertEquals(2, b2.getNumber()); j.assertLogContains("I heard about a rumor!", j.assertBuildStatusSuccess(b2)); + // Test case: anyone can use default value. - j.submit(wc.login("dev").getPage(p, "build?delay=0sec").getFormByName("parameters")); + j.submit(wc.withBasicApiToken(dev).getPage(p, "build?delay=0sec").getFormByName("parameters")); j.waitUntilNoActivity(); FreeStyleBuild b3 = p.getLastBuild(); assertEquals(3, b3.getNumber()); j.assertLogContains("I heard about a s3cr3t!", j.assertBuildStatusSuccess(b3)); + // Another control case: blank values. - form = wc.login("dev").getPage(p, "build?delay=0sec").getFormByName("parameters"); + form = wc.withBasicApiToken(dev).getPage(p, "build?delay=0sec").getFormByName("parameters"); input = form.getInputByName("value"); input.setText(""); j.submit(form); diff --git a/test/src/test/java/hudson/model/ProjectTest.java b/test/src/test/java/hudson/model/ProjectTest.java index a2d5bf3f67..f966f4e56b 100644 --- a/test/src/test/java/hudson/model/ProjectTest.java +++ b/test/src/test/java/hudson/model/ProjectTest.java @@ -32,9 +32,12 @@ import hudson.tasks.*; import hudson.security.HudsonPrivateSecurityRealm; import hudson.security.GlobalMatrixAuthorizationStrategy; +import java.io.Closeable; +import java.net.HttpURLConnection; import java.net.URL; import java.util.Collections; +import hudson.util.Scrambler; import org.jvnet.hudson.reactor.ReactorException; import org.jvnet.hudson.test.FakeChangeLogSCM; import hudson.scm.SCMRevisionState; @@ -553,9 +556,8 @@ public class ProjectTest { GlobalMatrixAuthorizationStrategy auth = new GlobalMatrixAuthorizationStrategy(); j.jenkins.setAuthorizationStrategy(auth); j.jenkins.setCrumbIssuer(null); - HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false); - j.jenkins.setSecurityRealm(realm); - User user = realm.createAccount("John Smith", "password"); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + User user = User.getById("john", true); try (ACLContext as = ACL.as(user)) { project.doDoDelete(null, null); fail("User should not have permission to build project"); @@ -568,7 +570,13 @@ public class ProjectTest { auth.add(Jenkins.READ, user.getId()); auth.add(Job.READ, user.getId()); auth.add(Job.DELETE, user.getId()); - List forms = j.createWebClient().login(user.getId(), "password").goTo(project.getUrl() + "delete").getForms(); + + // use Basic to speedup the test, normally it's pure UI testing + JenkinsRule.WebClient wc = j.createWebClient(); + wc.withBasicCredentials(user.getId()); + HtmlPage p = wc.goTo(project.getUrl() + "delete"); + + List forms = p.getForms(); for(HtmlForm form:forms){ if("doDelete".equals(form.getAttribute("action"))){ j.submit(form); @@ -605,9 +613,13 @@ public class ProjectTest { String cmd = "echo hello > change.log"; project.getBuildersList().add(Functions.isWindows()? new BatchFile(cmd) : new Shell(cmd)); j.buildAndAssertSuccess(project); - JenkinsRule.WebClient wc = j.createWebClient().login(user.getId(), "password"); + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.withBasicCredentials(user.getId(), "password"); WebRequest request = new WebRequest(new URL(wc.getContextPath() + project.getUrl() + "doWipeOutWorkspace"), HttpMethod.POST); HtmlPage p = wc.getPage(request); + assertEquals(p.getWebResponse().getStatusCode(), 200); + Thread.sleep(500); assertFalse("Workspace should not exist.", project.getSomeWorkspace().exists()); } @@ -633,7 +645,12 @@ public class ProjectTest { auth.add(Job.READ, user.getId()); auth.add(Job.CONFIGURE, user.getId()); auth.add(Jenkins.READ, user.getId()); - List forms = j.createWebClient().login(user.getId(), "password").goTo(project.getUrl()).getForms(); + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.withBasicCredentials(user.getId(), "password"); + HtmlPage p = wc.goTo(project.getUrl()); + + List forms = p.getForms(); for(HtmlForm form:forms){ if("disable".equals(form.getAttribute("action"))){ j.submit(form); @@ -666,7 +683,12 @@ public class ProjectTest { auth.add(Job.READ, user.getId()); auth.add(Job.CONFIGURE, user.getId()); auth.add(Jenkins.READ, user.getId()); - List forms = j.createWebClient().login(user.getId(), "password").goTo(project.getUrl()).getForms(); + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.withBasicCredentials(user.getId(), "password"); + HtmlPage p = wc.goTo(project.getUrl()); + + List forms = p.getForms(); for(HtmlForm form:forms){ if("enable".equals(form.getAttribute("action"))){ j.submit(form); diff --git a/test/src/test/java/hudson/model/QueueTest.java b/test/src/test/java/hudson/model/QueueTest.java index 5e626aea3c..689b63e5ae 100644 --- a/test/src/test/java/hudson/model/QueueTest.java +++ b/test/src/test/java/hudson/model/QueueTest.java @@ -943,8 +943,12 @@ public class QueueTest { project.getBuildersList().add(new SleepBuilder(10)); project.scheduleBuild2(0); + User alice = User.getById("alice", true); + User bob = User.getById("bob", true); + User james = User.getById("james", true); + JenkinsRule.WebClient webClient = r.createWebClient(); - webClient.login("bob", "bob"); + webClient.withBasicApiToken(bob); XmlPage p = webClient.goToXml("queue/api/xml"); //bob has permission on the project and will be able to see it in the queue together with information such as the URL and the name. @@ -959,13 +963,15 @@ public class QueueTest { } } } + webClient = r.createWebClient(); - webClient.login("alice"); + webClient.withBasicApiToken(alice); XmlPage p2 = webClient.goToXml("queue/api/xml"); //alice does not have permission on the project and will not see it in the queue. assertTrue(p2.getByXPath("/queue/node()").isEmpty()); + webClient = r.createWebClient(); - webClient.login("james"); + webClient.withBasicApiToken(james); XmlPage p3 = webClient.goToXml("queue/api/xml"); //james has DISCOVER permission on the project and will only be able to see the task name. @@ -975,9 +981,9 @@ public class QueueTest { // Also check individual item exports. String url = project.getQueueItem().getUrl() + "api/xml"; - r.createWebClient().login("bob").goToXml(url); // OK, 200 - r.createWebClient().login("james").assertFails(url, HttpURLConnection.HTTP_FORBIDDEN); // only DISCOVER → AccessDeniedException - r.createWebClient().login("alice").assertFails(url, HttpURLConnection.HTTP_NOT_FOUND); // not even DISCOVER + r.createWebClient().withBasicApiToken(bob).goToXml(url); // OK, 200 + r.createWebClient().withBasicApiToken(james).assertFails(url, HttpURLConnection.HTTP_FORBIDDEN); // only DISCOVER → AccessDeniedException + r.createWebClient().withBasicApiToken(alice).assertFails(url, HttpURLConnection.HTTP_NOT_FOUND); // not even DISCOVER } //we force the project not to be executed so that it stays in the queue diff --git a/test/src/test/java/hudson/model/UserTest.java b/test/src/test/java/hudson/model/UserTest.java index b81105fe55..34bb26a1c9 100644 --- a/test/src/test/java/hudson/model/UserTest.java +++ b/test/src/test/java/hudson/model/UserTest.java @@ -415,7 +415,7 @@ public class UserTest { auth.add(Jenkins.ADMINISTER, user.getId()); auth.add(Jenkins.READ, user2.getId()); SecurityContextHolder.getContext().setAuthentication(user.impersonate()); - HtmlForm form = j.createWebClient().login(user.getId(), "password").goTo(user2.getUrl() + "/configure").getFormByName("config"); + HtmlForm form = j.createWebClient().withBasicCredentials(user.getId(), "password").goTo(user2.getUrl() + "/configure").getFormByName("config"); form.getInputByName("_.fullName").setValueAttribute("Alice Smith"); j.submit(form); assertEquals("User should have full name Alice Smith.", "Alice Smith", user2.getFullName()); @@ -429,7 +429,7 @@ public class UserTest { fail("AccessDeniedException should be thrown."); } } - form = j.createWebClient().login(user2.getId(), "password").goTo(user2.getUrl() + "/configure").getFormByName("config"); + form = j.createWebClient().withBasicCredentials(user2.getId(), "password").goTo(user2.getUrl() + "/configure").getFormByName("config"); form.getInputByName("_.fullName").setValueAttribute("John"); j.submit(form); diff --git a/test/src/test/java/hudson/model/ViewTest.java b/test/src/test/java/hudson/model/ViewTest.java index 7a70d87df2..21aa56fc38 100644 --- a/test/src/test/java/hudson/model/ViewTest.java +++ b/test/src/test/java/hudson/model/ViewTest.java @@ -529,7 +529,7 @@ public class ViewTest { return null; } }); - JenkinsRule.WebClient wc = j.createWebClient().login("admin"); + JenkinsRule.WebClient wc = j.createWebClient().withBasicCredentials("admin"); assertEquals("original ${rootURL}/checkJobName still supported", "
", wc.goTo("checkJobName?value=stuff").getWebResponse().getContentAsString()); assertEquals("but now possible on a view in a folder", "
", wc.goTo("job/d1/view/All/checkJobName?value=stuff").getWebResponse().getContentAsString()); } diff --git a/test/src/test/java/hudson/security/ExtendedReadPermissionTest.java b/test/src/test/java/hudson/security/ExtendedReadPermissionTest.java index f989e1439a..04d3820e21 100644 --- a/test/src/test/java/hudson/security/ExtendedReadPermissionTest.java +++ b/test/src/test/java/hudson/security/ExtendedReadPermissionTest.java @@ -52,7 +52,9 @@ public class ExtendedReadPermissionTest { GlobalMatrixAuthorizationStrategy gas = (GlobalMatrixAuthorizationStrategy)as; assertTrue("Charlie should have extended read for this test", gas.hasExplicitPermission("charlie",Item.EXTENDED_READ)); - JenkinsRule.WebClient wc = r.createWebClient().login("charlie","charlie"); + JenkinsRule.WebClient wc = r.createWebClient(); + wc.withBasicCredentials("charlie"); + HtmlPage page = wc.goTo("job/a/configure"); HtmlForm form = page.getFormByName("config"); HtmlButton saveButton = r.getButtonByCaption(form,"Save"); @@ -68,7 +70,9 @@ public class ExtendedReadPermissionTest { GlobalMatrixAuthorizationStrategy gas = (GlobalMatrixAuthorizationStrategy)as; assertFalse("Charlie should not have extended read for this test", gas.hasExplicitPermission("charlie",Item.EXTENDED_READ)); - JenkinsRule.WebClient wc = r.createWebClient().login("charlie","charlie"); + JenkinsRule.WebClient wc = r.createWebClient(); + wc.withBasicCredentials("charlie"); + wc.assertFails("job/a/configure", HttpURLConnection.HTTP_FORBIDDEN); } @@ -81,7 +85,9 @@ public class ExtendedReadPermissionTest { GlobalMatrixAuthorizationStrategy gas = (GlobalMatrixAuthorizationStrategy)as; assertFalse("Bob should not have extended read for this test", gas.hasExplicitPermission("bob",Item.EXTENDED_READ)); - JenkinsRule.WebClient wc = r.createWebClient().login("bob","bob"); + JenkinsRule.WebClient wc = r.createWebClient(); + wc.withBasicCredentials("bob"); + wc.assertFails("job/a/configure", HttpURLConnection.HTTP_FORBIDDEN); } diff --git a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java index 5e51c5ff04..e58fb48d0b 100644 --- a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java +++ b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java @@ -91,6 +91,7 @@ public class HudsonPrivateSecurityRealmTest { // verify the sanity that the password is really used // this should fail j.createWebClient().login("bob", "bob"); + fail(); } catch (FailingHttpStatusCodeException e) { assertEquals(401,e.getStatusCode()); } diff --git a/test/src/test/java/hudson/security/LoginTest.java b/test/src/test/java/hudson/security/LoginTest.java index 86c1a0662c..91ef0e6e34 100644 --- a/test/src/test/java/hudson/security/LoginTest.java +++ b/test/src/test/java/hudson/security/LoginTest.java @@ -11,6 +11,7 @@ import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput; +import hudson.model.User; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -56,7 +57,7 @@ public class LoginTest { WebClient wc = j.createWebClient(); wc.assertFails("loginError", SC_UNAUTHORIZED); // but not once the user logs in. - verifyNotError(wc.login("alice")); + verifyNotError(wc.withBasicApiToken(User.getById("alice", true))); } private HtmlForm prepareLoginFormWithRememberMeChecked(WebClient wc) throws IOException, org.xml.sax.SAXException { diff --git a/test/src/test/java/hudson/util/RobustReflectionConverterTest.java b/test/src/test/java/hudson/util/RobustReflectionConverterTest.java index 0bff3490cf..c57acd55cd 100644 --- a/test/src/test/java/hudson/util/RobustReflectionConverterTest.java +++ b/test/src/test/java/hudson/util/RobustReflectionConverterTest.java @@ -27,16 +27,18 @@ package hudson.util; import hudson.cli.CLICommandInvoker; import hudson.diagnosis.OldDataMonitor; import hudson.model.AbstractDescribableImpl; -import hudson.model.Items; -import hudson.model.JobProperty; -import hudson.model.JobPropertyDescriptor; import hudson.model.Descriptor; import hudson.model.FreeStyleProject; +import hudson.model.Items; import hudson.model.Job; +import hudson.model.JobProperty; +import hudson.model.JobPropertyDescriptor; import hudson.model.Saveable; +import hudson.model.User; import hudson.security.ACL; import java.io.ByteArrayInputStream; +import java.net.URL; import java.util.Collections; import java.util.Map; @@ -81,7 +83,7 @@ public class RobustReflectionConverterTest { // GUI related implementations (@DataBoundConstructor and newInstance) aren't used actually // (no jelly files are provides and they don't work actually), // but written to clarify a use case. - public static class AcceptOnlySpecificKeyword extends AbstractDescribableImpl{ + public static class AcceptOnlySpecificKeyword extends AbstractDescribableImpl { public static final String ACCEPT_KEYWORD = "accept"; private final String keyword; @@ -185,7 +187,9 @@ public class RobustReflectionConverterTest { @Test public void testRestInterfaceFailure() throws Exception { Items.XSTREAM2.addCriticalField(KeywordProperty.class, "criticalField"); - + + User test = User.getById("test", true); + // without addCriticalField. This is accepted. { FreeStyleProject p = r.createFreeStyleProject(); @@ -198,11 +202,8 @@ public class RobustReflectionConverterTest { // Configure a bad keyword via REST. r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); WebClient wc = r.createWebClient(); - wc.login("test", "test"); - WebRequest req = new WebRequest( - wc.createCrumbedUrl(String.format("%s/config.xml", p.getUrl())), - HttpMethod.POST - ); + wc.withBasicApiToken(test); + WebRequest req = new WebRequest(new URL(wc.getContextPath() + String.format("%s/config.xml", p.getUrl())), HttpMethod.POST); req.setEncodingType(null); req.setRequestBody(String.format(CONFIGURATION_TEMPLATE, "badvalue", AcceptOnlySpecificKeyword.ACCEPT_KEYWORD)); wc.getPage(req); @@ -231,11 +232,8 @@ public class RobustReflectionConverterTest { // Configure a bad keyword via REST. r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); WebClient wc = r.createWebClient(); - wc.login("test", "test"); - WebRequest req = new WebRequest( - wc.createCrumbedUrl(String.format("%s/config.xml", p.getUrl())), - HttpMethod.POST - ); + wc.withBasicApiToken(test); + WebRequest req = new WebRequest(new URL(wc.getContextPath() + String.format("%s/config.xml", p.getUrl())), HttpMethod.POST); req.setEncodingType(null); req.setRequestBody(String.format(CONFIGURATION_TEMPLATE, AcceptOnlySpecificKeyword.ACCEPT_KEYWORD, "badvalue")); diff --git a/test/src/test/java/jenkins/model/JenkinsTest.java b/test/src/test/java/jenkins/model/JenkinsTest.java index 2e488bd80b..a68ee0a4d6 100644 --- a/test/src/test/java/jenkins/model/JenkinsTest.java +++ b/test/src/test/java/jenkins/model/JenkinsTest.java @@ -314,20 +314,23 @@ public class JenkinsTest { grant(Jenkins.READ).everywhere().to("bob"). grantWithoutImplication(Jenkins.ADMINISTER, Jenkins.READ).everywhere().to("charlie")); WebClient wc = j.createWebClient(); - wc.login("alice"); + + wc.withBasicApiToken(User.getById("alice", true)); wc.goTo("script"); wc.assertFails("script?script=System.setProperty('hack','me')", HttpURLConnection.HTTP_BAD_METHOD); assertNull(System.getProperty("hack")); WebRequest req = new WebRequest(new URL(wc.getContextPath() + "script?script=System.setProperty('hack','me')"), HttpMethod.POST); - wc.getPage(wc.addCrumb(req)); + wc.getPage(req); assertEquals("me", System.getProperty("hack")); wc.assertFails("scriptText?script=System.setProperty('hack','me')", HttpURLConnection.HTTP_BAD_METHOD); req = new WebRequest(new URL(wc.getContextPath() + "scriptText?script=System.setProperty('huck','you')"), HttpMethod.POST); - wc.getPage(wc.addCrumb(req)); + wc.getPage(req); assertEquals("you", System.getProperty("huck")); - wc.login("bob"); + + wc.withBasicApiToken(User.getById("bob", true)); wc.assertFails("script", HttpURLConnection.HTTP_FORBIDDEN); - wc.login("charlie"); + + wc.withBasicApiToken(User.getById("charlie", true)); wc.assertFails("script", HttpURLConnection.HTTP_FORBIDDEN); } @@ -338,18 +341,22 @@ public class JenkinsTest { grant(Jenkins.ADMINISTER).everywhere().to("alice"). grant(Jenkins.READ).everywhere().to("bob"). grantWithoutImplication(Jenkins.ADMINISTER, Jenkins.READ).everywhere().to("charlie")); + WebClient wc = j.createWebClient(); - wc.login("alice"); + + wc.withBasicApiToken(User.getById("alice", true)); wc.assertFails("eval", HttpURLConnection.HTTP_BAD_METHOD); assertEquals("3", eval(wc)); - wc.login("bob"); + + wc.withBasicApiToken(User.getById("bob", true)); try { eval(wc); fail("bob has only READ"); } catch (FailingHttpStatusCodeException e) { assertEquals(HttpURLConnection.HTTP_FORBIDDEN, e.getStatusCode()); } - wc.login("charlie"); + + wc.withBasicApiToken(User.getById("charlie", true)); try { eval(wc); fail("charlie has ADMINISTER but not RUN_SCRIPTS"); @@ -358,7 +365,7 @@ public class JenkinsTest { } } private String eval(WebClient wc) throws Exception { - WebRequest req = new WebRequest(wc.createCrumbedUrl("eval"), HttpMethod.POST); + WebRequest req = new WebRequest(new URL(wc.getContextPath() + "eval"), HttpMethod.POST); req.setEncodingType(null); req.setRequestBody("${1+2}"); return wc.getPage(req).getWebResponse().getContentAsString(); diff --git a/test/src/test/java/jenkins/security/ApiCrumbExclusionTest.java b/test/src/test/java/jenkins/security/ApiCrumbExclusionTest.java new file mode 100644 index 0000000000..eef3b21dfd --- /dev/null +++ b/test/src/test/java/jenkins/security/ApiCrumbExclusionTest.java @@ -0,0 +1,146 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * 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 jenkins.security; + +import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; +import com.gargoylesoftware.htmlunit.HttpMethod; +import com.gargoylesoftware.htmlunit.Page; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlFormUtil; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import hudson.model.UnprotectedRootAction; +import hudson.model.User; +import hudson.security.csrf.DefaultCrumbIssuer; +import hudson.util.HttpResponses; +import hudson.util.Scrambler; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.JenkinsRule.WebClient; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.HttpResponse; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.net.URL; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class ApiCrumbExclusionTest { + @Rule + public JenkinsRule j = new JenkinsRule(); + + private WebClient wc; + + @Test + @Issue("JENKINS-22474") + public void callUsingApiTokenDoesNotRequireCSRFToken() throws Exception { + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + j.jenkins.setCrumbIssuer(null); + User foo = User.get("foo"); + + wc = j.createWebClient(); + + // API Token + wc.withBasicApiToken(foo); + makeRequestAndVerify("foo"); + + // Basic auth using password + wc = j.createWebClient(); + wc.withBasicCredentials("foo"); + makeRequestAndVerify("foo"); + + wc = j.createWebClient(); + wc.login("foo"); + checkWeCanChangeMyDescription(200); + + wc = j.createWebClient(); + j.jenkins.setCrumbIssuer(new DefaultCrumbIssuer(false)); + + // even with crumbIssuer enabled, we are not required to send a CSRF token when using API token + wc.withBasicApiToken(foo); + makeRequestAndVerify("foo"); + + // Basic auth using password requires crumb + wc = j.createWebClient(); + wc.withBasicCredentials("foo"); + makeRequestAndFail(403); + + wc = j.createWebClient(); + wc.login("foo"); + checkWeCanChangeMyDescription(200); + } + + private void makeRequestAndVerify(String expected) throws IOException, SAXException { + WebRequest req = new WebRequest(new URL(j.getURL(), "test-post")); + req.setHttpMethod(HttpMethod.POST); + req.setEncodingType(null); + Page p = wc.getPage(req); + assertEquals(expected, p.getWebResponse().getContentAsString()); + } + + private void makeRequestAndFail(int expectedCode) throws IOException, SAXException { + try { + makeRequestAndVerify("-"); + fail(); + } catch (FailingHttpStatusCodeException e) { + assertEquals(expectedCode, e.getStatusCode()); + } + } + + private void checkWeCanChangeMyDescription(int expectedCode) throws IOException, SAXException { + HtmlPage page = wc.goTo("me/configure"); + HtmlForm form = page.getFormByName("config"); + form.getTextAreaByName("_.description").setText("random description: " + Math.random()); + + Page result = HtmlFormUtil.submit(form); + assertEquals(expectedCode, result.getWebResponse().getStatusCode()); + } + + @TestExtension + public static class WhoAmI implements UnprotectedRootAction { + @Override + public String getIconFileName() { + return null; + } + + @Override + public String getDisplayName() { + return null; + } + + @Override + public String getUrlName() { + return "test-post"; + } + + public HttpResponse doIndex() { + User u = User.current(); + return HttpResponses.text(u != null ? u.getId() : "anonymous"); + } + } +} diff --git a/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java b/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java index f5e746d161..8737d26974 100644 --- a/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java +++ b/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java @@ -121,7 +121,6 @@ public class ApiTokenPropertyTest { wc.getOptions().setThrowExceptionOnFailingStatusCode(true); WebRequest request = new WebRequest(new URL(j.getURL().toString() + foo.getUrl() + "/" + descriptor.getDescriptorUrl()+ "/changeToken"), HttpMethod.POST); - wc.addCrumb(request); HtmlPage res = wc.getPage(request); // TODO This nicer alternative requires https://github.com/jenkinsci/jenkins/pull/2268 or similar to work diff --git a/test/src/test/java/jenkins/security/BasicHeaderProcessorTest.java b/test/src/test/java/jenkins/security/BasicHeaderProcessorTest.java index 1c7c32d43f..0b471dd2b3 100644 --- a/test/src/test/java/jenkins/security/BasicHeaderProcessorTest.java +++ b/test/src/test/java/jenkins/security/BasicHeaderProcessorTest.java @@ -7,8 +7,6 @@ import hudson.ExtensionList; import hudson.model.UnprotectedRootAction; import hudson.model.User; import hudson.util.HttpResponses; -import hudson.util.Scrambler; -import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -20,6 +18,8 @@ import org.xml.sax.SAXException; import java.io.IOException; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import static org.junit.Assert.*; @@ -46,87 +46,68 @@ public class BasicHeaderProcessorTest { @Test public void testVariousWaysToCall() throws Exception { j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - User foo = User.get("foo"); - User bar = User.get("bar"); + User foo = User.getById("foo", true); + User.getById("bar", true); wc = j.createWebClient(); // call without authentication - makeRequestWithAuthAndVerify(null, "anonymous"); + makeRequestAndVerify("anonymous"); spySecurityListener.authenticatedCalls.assertNoNewEvents(); spySecurityListener.failedToAuthenticateCalls.assertNoNewEvents(); // call with API token - ApiTokenProperty t = foo.getProperty(ApiTokenProperty.class); - final String token = t.getApiToken(); - makeRequestWithAuthAndVerify("foo:"+token, "foo"); - //TODO verify why there are two events "authenticated" that are triggered - // the whole authentication process seems to be done twice + wc = j.createWebClient(); + wc.withBasicApiToken("foo"); + makeRequestAndVerify("foo"); spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(u -> u.getUsername().equals("foo")); // call with invalid API token - makeRequestAndFail("foo:abcd"+token); + wc = j.createWebClient(); + wc.withBasicCredentials("foo", "abcd" + foo.getProperty(ApiTokenProperty.class).getApiToken()); + makeRequestAndFail(); spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt("foo"); // call with password - makeRequestWithAuthAndVerify("foo:foo", "foo"); + wc = j.createWebClient(); + wc.withBasicCredentials("foo"); + makeRequestAndVerify("foo"); spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(u -> u.getUsername().equals("foo")); // call with incorrect password - makeRequestAndFail("foo:bar"); + wc = j.createWebClient(); + wc.withBasicCredentials("foo", "bar"); + makeRequestAndFail(); spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt("foo"); - + wc = j.createWebClient(); wc.login("bar"); spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(u -> u.getUsername().equals("bar")); spySecurityListener.loggedInCalls.assertLastEventIsAndThenRemoveIt("bar"); // if the session cookie is valid, then basic header won't be needed - makeRequestWithAuthAndVerify(null, "bar"); + makeRequestAndVerify("bar"); spySecurityListener.authenticatedCalls.assertNoNewEvents(); spySecurityListener.failedToAuthenticateCalls.assertNoNewEvents(); // if the session cookie is valid, and basic header is set anyway login should not fail either - makeRequestWithAuthAndVerify("bar:bar", "bar"); + wc.withBasicCredentials("bar"); + makeRequestAndVerify("bar"); spySecurityListener.authenticatedCalls.assertNoNewEvents(); spySecurityListener.failedToAuthenticateCalls.assertNoNewEvents(); // but if the password is incorrect, it should fail, instead of silently logging in as the user indicated by session - makeRequestAndFail("foo:bar"); + wc.withBasicCredentials("foo", "bar"); + makeRequestAndFail(); spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt("foo"); } - private void makeRequestAndFail(String userAndPass) throws IOException, SAXException { - makeRequestWithAuthCodeAndFail(encode("Basic", userAndPass)); - } - - private String encode(String prefix, String userAndPass) { - if (userAndPass==null) { - return null; - } - return prefix+" "+Scrambler.scramble(userAndPass); + private void makeRequestAndFail() throws IOException, SAXException { + makeRequestWithAuthCodeAndFail(null); } - private void makeRequestWithAuthCodeAndFail(String authCode) throws IOException, SAXException { - try { - makeRequestWithAuthCodeAndVerify(authCode, "-"); - fail(); - } catch (FailingHttpStatusCodeException e) { - assertEquals(401, e.getStatusCode()); - } - } - - private void makeRequestWithAuthAndVerify(String userAndPass, String username) throws IOException, SAXException { - makeRequestWithAuthCodeAndVerify(encode("Basic", userAndPass), username); - } - - private void makeRequestWithAuthCodeAndVerify(String authCode, String expected) throws IOException, SAXException { - WebRequest req = new WebRequest(new URL(j.getURL(),"test")); - req.setEncodingType(null); - if (authCode!=null) - req.setAdditionalHeader("Authorization", authCode); - Page p = wc.getPage(req); - assertEquals(expected, p.getWebResponse().getContentAsString().trim()); + private void makeRequestAndVerify(String expectedLogin) throws IOException, SAXException { + makeRequestWithAuthCodeAndVerify(null, expectedLogin); } @Test @@ -136,7 +117,7 @@ public class BasicHeaderProcessorTest { wc = j.createWebClient(); String[] basicCandidates = {"Basic", "BASIC", "basic", "bASIC"}; - + for (String prefix : basicCandidates) { // call with API token ApiTokenProperty t = foo.getProperty(ApiTokenProperty.class); @@ -144,7 +125,7 @@ public class BasicHeaderProcessorTest { String authCode1 = encode(prefix,"foo:"+token); makeRequestWithAuthCodeAndVerify(authCode1, "foo"); spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(u -> u.getUsername().equals("foo")); - + // call with invalid API token String authCode2 = encode(prefix,"foo:abcd"+token); makeRequestWithAuthCodeAndFail(authCode2); @@ -162,6 +143,31 @@ public class BasicHeaderProcessorTest { } } + private String encode(String prefix, String userAndPass) { + if (userAndPass==null) { + return null; + } + return prefix + " " + Base64.getEncoder().encodeToString(userAndPass.getBytes(StandardCharsets.UTF_8)); + } + + private void makeRequestWithAuthCodeAndVerify(String authCode, String expectedLogin) throws IOException, SAXException { + WebRequest req = new WebRequest(new URL(j.getURL(),"test")); + req.setEncodingType(null); + if (authCode!=null) + req.setAdditionalHeader("Authorization", authCode); + Page p = wc.getPage(req); + assertEquals(expectedLogin, p.getWebResponse().getContentAsString()); + } + + private void makeRequestWithAuthCodeAndFail(String authCode) throws IOException, SAXException { + try { + makeRequestWithAuthCodeAndVerify(authCode, "-"); + fail(); + } catch (FailingHttpStatusCodeException e) { + assertEquals(401, e.getStatusCode()); + } + } + @TestExtension public static class WhoAmI implements UnprotectedRootAction { @Override @@ -181,7 +187,7 @@ public class BasicHeaderProcessorTest { public HttpResponse doIndex() { User u = User.current(); - return HttpResponses.plainText(u!=null ? u.getId() : "anonymous"); + return HttpResponses.text(u!=null ? u.getId() : "anonymous"); } } diff --git a/test/src/test/java/jenkins/security/Security380Test.java b/test/src/test/java/jenkins/security/Security380Test.java index 5d3475df92..c7b3e90a6f 100644 --- a/test/src/test/java/jenkins/security/Security380Test.java +++ b/test/src/test/java/jenkins/security/Security380Test.java @@ -67,6 +67,7 @@ public class Security380Test { JenkinsRule.WebClient wc = j.createWebClient(); Page page = wc.goTo("listJobs", "text/plain"); + // return "0\r\n" Assert.assertEquals("expect 0 items", "0", page.getWebResponse().getContentAsString().trim()); } diff --git a/test/src/test/java/lib/form/PasswordTest.java b/test/src/test/java/lib/form/PasswordTest.java index 0e3ed7c783..8eef7d40ff 100644 --- a/test/src/test/java/lib/form/PasswordTest.java +++ b/test/src/test/java/lib/form/PasswordTest.java @@ -27,16 +27,9 @@ import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.html.HtmlInput; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.html.HtmlPasswordInput; -import hudson.Extension; import hudson.cli.CopyJobCommand; import hudson.cli.GetJobCommand; -import hudson.model.Describable; -import hudson.model.Descriptor; -import hudson.model.FreeStyleProject; -import hudson.model.Item; -import hudson.model.JobProperty; -import hudson.model.JobPropertyDescriptor; -import hudson.model.User; +import hudson.model.*; import hudson.util.FormValidation; import hudson.util.Secret; import java.io.ByteArrayOutputStream; @@ -51,8 +44,10 @@ import org.acegisecurity.Authentication; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertThat; -import org.jvnet.hudson.test.HudsonTestCase; +import static org.junit.Assert.*; + +import org.junit.Rule; +import org.junit.Test; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.MockAuthorizationStrategy; @@ -62,25 +57,43 @@ import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; -public class PasswordTest extends HudsonTestCase implements Describable { - public Secret secret; +public class PasswordTest { - public void test1() throws Exception { - secret = Secret.fromString("secret"); - HtmlPage p = createWebClient().goTo("self/test1"); + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + public void secretNotPlainText() throws Exception { + SecretNotPlainText.secret = Secret.fromString("secret"); + HtmlPage p = j.createWebClient().goTo("secretNotPlainText"); String value = ((HtmlInput)p.getElementById("password")).getValueAttribute(); assertFalse("password shouldn't be plain text",value.equals("secret")); assertEquals("secret",Secret.fromString(value).getPlainText()); } - public DescriptorImpl getDescriptor() { - return jenkins.getDescriptorByType(DescriptorImpl.class); + @TestExtension("secretNotPlainText") + public static class SecretNotPlainText implements RootAction { + + public static Secret secret; + + @Override + public String getIconFileName() { + return null; + } + + @Override + public String getDisplayName() { + return null; + } + + @Override + public String getUrlName() { + return "secretNotPlainText"; + } } - @Extension - public static final class DescriptorImpl extends Descriptor {} - @Issue({"SECURITY-266", "SECURITY-304"}) + @Test public void testExposedCiphertext() throws Exception { boolean saveEnabled = Item.EXTENDED_READ.getEnabled(); Item.EXTENDED_READ.setEnabled(true); @@ -93,24 +106,28 @@ public class PasswordTest extends HudsonTestCase implements Describable - + From 2476d1417ecfbfb9864b86d544056421e67b0679 Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Sat, 16 Dec 2017 13:31:28 +0100 Subject: [PATCH 74/85] [JENKINS-48365] Restrict InstallUtil#NEW_INSTALL_VERSION --- core/src/main/java/jenkins/install/InstallUtil.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/jenkins/install/InstallUtil.java b/core/src/main/java/jenkins/install/InstallUtil.java index d89cac2df5..5b3f95555b 100644 --- a/core/src/main/java/jenkins/install/InstallUtil.java +++ b/core/src/main/java/jenkins/install/InstallUtil.java @@ -70,6 +70,7 @@ public class InstallUtil { private static final Logger LOGGER = Logger.getLogger(InstallUtil.class.getName()); // tests need this to be 1.0 + @Restricted(NoExternalUse.class) public static final VersionNumber NEW_INSTALL_VERSION = new VersionNumber("1.0"); private static final VersionNumber FORCE_NEW_INSTALL_VERSION = new VersionNumber("0.0"); From 26262f873275c99005b1ddc59c19c21edbe32bf1 Mon Sep 17 00:00:00 2001 From: Hubert Woszczyk Date: Sun, 17 Dec 2017 14:28:18 +0100 Subject: [PATCH 75/85] =?UTF-8?q?French=20localization:=20Changed=20=C3=A9?= =?UTF-8?q?=20to=20\u00E9=20(#3159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/main/resources/lib/form/advanced_fr.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/lib/form/advanced_fr.properties b/core/src/main/resources/lib/form/advanced_fr.properties index 7f82aecfd4..c998a30a84 100644 --- a/core/src/main/resources/lib/form/advanced_fr.properties +++ b/core/src/main/resources/lib/form/advanced_fr.properties @@ -20,5 +20,5 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -Advanced=Avancé +Advanced=Avanc\u00E9 customizedFields=Un ou plusieurs champs de ce bloc ont \u00E9t\u00E9 \u00E9dit\u00E9s. From eac29da4f1fbf98d85747f88f3f8ee48f369034c Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Sun, 17 Dec 2017 16:39:29 -0800 Subject: [PATCH 76/85] [maven-release-plugin] prepare release jenkins-2.96 --- cli/pom.xml | 2 +- core/pom.xml | 2 +- pom.xml | 4 ++-- test/pom.xml | 2 +- war/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 1cb344f3bf..8f76554442 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 2.96-SNAPSHOT + 2.96 cli diff --git a/core/pom.xml b/core/pom.xml index 1c3e84253f..49f2207f71 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.96-SNAPSHOT + 2.96 jenkins-core diff --git a/pom.xml b/pom.xml index 61644dbd00..dc96249e56 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.96-SNAPSHOT + 2.96 pom Jenkins main module @@ -58,7 +58,7 @@ THE SOFTWARE. scm:git:git://github.com/jenkinsci/jenkins.git scm:git:ssh://git@github.com/jenkinsci/jenkins.git https://github.com/jenkinsci/jenkins - HEAD + jenkins-2.96 diff --git a/test/pom.xml b/test/pom.xml index 8745d54534..6b1ddbc3ca 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.96-SNAPSHOT + 2.96 test diff --git a/war/pom.xml b/war/pom.xml index 13086ab727..afa71b7fe4 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.96-SNAPSHOT + 2.96 jenkins-war From c32b6d807a5647df8b5347c8acc1b42024b597bf Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Sun, 17 Dec 2017 16:39:29 -0800 Subject: [PATCH 77/85] [maven-release-plugin] prepare for next development iteration --- cli/pom.xml | 2 +- core/pom.xml | 2 +- pom.xml | 4 ++-- test/pom.xml | 2 +- war/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 8f76554442..6042680574 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 2.96 + 2.97-SNAPSHOT cli diff --git a/core/pom.xml b/core/pom.xml index 49f2207f71..4517bab996 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.96 + 2.97-SNAPSHOT jenkins-core diff --git a/pom.xml b/pom.xml index dc96249e56..4ce5365476 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.96 + 2.97-SNAPSHOT pom Jenkins main module @@ -58,7 +58,7 @@ THE SOFTWARE. scm:git:git://github.com/jenkinsci/jenkins.git scm:git:ssh://git@github.com/jenkinsci/jenkins.git https://github.com/jenkinsci/jenkins - jenkins-2.96 + HEAD diff --git a/test/pom.xml b/test/pom.xml index 6b1ddbc3ca..da6245605c 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.96 + 2.97-SNAPSHOT test diff --git a/war/pom.xml b/war/pom.xml index afa71b7fe4..9147f0ecc3 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.96 + 2.97-SNAPSHOT jenkins-war From 5098524513883a48d07fd32d5a6f058d68adb8b8 Mon Sep 17 00:00:00 2001 From: Devin Nusbaum Date: Mon, 18 Dec 2017 10:59:47 -0500 Subject: [PATCH 78/85] Add failing test that reproduces JENKINS-48604 --- .../install/LoadDetachedPluginsTest.java | 22 ++++++++++++++++++ .../upgradeFromJenkins2WithDependency.zip | Bin 0 -> 150149 bytes 2 files changed, 22 insertions(+) create mode 100644 test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithDependency.zip diff --git a/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java index eecc1e2b49..fffb220d27 100644 --- a/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java +++ b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java @@ -26,10 +26,12 @@ package jenkins.install; import hudson.ClassicPluginStrategy; import hudson.ClassicPluginStrategy.DetachedPlugin; +import hudson.Plugin; import hudson.PluginManager; import hudson.PluginManagerUtil; import hudson.PluginWrapper; import hudson.util.VersionNumber; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -44,6 +46,7 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -81,6 +84,25 @@ public class LoadDetachedPluginsTest { }); } + @Issue("JENKINS-48604") + @Test + @LocalData + public void upgradeFromJenkins2WithDependency() { + VersionNumber since = new VersionNumber("2.0"); + rr.then((JenkinsRule r) -> { + List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since); + assertThat("Plugins have been detached since the pre-upgrade version", + detachedPlugins.size(), greaterThan(1)); + assertThat("Plugins detached between the pre-upgrade version and the current version should be installed", + getInstalledDetachedPlugins(r, detachedPlugins).size(), equalTo(detachedPlugins.size())); + Plugin scriptSecurity = r.jenkins.getPlugin("script-security"); + assertThat("Script-security should be installed", scriptSecurity, notNullValue()); + assertThat("Dependencies of detached plugins should not be downgraded", + scriptSecurity.getWrapper().getVersionNumber(), greaterThan(new VersionNumber("1.18.1"))); + assertNoFailedPlugins(r); + }); + } + @Test public void newInstallation() { rr.then(r -> { diff --git a/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithDependency.zip b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithDependency.zip new file mode 100644 index 0000000000000000000000000000000000000000..557841273ad6240326625333b6bda5b61586225b GIT binary patch literal 150149 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'_RD7)2@On9eRhfG56 zbO!c~M}B_G40jD#R(h@&uy4%p6qoppISgtL*ridFX#WmTMxoCYYm^v$hq>GX9OfX> z;1pABxtqy7VmVsK+%q8T32ilmXcqVW1?BV^8~EgF<-$F_YbDknLk7n^yOm9R;~3cr z+bgvRm?RxaDI-li(a&g>0&=D^7?dJym^@v-WDMuIzC_Smr4`R+Df@L5{B?)=Jpq2* zJc~lUm`*?5$G`d`3+v|ko;T~V0M+y~5EI!?0Jh$RZNPu^q5>Kl<5A0X#1qluKjxXc zJWx=!kiQcBRw9UT^L}VI&OBK`9J!b=TTUnA-aOaK^X(zT5ei|HuuW@&4clTbz^_c3 z#Z6f@jOKAAlMLzZSTyvCIX2{-T%_$KL(#}Ys)0BRd0WwkbR4HS(X%TqJ@YTUL1~4( z@@@AYn2P$S8u%_o+GJK&drzrT7oJ9^?@8sW`lVFg$&rt2npfgYW_Z(J6ix%d@5U15 zA|C>ITZ)u8z#NBRak9HRXNxX=6zYyiS{kG)$`923ReMNMGoG{SAE`fFW zwk*i7lSVG!IAR?bWWYu>nnhG)`O(Mw_`3fs7oYLbw(a%jpZ&b0U-U<4{5{1q^yW54 zCs#K{_a=8G7Y_gXr`mWFgVI1iWq)WGS!>yqMiyj%<5nzGgOP*~2tt2#{Gvnc}_vX*%dKbVm)(8J_Rh!eokJ{_OSG#0&E|#}hYU$04-o4sdBm zyt(?%>O9yOe_?RI>~JG^at{ZzB{?170y+>X1MuD;jAgZQD1uSncO;vMX|-|r(9Ea( z0F<Fq>%%z z+JoEuOTaz zae^iy>~wqmomI$bl6(i;;PXSi()HCbG{wER&krxGLX1kwK`M(PjnQaRC|v=vpPV_X z=^@|#CS;6-%#oJ{dIW2uedaBfvh>hZF^k0K4@>jATmJ8*&0w8+?e4yO>F>eE=r=!Y ztG`@XAgWb-5xZa129os4cQLF`3850Wt;F-Ui8w>MiEOjbfXyk=kJtDP{{b>asRLUi z1GMN@UnD=A^1r(f(KpGu$4PA!I$Z%CQ z@wi60Sfgl>pt-ph-2Mi#{5;z$7mkD8w=p>R%GCRyODcZ*!XT=yxYdmw)ZKY~yFwTn zb}Q6=|H_QhO&dg5nnCJ}Lz$tDu%(p?Jl}(8lWveoRUg>vz+5EN9OiOEipn+=72}nw zYY#xzEio35sPd~Ks)HW}+JM8@q6n+j`%YtF*bzFFdt60J7vmwi^_XGS;)HFn+5ruU z-=;O!sOSjDlg7wBvTz6Eb5hi#%PIc8T6BxZIDsthjKf&v^(NwS(2$X{?w4J&!})G> zD(RbdsNNilU-5wB@#H$}n5Ku*VELyy`rfKN`7S3->S3!uTOx~&<+B&;Do@U~(H2>G zB&LEY8OpflgiIyLl;875rF9*PE^}^cP+pvAMzz>eqgW9m3Yr{cu-cnl<+=_-7fkUY zUX%pR;Hx!L#Ad4k&t6igG?kVmqAzmNIP2SrC)@0Doq^2{#~+|YQ;1#^eO?pDg-gFL zksOg^$upFu)?U=abVq#?i;rHG0?%`o+z8YBsAe7)08OGr17IBeM4$c$I z_Yx|h^Yk<*AG1>o-R<-jQd25-j`ZXKx`c7!2&0sDlXoT%XHTft<*jGKFZ=TZePBV{ z*Ei%E{7AV)J(Jn@4hGN7H>uqP3zf(GMv=Wqk{*4*kt0zmvesN znf=%C^ZGag>j^6tOf151&O!xQ3>38%zUUe0FqTgq((E@HtgMvrc%9a5={Dz^<(#JU zz_MKjV@uHnzI4QKz?oj9DpZ75)?b$`#2c^ba^U0i%W3mXrN-;EBvrm{#AdY86Lv#M z)cIt0wp;t+f@^X-Hr!X2&ePzAS{JRMHk#Y73yp52XTj$TC1)qokev%Q8J-Ohx?idI z$!{A(HE#s7Bl)JRAL0*_7%XK7HD31*ayv2F<8}6~LSXl{)jU=Whz&1I#QJUlsB*i# zCLW(EYH?X;we*JwVHIOeR719-r;Ddf{y{Pl2mlvTNMDqYicJRvyjPS&WM zewTpE?Q)bYeCN#D-$ne4j~=TE^mO}w3=H{-zV$EHJMJ@eI!Mx$kzrorJ?c{5mk35_ zmr7D2NsF%pI@b0?Rt(J`yOTL7JJLm-=puqc0c8ZjLloGdTnZ+H+Y2_BtkEBseBWd= zNBBcY5-1C=w2<}u9T%K+6UIHCulD3?<5sn11nDYCINhBLSJYLoDCl0FYF=f)O;7a8WBG zfOe5dxTJE!k|1Fm*3PD{`su~~>wEcCDM<9Kw4b(=zv09{HvVxC5Yp`nLmDlDs3(ZK z$%7zag;Hx!b7-fMLWJ*in~C}}S!pHq6}-xR0}~>x0ynG=@qGF$$bbWdO!M@+%k>?z zvehj5o#2>$?biYSkV0xY zV>vD7wbB)9CNd(OZ8bO0l2dgKjaL=b3j1#BV`pO`|L$})n;Ys-g*6tr90tqg*d?gY zX*8hs&qEiY1x2X(l@f@49HK0wTbuZ&e&Q1Acc%*+sg*U&=hhYx98*gdJ)4SFqGnt2 zt%zzrsxO1S1MehXz>!pt8Dl-ZdPHm5tIvdXhF~p7*6Ge#3Ik2_ePt@gWOPF~Z=hU{ zp?16uT(4
    d`!c%x?@8w`mGt83}fw!pogJ)XL>lnX*xRwt5b*o0T=Pl|i8e&R5I zGD`iF_OJ3$)8*9BZO1u!C|SAEUGw!?y!g>zHq}V8J=ZV_C67q#Q9E!r#h=kkcAcGh z4W4DGOj<|<$Z4-8y`L2r<7#f_053EbqsSh*{jFin?!`IZ?o1vc&Q{j z_m&(aO4Uv3(Wb)p64z9)<@6*7Y8c=yT#`CClQw3tdyvFqhHs%5{8PuS90kufq3MJA z-xEn>J_Gs*?}Xpa_OJ>JkUZU)fZtk=Iztda2x8}UvBWq(PK(}z=H8!si9fE0F+E52 zMh-H0!F@UsdId+B%VXX_J0cOGfn~=o;S)~T72?+Od>ZZZxCdMB<9L?#q=_%i(N1P> z2nU%`l49S?$PeOQ(Rj|8og-?1a48UGN+vS7FW#|!@7qrIO@GM&ws{CTsQYQj{urta zKll4d6|$5CR$*lU(?5keVckj=>X_R3HR2ED8WGFwH!qZq*5?r7E^I1%+?zSsK{{;4&{X7o$TXKw@YN z`58Fr>r|x2q-SWVIHI|Q?ly%(Of_1Ex?WY<%bKX`a&3@D+o-9vTJ@PUFHxRb?LAQ1 z)hd+gXh$pT7)jmb*bKSRYYd{!-5R$VTW&MEIT8fHnv70462A~Cc8%UvbF4UXRxacz zPQ2Q!`wCXC!xAabrar|wQR9EZhDy0Qhw36zll1ns&SNysYBUKi)Q7-=r`Z*oPqxJ- zQ~WR>@0z7?vQsFp+Fl+irV+~0kU1>>)6=GKPkzkJqL3GfRhCim$v^|1|A$>+nsjR| zTY!R;SMpA1G~n!Ep}+GLwsyUqLQ{=by()Hep9-+()dLbs+QY7VnS`98 zS4I)N7u7MD^Bo-crL%Km7Y*$*pk}J9A)^emgCfl2;He2jKshD~6D_jtdfDj-!DvI^ z9=8KAm1Bk|+t8`?j>tK>q@Rt^pwf;`%vYk|fw=oBNm$T0;Izp{3|s;ET4|-QpqRm5 zmZb;K1ByDg`h+4^MXm0(zva{mM+^Xh*`v+}mi480dqy_w~G& z&L#i=aSbn?7x65b!!Xw!hPd|*2V}%4Qhrvo$lyISh2YHyPRS1_bA}==owr2$>X=Ra zgYb49!e4)=Xweyt+h-fK<2R}9DM~q&$KLP1_$=fB&Xr&H@rv9Y(5v>DGmr5Jo8et{ zgc|B?`_R}G7|b)9!gx^kNZC-+KSIbC70{-8vgBfhCJSCGmjsobpjApO9JSe}W@8QrupzuAwx7-nZl?{7=ms%DNI}ycC)^R}kzi?&MH~{G zsgA_w74HE-+Cu)D4wzgPW;($Frp7~R|FGe>BN(m)aGPBYfPhw0Dm!^Ip41s|R3_@9 z$fl5Id1@xEa7)G1Hl>hCNPN&|&(Mi+>rgx_mlM?QS48wR1C&POzeE!oz(73(x4%Qw zq$Y`8Vx6Jm88>H_=_g!-HLr17?#~vU*a^){2sFFdas#IS*VYT$#yBeZV<)!rHlqs2 zwdS-Ov`jQeal%y$HHO5T4$$-I*SYMAfBUPLw8zO>HZwE2_du~6`Iry}RkI`3%tueP zZ4kjZtG&8;TI%^!HLRKZX#5E_69oaS3@cYW#U*XA_!`u_bc%>CFOq&((Seo?P;Jll z#)i>_L7ZGpX^ZzQR9rB$$=Or%aOO+^H$ce0-;~`msb?crLbtH@=qm;@E-Qso9ngxrGFFXEPVefN^dB&hz?f%`EYS zyPex^GcpTkKQ#7z@*QNEc2JHj*ej8>#!Jm6>DIEhpf>5Gi6d7Pu}RV$@uEa7;7Yk; zxn@&bee(R1V9lNUX94ou9I@36#X4;=at;_U%aV2y$dpWJ3uYQ34%k~BlO&zTp)4py zZ@rk_h@xEzir4;%lTkC!Vpt|oB`f?!4i2YvaT+2TT0{Ax99xF7zNO*D6k0VbowQp( zsrT)dw5kB~{X|=5k1MIKKqN+d2H=)ucHr#c+x14W_T!ue*$6c5w_~C>5lV!f!)bTJ|Z5kC}=~ukosQG^%`*rf`D^EhcurkrZtb z7fqnmV`U#xAv#3`yA%!~)rZoE^m^!;N`8V+w7?{+Zc9?RPtuCpE57XzJ+4Kr;?rC4 zYxH-wh4G5(cN6_G90eXYKl2Tvl3G5U+IIg`ofgftsp+3e+LrR6p3}J)vMlh6pLf?g zRhQ&_RFWro=t}jXD#!1fw@-MTk$$Hu&*ec~BcM<_D?TM{T9md;Qprx`qAo8>_04WxNxMm*?Q6l2McOF6+<|+z+h!5) zd)j6O_x+@gFxYF7*QfmI18nFDl}kO9OC30_vF)*$_;erm%-G~Ehjf0dZV!|j4Ec(9 zSgrib)K`OVyuFCtfrgI|a&H1hlKc>0Hx1?Qiy?g8|glSCWW zJ0l}zsi~o;$x2xXse(x@E3dhOp=QFk<`O>l+w#@?z$`D-JTRRbXOU zEEjD!r*m$(cc(Y-`}@AZZ^M5bn2D`?58L@GwNiTDn08=}fh^gTASx!TChSAhZPeA~ zGr^QaZ<$E3(znuKjK!S1{Vp$NP*e*q7k-&#qq{EtcQ36^ohL&dYf; z6Ui_mt*TYpJ2!r(oN_G6xe!;Uk1EO%E0}G+X&uK`ob-cZE&k_Z!!)!k6`SqdVA<<@ zt{WW~EaTLg2t`=h7?G790=sMr3-h_VC~GH}LLFJ`Wl2&`2w;=;xvkydF~mm!?k%K^ zYL$xg*z%JRcN%4j%Y`_cCY^~lf+`*|A7uz3;Y7mPj1Ts+@ke2dTL#qLY71rUj~G=T z@OjGK#l3R#S1^dIRnd8E&!L zrl=AsmGqrtC{2_b{oW2T&8tev#EyMHS!9i@4D3ztm3z(8KOSoQ8Vi4plB*;a+Ww!f zaQ^1>gZ|&s4|SPo9Slz%R^>XS36RYX6Xcvz-p-#0LST}6z0u!ktSf!-_%K`tbU!B1 zvfa?8Lw*7$5$=WAs{J_y_gvvJgdwaHTe>-G9sh>+{ra$8CIe(ykih44nLm;+SIA)l z0loq>G7{`t%uXVCxtwUZC?&d4M70|8VRbzWiMYDUx-w?_<~;6FbDQpA(x0;ri2NbU z014pe7qwAn^h4KNKA`F#{bgSO4OO92INx}U24t=Ak_IYP)1j^WG_lA`O_P38DaRMn zE}KPKCYAbPG$WDfY*NzAw~LV7u{=;uAIaUis?6?; zJc@;U->(7Bsw}6Rl1HvE7-K?uobkGRP8R*rXYa|W<^^)+51hYPTBI>~XyG!!=f#7M-h*TRGC5-`Lb$h*Xkn!cA~Ijb8zAfT{r z7^ee#0kjyRxr_@puaAv*_{EG6=rB5X(hzRV1uh}A6qMZ(JFqjDXFKl)llQB{O>m4J zQyJI-zoEKyis+#2W{2;g7=ozy6^mY{q`u*mMcZLCld^pA@-|=~Pk?^49;7M4)DG zj;-5z!S+_oEPtpsK&y5zhN?Nh((0fq#??6Nd)_vE0dfCEmhoYFzt|{HQ|=2^*?ujS zMlsA6i&K;IlbNEEvX9417Ny3v*`2*J{N4mK{|FD{Ezv*HR4m=0G*I|eitf7XSNsWC zO}$~P{!3rG*6#5cVY?>Q=8oUYvMQcCWLFlkcDtfbHbYu{2Oc^@+vcb&1L7h$!#mS%)RQH6;%RO|lxg9& zvlPo-F7KZX&Mn}m)zx$HIG3#KV{nhz)Y}Zri1nmM#|F~4ObulvSJbzfc?5{!SF=!d z!H0~CD7&H#af;r9z{MgX_ny%>EHCg*^%h#|C&b8j)EOCqLP}K@*0;1{1_8I!IW>wg zx=g>z+_=-IP<^?~qn*aPgunUfCkM0Bk{`D1&)|f`W6ZH>pF5ZzsfC3x;T~Nh+EmlS zut2MW5B3x_Gl)Je}SY1;oc;FHja7F_ZlK45=OkuuMZa15)61bD1K7-xe~B z#Zvpg@G-FjhY|^zOcKk}zM9qOob7Ogi)F*^xI#j|2;KrQe57MJd-X?f{2Bs(4vwUU z(AF9d0Ko3=RIj1`cl8=68B{(*k5=vKh~m)KWS?y}I*_ZvT@3`XC^2F{;1OZ_R~N?2 zyC+)r_X$qgr$UL6Z^0xbAB6f2KtOdH%~NxVn^s#~m1bAke|SDYty2T}XXx;C2n3~OKiZwP$?Ser^M3+N@@9- zA)qf}Zpu+1+AyZ;YGB?kBbg7FA)cDR*WTtOXPZ{Q z7v0@gMOl+=q_p)yNnl`1q<+9jN87WjiZu4`DSIcQ|sDcfq z=CwO%-1Xe$T)W|2a~!)}&W}8TsQAP))SYq0OHtO5@y(k32jQr8?p+F#SHccH1?yAX zj+YYC#(SP02(P}`yv`7}Qza-6G}y2*P#Va~^0WO)tU@5vHor|QgB8CZ*LGRsPVt%F zKO_+ojZ!fyN>4F9dNEjSDN(KIT`W*%n_yaEJ(TG8djr*lw(q$cBb|+eiK2>CdmNSedNmUoWC; z61yrMU9NeXN;#ZL8m^M51f5nsm9RHV!6Q?Qrj|VcaZyJ8wOS!C?SaA}rYtKk6}nWZ zU|4#ELJu{{;zPb{B$qGr)exgWf0`roZY32`^a($wkRex|tVSj{K|41hOi#C*lNFKZ z#Byd7p2h3$di31FWN?0)t~2E#o5NSmP7T$a=mvypLS)zvQ;6`ro0EPSE{NigfhvOC zNZeR!qGUKY#gW1uTbtZy5zhd>nX&14iMfTwv}0Wga|$PoNGmOJsl`{}$X-xg zBoZf^x2QoG(iO@h7Nql7&OjZM*ZZTvtMJ^5JBP0?g0jhvZXlLCCO@_0Lt31l0BE(% zO1_|+L=d}P^5uGDjzw5&m-f5Y33+7BFIv~AL|zoJv{$YqD29<2^5TSubceaIb-Fdi zY!_pvNY|WB3DgeTvryxwK&eY=oMH!Zp~z1HU7@P`bD7~7{dp_X^(82CDxIxTkGs&n zxLrjhBA1TVRZ4)l(v7mQel%vsyP>amOXjIC&UA_LDj?MJh>)|hxKVF^98uz}g4EW@fBq2FDv8bC_8f$MB4 z_{Ydz^w*TM++DJB2I!g8NKgluFiX83MjH>3b;E`r?**Klkaysw(?&-#f`k`p%oY+| z!G0{dZcP@I&$`UeiWY4ce;!86a2ezA;>X$%mcJAqY43~+_h`n6RY%uA4sLOy8#u z!$5N)%_wR58q_uy2SgY8DV=>*7Vu{=y|3*J_7bf%hBloYv}vT@w?o1iw*N1UV~ zbc1{8AWcK?_ks-q^DleyizC;ilZKaQV?tP((kjOG#G1WV3(J&q==Y;1#}k#~6>;ceninUi1k4zX55Q<&Y+9=^jhiDjlRH~Cu< zk0gEOG_%Ln?A+dn;ugHBTlP9RhPQ4cetj@qP|U1}dF zyq3eso{7+lojniy{LX%DYH#ThH~a*7wwN?FmZ>S_gdjCasfpl1SJf+_O+@u}LB2xx zCWL*Nbd=?Ln=tQZeACN!8>-B~#!{-urF&Q3ko@5m*1AdkkLZ2P8pM>wt7TAUPQUE+ zUjlR|zwUfY9mq@CjgpRx)(8~IwF$QpB;l9}z)nW+O^5K3b$p}<$0Kx{!L=_I=G-SG zhb2LZ^qcL*y4w1Td77Vr&@|8 z&5&ue(fd7oS8+%ajdnO`HUdHQCHgrcLQ~tATqSUxEL;N__oK!$jzb;3$?@t(xXkTt zS%tK+8{_n{Yu$FYGXKzFn_S2|ti+1V>BA46b+%`j*U^c^ut-+Z$o-gxw;6@_?UgBE zry%-`DT`k=x0&w-TwhCo=m?R@o@V%J_6<}&&c5)*YogSe+!7usx`(6y?DOzAFPRP? z^D2E!YtWiC||Xhs+t{>h-lu*mBL za2Ed=75jTVoR1;c2Px{wD{xm*CrVcqC%}*VhI2!XxAruJJBH=Hj0M{rB^O$WHh$ao ziX*rlo3*bXmC};Wx#7GuMCsZ;Zt-`i5nL5g8??mb?g!rCE9_!_f!`@I58F!U=(f}m zoU^UO_6<&vkm>Hc+|&8caj}aLc4yATBf-csfR}HZ*~uQsjy;=%69Grkhn?WHWrx+& zg9HyT(#d!f=c>_*1dmh*6J)}5T0)Od1MOw91IvuuH*@ulHpxa{5SuvV2xfrZM7-B{ z*-$&~eMu!Np-TQN+DUM_uTU$wfElx&ue^5hZlw>#F!IdSMwH2Yb-$W(jTG8<4o*gj zhyyBgfp=r&gb~$QCRE;GRz4{%p&ybA5rkLfF|pZ3fF+tf`*F}Wi6m4r-G`rxs3gMM z?3KVO>rUQ}w0s*w=#lKEUp1#^G3^dtwxy2QqqCtdwr=k4kkhB1T1YVTtkR2Nt&DBj zx~tb-33rh*1kZ)@c6Q?6nUNynfnCb^dDkN%@7Ia%^=7n!4|Vn6Zc4h5=M}bB3cGGP zKdlu$xiC2D96HfioY9fPLYdr^KEgU~ICqd#84Czu`L^pl6%rjlgs)BlI@42YHGVLy zAB|r9-NYvR8;Y*woARNzhKO8fTfWR)3#0?nuSqKH=?at2j@w5li!mGo-gW0ozy2zx zY}CPMZm%DfuvxMl>&rIWB&WBsN4JfqKVTvFjh;%qPa;5gC9`3bm^rd)6NK-}e#V2E3CVN` zJa3D!b@;`)SfYJP7k6|81CkiY7E;24y5lTcc;C_5~sknF*(9yK$tXhEFupyaZ( z2r>6`N>BKt>~!6yXLq;8Tt7iI{(3aCnVw|$s&{0>1E)VXI8JbyQId5VJg@l;GmZA- zj;6v|lX>pL)Cj@D*P~VXYwuKa@=a@!1YQuYx2qo?9L!wp-^#A0HbGZ-(tU|G@byJY(j#C(SZ0EZO!HllE^Tdo14f1SZSw%wFO zVL909Joe1Oy^*gL|8X(TpF)jH%qdDd&S)-o_(;PO z`|%rMU5fQC-l%C@rL@rOW8&h-C{u#Pt2{=cv6Vz!-DIqM3nI4ZSDoxb@#^vX4=PJk zw6FJf{q(K@5f_V)vL{lTdqfe@gUxi)hQ*FMm-=%osPOBm<b;tl$k`eEBO)*ml(Okwc}bX`V}g zL!OGqB3H`p1a{{5MVdtl$Nep(6H^?TWMItdawj!Qeh$>StkM--h6xCyC*3$I>0~iI z?&+z!L--w_p6r9U^zE@}jbN&KrS=TX4@A=Y{e{x;{pjo(>~6BVweUKXPFl$ME-fti zbCJ_GnPbzDjE-B9Fk@&?2bvILpemWw)~8*Bhh3ZGy$T^GosvcyahrE#$oU+fh_#Hw zkl}X1C__z_HV_OZZObhX#0|W~PXR@(oYug%Uln7GCG|{@hq=0b91$%)h6!|PEkU?h zM^N=BjBSuhY4u+aCF4?ym7KA`l#KCXEBqXxP7j5!jkS2zRN)Yl7Z=2)SDHA`1XddZ z@XAHWBo}cgUrn5($&WCcbTx{lCis1cb6whaG|3w=O&lnOv~K(IA+9GuGTLctSb2aX zsE{o+Vy1{xly-YM*JMV8@gP@8tORL9A~knER|~Ov#O6)NHTrAmhe`0R1i`>zxnuhv z2fRfxPfNy!2X3mb*EHAX&C%mL9KNmgNN#(WN7>;=vD8PA=oKQ4m8{oQ_`>rjv|V6a zH+<=pY3)bvc|Co&mwEh&euQbp`K~B_5-C6hV?v9RXTO%EzMdjH$|rUX18&-wcB15L zu{R-&Prqq-zjRK>-8XJ@#(>*F`3767cBI1$xOLu?`^HLjXG=-~X0_^a!(v#-x!;y} zwRSo1hDv7BW4QTt;DX-Mp@PbvZEzFAEv4#-=&<8XJ{Q^NQ!Cc2})enlIqpQyF=}#aF*&k6mFreS1fv^ zyZeJ6wO}0%!U|82#*5qU8){4L`Y~9dL!zBwu#Zz%#pB|-dF=9Dd%WeE(iz^9NXa5E z{XHFas=hqRT|9v&BHO3$0;*AO>^5{q_{LsK3tM_-IMiYFzQ^Sy{zewB)YE)Fp))kj zEI)k;>ziyE1Di)f9N*+=+2RqE^+PD+DzK2Fdz8dzn^a^%XmzOQfau}f!qcFRSC|;f z0FM_PsPOum%toBJ z$#o6VR=^yde9~KjOi>EUj~aEQXs%1TFsgLvxN!Tb`lBQ7OoU~!{qxL)lVt4!+BB7y z@AL(NKu@e{f_(ZnB+uSqnXUHxb2EV>h+FXX4I_Bd`A5qgv{W2fNe=pS@&WcB{yxtn zpC_)@p*~N^jwI(*Pu!|et;ziQ-vF*HLwMk=$|6p@uI#~V{C$l5NY~DGxJctP8i|7T zqcU5KCK0Z3wy;*wj0wKXj2i~$KW=^bzic_Ymis~W#S`- zM>5lHhh!U7x;uO+g~NrY^pT8g+J|fb4nSYkkL=))@i2VFxCD;v66}{b!|edGd(MZ| z^>44TAKxX&`&5O4O>JwaffVchP)~HF5y4LjZh87g0`u2|)SpvTdCNEs!QcRZ6a4?x z_wt_!Ol4hj2SZ^KbNhd1s%n)r6_$iixhb(>Bluw(WSeWCMn%G58>B=R*WKmX4(6hm z^5R&;6c!4EJ4M5&P%h?1E)y z%m{pi^nlnY5O89aXV}Dw!CzRZ6mVA3YDt$mlme{OSfaoh3(+c>7Nu1n<(93s#Kd6OU+0u=P}EebRpZ1Mx@n_SJ`)B}y@@3a%seHHUSSpw zXw+weMVG%5`5uv8xTnl+_w#N-*0@mVs{qR#QG)r}b!|=1(JX>k6jPe*2Wg~RANiL$ z*x}qhhkVIUtZ8<$m3hNG25fyNeW3S(fw&nlbGrBn%mgl-2!P~qXMjO3d#JNRh!mzw zYf)pWp4&@lNsVC{&do!)T#@iLnjogy@kpvnbo2voV?_DVboOmWe&N%aU#ifBL|>g5 z=Pp<35g%q-lFwnHGCTH+LDWOg0;<#YRZrB0de++9%9R0~;I5O4zlr+nYx@lZ_MK$n z6NK(@s}t|DZ*Ww5yq|=cP1T3{wo;WG9Et^-O>q-Oq=lkC33az_%`XLP@vy%Eu^1OW z(>|{INUGA#3V13m8 zU`zT6a*uPwOHXGRGeqDrE;D*^K6kb=T`o%}QLjGnsJj_;whxpjDks_ozjf0@t@LbG z%@twj^uFko(3PydzshL!&AInm?iB**D!4IB1fv>()JYaLd{;Ij&K|MaPWIi6)C4va z(Oy%<%-ap5GdXX>9#-K%j*P! zUjw=}c^mK~_vCv{Z2K7h=-w5d4qeW|y`0^8O5q90iUC@<& z54$Z7ky?L9!Sl)*d?Izt@mmEzv3ejQ%k%S#i$?jCL`6j*BZDCCA)gU(5gD8uRmk|T z5P%DUfsa6esAUU6V*o(|azE+vVP8x#cpf{~n9YFp2Z`+-vLSUOd^_(Y(w4mF&Do`2 zMuxk;+Y8_{&)@$%n+aJ#gRh%!HJFy3+h`5*w4C{v4y!|hleu;W_DEwtVWkZ?Ttxbw z#+#%4-PQTwrd1!!>cR}gjZH~}>eHil`EG+jq{Q{lU!kD|wMO4nT~g7?cMV{Ss!x9a zcCyN?o_Z$@q{Vk~`bVlCJt%L**LTjL!sU{OCthuCovc3nV}I|jx3fRr=vFK85mcc8 z0O5B4faL!rC$Dd1ZK!Cgt8ZpvX(aH+Jx;*Nz);B9+Sbs{&cw>{za{HO6ZhQ|`qBE< zLV;$(=+%pF-yVgkgnhsib*k?F`8owG7 z6r)TNF%*TEH=^kEQa2t__m}!lzSk~`Q%=I6#Oh5dC*7~SCNw{eCE4vbjV&`~BmB5O zp#~J6v7!UlC28JlmB2W zG#+*BoQ6AKcXUiP6ZIn3bY_x0v`i-rwT;eGg%;pLvp;d zlCNA`!yA0@-pM1Ondpsk(7WY|bY_KhadPV>hl%py3ZvF@letij_4)V&GXTQV@djmt zPjNrKK@si!462gByBX@%N<`?0;56$7VnWq?G_w&p<_u4G~4!p%X8v2~A&7>AVb-I)oiHVo}>tH^Zl}ZuYvtbRUCBsRdmd zM19E&EZ~+*RdsbS-E@9Gk{oDIp1-MXQ$0GOUoOL!>%UN-QM9k2&!!KIl+x_2M;i%o zwIgFZm2#MpSnu&|mE>`y7SLowovL51?v!jGp)U4KcKia9#AMe|0orYwPuE@?7(q5` zs-m4_;~{D>MlK<@^}d*Z0X%;IlA?Ry)lh#E#sur(DqvX5?>N9jYp60NN5nbDI{Hlt zC8-HnBI4xeT)d+3{xQ@Zxf)Ad3UPaIEUd?St4P>-Yq0Lp@Q~B0`Fb%lkA`{LL0Cc> zqWsngwXjK3_^W_c*EH?07CqSesO>CEQ!@U_fLztQ4L8+Jt1}E1W&XQ+suQe$GcY*P z^PE1ZZBsa2L3;E}Yfa0I-7Fb0lUpPn9h*$)@GG_{KqsEhG zNL)p_yzs`x{we~m+%@#Dnh+-t`6D60kkELi&+Hz2;)j`xZ8BoN2rsBs33St*Q=+xc zonb!!=D8Gp@D3>*L6jlt@vFY$q5@kAJ(=YbcYjCfxEC!*h^3dIovPW#(Q=s# z#Wp!TH@Cd>uK$aRY+{l-vV?m>4z+qig2Hfi?^i3Aovt`@8hvMP2F`# z*oZQ}I~?5xZ}&WT4>lG!PXV0BZwl(pGuNa(%86Ns2qf`}!qsA@kplv?Tt!0(`!+66 z)*@#4Cuk%)>hTQlqS`56ZCD+`dc0M_m8M|(x4EzHRKth?s6SMvoYT7SDRI=rZxuV6 z_w3@9KiQ@spvVptE3l05VV0{Y0=89YUKMqOBwj#@%dB(Pf3=V#!}U8}3<7yG3<90a zUkTEqK&&j@Ocg$v8JAtr+1pl4Jj9uJhIbS>MIe$r)uWrn>{$P(9N(?HO=bOtO^ZKs zNo!r>LpHEsmal*`ZKz|#D=u#Jz4QWNT>?VpXLH0xFz13kt&=(gLMwn-7WHtisFdaX zF&2UATE*T>^0KW{8LE;{DCh3AgLES>x53*}YDW>F=j8aZ>J`%E`6IynVufT^gV0D; zvK3hzXnSIqYD7L~yNVrj3|Lxn;T8B~D8D+^jAAAWfp*6haF0pQd0_4{`@vKtRt_%X zro#cXylr4;$4;?%M*sf36Tpbmyo(!rb};QVObWX(|Fh9<8$ zSFQfl-+XUI!AB6k~>GK zf8J{i9mDIBb5A`j-2>v2*1Z++d8$6AwF|vIef7i{mBX4-5CDol$0kL$e z73^@qE$;Sa(3^4K-O`|BfjE!Kx3NN6|{i!A`{wOgVp=dtWF##9UeL-74i**Nwa=eI)+MqMp5Gg=E4}!k8~DcAqQreC+K#Bcfx5;ctw8}!EH(&Ezc*tIoU;Ti)#N{17yg2y*6IAoF+^TQ z?u5xrrt*dCWGE3yJ8CMVvOUK3AT%4 z8xL#J`G5*vWt~F~&1d(C59Jdd6kU%m&}D8E=jVHgmLHcpL3X39xwz0`AC;8g7Sc-AJRPtP@>+u*meir5+6~VR=eo{yM|Q z(EK~-6CJ+r?4IKw^3})tEAUUbsd#4$ovom7V2R!;jo{kdge_y!tJuqPTKbMHNo;YJ zc@J{RlCOw|>VA1-YeEHEx72OZCRXeU@ zof%kWo95S8?=DVLQdA3c5JLl=z-T3HD&lukSuv;*bMORp45j0YN~;_^SKg;2*6uZF~#lH4No`3N9m$Fu7hZxeE5 z5LdqqqKxLdL&@O>3*PQE2xMWDXO8cvmFp0o5mN9PMQ&kredBMMPC!CE87y14@J@;u z*;KAK$H@`Y%2=Umbj^-Yvr4v9r6(NP_JX*1fN~v6RS}Vk}EYiue*1b{R{SC>AWm32SQXTmqPu^jIiV55Eg$5)a}8Msm~l zk?-#YxNNhOx`(Lbv!cnD&lC;9b_XL*GrAEl_T$2ag-+E3Dq>3gs|JzyPG-`Uwm#eA z;lfjB*~#|nQser_%gvU6V{+s^9;nqSo{0ciV-Cvd(zmj@s&tMYy)sG*NBB7Fp4L{y zh0A+Ojs!Vy**>B`FBXv1pSK`Yqa;T-gw_pM3osIm~k}H4tM}9{KjAy0^YNnZ=gsf=-b_9 zv=W~JPGhm}gWD#jEilk;rBk1@-LVI-0wa2g2gUf%h%oLMM=Q%toS}1^s#ntEu-iSC z$Am%HL3_d8*2oNK$z9{uSMu8%vPIHlx8B$xC@V;FGpFv0v9|R1p(jl0;loslpC?r* z^$=J+$ykqnEjiALllpFQ&d-j%I0nbOf1wB7+V9GBG?AoK+Cm3Udm@5`oWMT4bML1; zSyJ%>?fg6^tmS^C?OI*sNw87O=`@)|%#3Wb+pE>;eIckyo5D!g%E1wHmE;2rd)X;E zZ?i^UPY|b&)VEHpH%*Vjbmrg9tIVJ1a*5R!cAB$sn(G#6l0x>&-1GyEbp&9{;<^^A zETqCs46yEEh?bZ z`PXoL4&>H9swenSM63P`U^T!$y$OCVkgaWh&Q@*hO$_be1}mXIy+s5}tdt`r#}eV- zW))^evRzHRZef!;IJNVzQpL>Fi#mxNhU3Xx&eR1~GngX7HKxXuDlY~NTrR6u^vG$( z4cTP^%638898aW;l% zQ`gsm{I^T-KQ+ewQ{&D5-uTz+_&-WZ~b}Tf)M{4KRlz|@&9w**z4+>DeKyr=<1mpDj3@THQt&VGWw5`R|@{J z>k|oox9b`HT4Gigg_iPDhT~5XZ-{?$E%v{hUjAAKaNIt`^s|?AkpKCR56}+ zrM>I3P=@tyo4vk-zx2j@hPT`OmQ&_S@JoxwXRw^Z|0l!8my$1i2A?H)oqpSO@Fo1E z3)?f?)AjFrvb|J!>8katg6i?x9$PQrFCB}X;b@SuUd*l+KsehGiM`Scmy5c%6%QD4GeZr*!_&&K?{t$Z&PUTy?? zRyd6NTie233cOst^DF?&|2vC$Uh-c)HGk%NmHhvGhW@MaUu8Fcx%Jimja=u;oW;wW z%QGIU`R`|7WF;UV|De$Q{Ki580Op(j`0xJ!P)h>@3IG5A2moMuB}=Jbhssci0RY3; z0RSHW0037-LM=&7MlWn>VlQ)Ja%pgMEpugKb#iHRc`j;Ua;&@sR2+%cu8q696WraM z;O@b#ad&qDG!Wd~2@U~*69~cGEw}{D{`>Z-1)r}lpL z`*x!w3l0GX^7;UHQOJV)%LDdhkr!8c%OI^F!KC#2YA_&%udBI(bn1eFfLMTmfMERV zYVvOtq$R{v)fnX^T$T0g@mNrN8n>RSkK>MW#jITIw?q*O-^N*-YxA@tHh!B=Zi8O> z8iS0K1aWCunKm&zdMC~2)z$qI(wOv^76+S*rf7n_uIC2Xuc65vJrjn;j?Ai8d)Wn3 zE{GGcGhrn^VWWs*2wL~+&#JjAsmkrkmd0o0$fsy3W16Gu$SQXY__l9w@!=Sn6zsO- zM}?YD>=zOe&P;dmHlMAmDpx5|3v?1>bN3N>WC%F!Qv(EWj=81M9@{_$v3-@^w^-&+l`+VKSVHm?H@(rX-?iSzpz#M+yX|d z(R1%;ZCV1;p!dVv?)y#uGv7C+V|DOGeurv;rU$aNKn(x<%3J_aDeVwpeLCE)Pm6z-he3m z{NHZE8DM8(Wbg48n_~Y@ zH)ZZ*Z}0Y3A5QYW+@k91>!y;wo~Zs>^DlOy{`c)f%=EXn*gHwvI@tWhh5K(0OwH8U z<+nYmm>SzVnV35L#es4Er#o@CuyirCv2=EE{#$_g?de~f1%dx`Q*Rv{ob25IHlp@+ zE~XxTby4~M(@l%o0Gyp204^5db}mkT#iZ>2t5gYyfuVt^jk>zdGgr z@p<`;>_1NYzqGU0Ezd!Gf$)2Imj1H9zbuw^CZ-;YR;D&Ko`g|+NS(|mkoTDzuBBku z{gQP{oi*5+ng`QuewlM!SkA;&JI|?d`%yj43Q^xk=>fgWa*bR75yb zT3t3T{cphk`7vM5YTuDbPU1_~ZGrr)Wd4tJ-on+y+1`#(+{4Ax&iUn)k7}|CUCb!1 zx3ILF0o|~|MnT>IGH=vqb7j6`Kp2@%DsRlzLc6Tmrj znMWz5Jt$TOT|4#>k!OZ9YAJkS+#w39bJ}6JNaN?N3Rd7pE{W2&v*B;U42`B4`-2<< zvVjT~S6}^@MisqRGxQZX)WXexd8|^==Ik6#hYHDlt7ot{+Qg8Kr1o=Yx0%CI7qN@Z z5OZ{Bb-FiNxE{u)CxFtEJC*sY(`Om$0$pPaK*L-I{{Zs1?+2duYgiGQSQRu0%NiVo z$Ew^KYO#hMnldL%7;o9_Fu9^fkrK9vyt8#hpTyGmxT?i%Qmn}t)@luc7j?@ohZfAg z?d33aK9ABebUDEnt!reO;c!u;+k9L4v?NpcAg_#Yhy3Tusz0%`&iOmj^#4DX)zZ$= zg;DzDk0rpy(#zCoh)HH{NM>YcbeK+IbegW8MjpEM;zL3NE4<7H)dblHR#{qruE0)a z_r}Ki?s$Z`FY);@j39sBKYpOjdiKjR7jO^|*8gq)FE<;bwY`P2tFhN8U_!5n5#$3;-+AbmdIM<(-a73$k^KH^53FD z+J<5$2^US+Bv3c)W--HbU@Kshpue^DO79DULly725RK)sKUhDknFe$bWbO$-L!(=`cJBP=YqRu-;y z$LJ=nm9S)?{E}kaiaR=^iW+FPcj$>Bp1(O1zD4-sOPAb44kbH zt~Zkz{jD9R=DJ3lY<9FU%0yMX&;@a(@BiyDpATVA1PgnpUJHpt=I+=lDF4}0`pYYDD94Xh9 z@^B?6fmjfYRuN0)sdDcj7GV#Q!lE0U5&+CIpD2buAh2BkP(ZK0GXYe-q+;oQjDQ0n%4AKp-1 zx1F#9Rq%jx7p6*kU2l);`JLj`;yGLWy&?0%1;7l`PO z`y|uGBT^nXo?a&){6?CVjNvjmJ6ebSy_R0Z1E36xZH^vY{X?k+wHG!&6s7~OOs z5`3p>nVk;RpElS>A-ohmMQ3ZW+vB2nx*$@d%8JDYI zZsr0WTJ=iP0!YiuScn)))8i5yT4#dSaTqsL(A1;@qKky7@{V~(@zPxy(MAPewPkp& z)k_kKq~dq{Fy}}gt%6}{pu)=`gMf^a zfPg6dm-qBR!mo8RwhEqD)t8P}&zS1jc-nu?ze>nX@=aHU)0 z8{@)3wXirlp#sT?YQc91S8%!ANJuQu<=Y_w!r7mq#nc41@_p2Zd#^ zwSbq#Ul3U+^kff=XSbAf2&*|;<@;Qaug>oFX|wC+gbEz*_3At$w-(BqRceUeD*$A8$z;v+8DUIrM9F+ZsQ?y&pF zcg;PPbev~fJ~>lGdEY=2Z$+?QN1+k)!BgDeKUDhosCGrDJh)Fjt=^v^dq@lFC)e5! z^fhuk6lC603AG1lJe+Lg{Mek1TjYoyj$ZEHt~kwWq{eTqY!}x%}Wg zk|)N_mcG$MLk!xy>++_e$&WpbB{WKq&31CO{=2cg9C>_NNvF19srDBpG}qv`Y!gxz zyqI%lcyrz5Ou0+bEUPR7ay)pW&F~_XiAPp6NJ?2VP8-w{#Ps{fKt{5+nnnZbaVDKo zCgeacOh_=*e0o)1>dy)0Oc0po?=2~+{Z^!~-0}zI+$ks1EJ)3CGgw1MBAY`)TXIQ` z)gTdAS>F>S?Z~-Opo`Z19Hn0MDYDgeY9D8T$G=2t%#tpWw-;>}i7wn^6 zF=kfKD%8RKvHrn$qC0>GWduiSGE+jidsk#I47Zz{gKzX|_CgI!y2~FP!aTtRi)(CX z$E;6*tRX}9paw=8t-y$q18<1Dd8WwcVQ0^@FEY*byYErj|E+e(ouiBbRmE+_ScyckkHZZwaL9E^T}SC0mn%%#3n%Rxot4 zE`<@wWv^23oc7vKoxCrf>^@v~+F{E=lv zdfUS_IEQ(phzg^8`B|)GQiOH7zKm*0(N-p=?;V*8?h>*XZRZYHxmk;X8glg-Nodc| zFE&5$IWF=rCd_>CNR?F5;Oui`tm(6(!7j;&u{7G%Bmq*Kg{0g_xQ4fWRJP)7_z3iI zWWUPJSVZe}KG&^O)6mLDV`e)9tAfHsaW^X-ICpWSP!)~SlQ809>QBYR4Ua)`#E|Y& z*_NWT?UqnAGxH}O@wSq5GBBY|k~{iN+${3wBDQ&|&t`mqcSc`EdcR=g=#B4h@5GMD z#+gyOsjcqOW96wy$hCIu`arxgc}n-Cay>ZOV3Uv#d~NWo8FxV9n_vib?pQ`twMtCd#)piNcZLw}DIoK8I%BHLsxul2}i$=~|ba4a#w zCp?nHq|jv@RiDrwC^Q#b1fN5_omBxZw2V&izBPFyJT@BqrkW8eOhsR~g&O@wOG&Ju zZnQk_QP<8Xp1}vDR(%=&@wkQ86_63kC#z8(ADo+1nEK$|63Z z82a7ZpNERQf3>thAO~ zJz9NjT4YhAwq7RH^rlaI1!uyR2V_jRWRHye^qvu>C>$(J5) zYbdaH$nWq~oqLD8u*}U2g17AQpgNl=Qn1oT^ie3xc#D%vH&~D#*=!lVYQkZ<+@_Hz zhewOuCSj3Ockx>x88=LF-BNJG*SAr4iqi73l6EPl__J_sOZOj=kWI0)K7O*IE^KNN zo~RMzltJrV!BvF2*vpE#9YqbPi=*)|b;Ih&w|9e_&l$z3^(g-oujv%r2QxCJVqRSL zj=+5ON_?|Gox5tV)^zre<|`J#2K`_@YlPC7w~IOZvhhIy(^2r9*1XdAYR^m6B*QYd zLvR`X>7c>Rp54}0xpro~<0Hb9*~W}tX5_q`T{`@IW`j0_BZSMPslrhi**$$f;yiqv z!;UaY=I6VuR1K5jik9k28%B&b@ykQE0Jn}qT~B((%iK)RD&i`FZhd5u4M3< z^TFBIl9m)5#+PFKfBHxsOxD{)`vv-0RkbrE4C{*ois-}vF6HL>l~{Q&#?0)xrKeoF zOQCAD=7%yV+D6#zX>PI63Rkw(oHX-ua=L1LYh(|Sw=5fu$@QQk>jrYUv2*CR3M{9M zz-2(g&M3~}w@NVlJi9tVMlc#r=H&NmXN;+lLa0buI#u&Zq1PP=UF%;^M9LG=ojL=P z5#?Y&r^f4=sVX1<{>ZRKSRu0*8Ko~c;Rf@`3g)BFA^M0?SUBm8W11)cEk9uK6d+5G z=GX6=*xKp{nIllL@j1la+N1Y6AIzGj8E41=c2ts1L^G7qrm!%mK;EJldbtUWD<7U~ z6AxjNH_{MZWqLkTa5TVr3FZw@L04RHkE*JObGjsh500Y+h!x?$(AUDyoZb+ye_Kq@ z-jQ|Uy0^z1Ss&3&Y%V{wCyLq8bIX#@?O8?VmI5zuS$3-uW5O0G4; zCS-u}IQj{&9DK!33Uj;u;n6d#j7}RsV|>3XTTE+Qhw$P8s%>-K5qfQC7vV%O z+lr5{4RUBAhF5GNj1a;FWS>P!GZdyz0gP8{kMXG$-?F||m!1Q^&5kmdCx$?v^dth0JrpI*~bt5q1sNKM-#jFHxL;uZy&&7bEF6{|u zz9;n5p#uA+DEVn<__*Zm_dZOcdVX2>WRz8sAI8|ID^=rr@1+sMkZ!khzsd_D>plq= z9Ii3;G(n1g#?ctQ>9)=BA2kT~HMc;M%?4ZS%-$}pRx?DPtEkf!yn@t^E4h96AaD z_%%TqvYaY!XmR+synfk_%1<&DWoA^ySh)i8Lzk>1LW-`$rpTd`)n9Zj*rl0^W*16- z@B~Na@~zog&b(iM7=<4)KO`R|?xF>&<+lh=s%uUwFym-@56Mt+S;GJD0aDlFgQbt< ze2-J36N2@5L+Q}6+0QpJNh3j%qfNEv+DI3N)CrQ5kvXOup251jk=JW5BwmT#9>LA- ziJoWT+Y_4*SFlSVOl-{P2>s|cvINeZ%0sp3YC>b(8op5+TpUkz&>Mx;_@$B0}tu>Os9wiI3pi>Jyyehk<=U9O=b z-F9cjktN%9x{W(XuK~E_MLy;4TQzhP8kO@7WcSQAJrfV<3ZZ3U>XDSjE4Aeg!$Bei zMupK=sn3oY8qd%7gBXBZvmay8br7qvw99t&Rf4#m^}tmJ8M8crla(N za(x%KHdbx0xEx>YZQjjF3DWNu8I#UEX2b5$71kF))HiFs!;+n8gzX%1IpX_bt?#HE zBAaTR(@m_>3gk&vb5BI@Z&12CTWj1_vzDdvmPA!4=a)|~g)j~UBdb)pC4-f?tgF}^ zg%H*qRX7pq&6$S~5_8pB(@2nuT9#KmWc zeEyEU*IG0DnAPr6ZM^^3KtxZ&y^3{xSLlp$X2W)E4O7TIhD>6Rthv!cvAOdQ({oXG zk?_}i93@W#fSZ`S9^h_{KYV6A4TJGr+zm*aYsIO4YwdTvN5JKb7oM|Q9I?ykA-9;4eK)Ow5HsuHLvp4Ebp;2hrzgoX69 zg#NXLa^l^=XShKL%A7LA_Ppvj{zWdxdDE7Q&lI& zwU(S2A*I$}%df>#P25Ee&NB;9Gz6zrCusI%x-OCt%VyYv*$!HaQ#m)wUz)y^zqEXt zb1=4*NeO+HWi(pO{tVWrGnO^0nEME7aYW|Oy6va*MV!ko1ntWR{yqfHIlg*O!a;~) z%~X1?`q?ekb`i~oYMmx#+NF8AFE`XWBdx*6*I`DJTNp>?3@Ie7X|oMZm*5B=dik!6 zIQbWGkK_;TD*aMp*GX2sC>4kdj)A9w7Z?%*j~64FR@ngXEh;T*X~! z-Mds3hbn31s)stX<)E=mc-qEj-Uuc&Hp9!71GLb~ zCxW3PVZY~+(8N|lWSh4p996gihwIAszp|dj-*h0KNa89kB6_TcSR`Z*&9Rxc`|bEV zpB^G~u(9h+I}UZRtMf1QmIZM@?;{;Cdyr*w+S+Vc_Euw1NbFB=%Qee%+J)P(41bB0 zF=N6_)=9y%Az+*4Yhl8{A}j6odupH>Mm_!*3=tRY=`|che+H{L1~tig(<%cyCEZ;A zelklcgPZ{FQi2wL=!AU){X^%6EKEl`6HX)sjmK<1VujQ%jCze=8H(X{BU>MA%GZ9l zy4o3?$|URhmHYB)t&oq}{?)sll?LaAa|{tepyx4x-3w@CnI^`W#f-@(x<7AeGk12vIK>76w464!KU z@PeYKu&hSk?eYR+G7=Tl5)}_h=#X_lFWI1i)$MQs+gYPnBa4NeMz@;BklH5bJDW7^ zx@Is~E8EkOFvO&1yQrwsDP1tlZW%4pgA*XD){|U|io^GuquVReshf=)pOekrw>xq; zgvi#JU99~Gv0=#ybMeErFCB@FE}6@|BC*+-@i<+=^V^9{b2O)M5zWa$I0oWyh=QR; z1>|BQjrE}8l5iI4nxWtFJbQy{K~1S*YczWDkja5ifc_o%F`oDkx`(US>7Hp9%^m|) zDgM|z)e>eGRrHfN+83O(G-u95oI9@-TA-IE&CjAwpB5AxDd=w*% zkCSY?mFO<8%_>7VI26v>PLmO-Qd1POi$w;jm{0UuI!8D6#QNqiK9h@+!na(#qlON- za{sWQxVLhpyOlblz{8uON{t~vINLIpKr7zO}lHrXZ=O|vbat-x?a2*%vb4Z|G@~!-5fg#un_D5e{jfN%% zGt*^O3a4+AUO#x?AQ=hihqA2N*gp}E@bY|(ZONECi37&qR=n49iVT3HsOD&9cAU>{ z9s8!A;BEvRtMkJgL;YiOZj(Y%jvfbXTA?jD8zL)Kc><;>=101Wl8pPppN6=wJ}1xh zReJpf^JkG(GiP`vcz!Y{2@S^3?v{OF-5AKTHk8J99_eIL)~;x-5PJdd@oSBazL)h7 za}%aO!7T~8IV%-EaJuy-MMTzsg&+7gcn)|2ez0?eo0NhJ7~u6yypIY<@BhyBNi(w@>~$vb4{K zG4fuaTERl(y@pcN)~FxXF4DE+NUOGBjnU4iu$lIQsRrjumFcm=H3-?C3kB322ib2^ zEp+w4G@=g%p%WV&viOz;Zb;ca^fc7>+o4DE^PVMLFL^0s9q{np3{w&E0j##z-9;PC zlekkVDN%Vx#B~L8Jd7jjY$qOV?LjF%nLrMT@cM3zMK@XJGdE8Jy^`&YNKV&g z8Al_Z2&Gw%@~@es&iM0|?@Qz$2%yV7e``r_nx7EOC zEo(Kh^ccf#eo~tnI9-@x9)+7e@MlF$*%W@D`+4xAv7no!9|f6~P-CVn;wvZQr-Xbt zJm?f2BqtlAW?*BSx@HweHG+gDYgB+#8NFoq5vf@4kv@D2RMWkOq&Vj_ee5a~J1nd0 zBa3uxj9?ln)6xTe`;|Bu9okQa2q>S5;Wjn0-qwCPXKym+34}k(x?Kw$!NF zAFIPUdj-RSqYSQo;peQ!O!QGl8yk#_hk0MCF}`Zecm057MQX$e-DANG)kBc$?n}JS zk#i}NE{^Qt{!A+FNz+0$(_ap-0{v7a=+k*wEO#&_b(>n3Gc^<=ecKGiUB=2a1(F&L zqW|&Qze6h$qrTszgj+Y+QHrDrpwP4KK)tZ7B_}w>oq{yZGkD@Q`4}WWHwSm7$N>3$ zphZ(@C*tR|fUYu$k%nr6+w;E##YtkTPqJVjAc#=^eRTPIE0nf#F?BLFef|2zKNORi zj{Oufj(}aO867UeCM!-ODCB}2{pJ~zh{zNt5Ka^$YKNVpa?VhrL8pPs4-H-J4bctg z19kUK;S42|l&Cz$ed~PRQ?%m4=1$`=$dR!p9D37kx-8_DV{-sq(@(%k7YFhju1pX! zBfcHO+*tEr309Y`FCRhhTY^Ei+}p)eNlqzeWZbg`GC9w*bxXZHx9Sm74Jq{STC;aEO?p)_1X;@V?lyU4sw7LCphMVJY9}sDp^}|Jtxtqs&>t@dM`U18W z>m-fpRRoBR%1ub^1;R<~hJQwbW6GeJ<>lSg^`<*;3=`>j3@D3{R}50COZ2OW1}#1mfNLal3|l0O10huHBVKq{dOo@ z>7~1(QHGd1NI17O6Ccetn`IijHi}X}al92;d-`AnsL*0piqR zQk`EKC*8M`(Z__;0I}dJ!&V}}3Ln*S3h?u_Zu_1&h#tLQaEKm|V-O<1#0csgtcE7l2K)cQTkd35;p zFmq`1SV+hp)Z={;Y~*^nw;t1*7<|a3ZU$s{B0KMbFV%( zOb~bCq40SOON>pdTqWz4tz_yMT-OkA^3X75h|yD$bhqb`Q#vrbZda?TpdIV#4Rf9i z$Bf_jtPCR3`z*Ce*&)|%Z*|K)U38WC9fZokl3a5MrDfCp%5p4oYR=#-`@5WU-LJ?Q zELvuGv~ZitO7O+VQ9RfzK?W)D`IsuJ5At0mfNuS!OyxhmbehW`P}_ih)mEHA=m_7@Ng}uBx(e6mw}Ox|$!W7f&Qq zU5q#y0U^PX4=E<7+UaVQJ>BdQg_-{`CzFLfs}B+01!enfkH`zTBZ%WtSqh=1Kq6vQ zcJIJK*U8W9Sk@7thV>ssO{`Yq9@FXRCl>)7*?D4TN!`t1mbwsV10=Z1DGr`6KAF26 z9*6j>Lb3N}$d(Kww|nU%G_od*R#+OA!vLi7R1(Wl&Fv58Q{c!)Y3DsIzKKLqpD{)7qw;_ZbYr z)H1=zG^j6c__3frpz=^Tl-2Pedl|s&8n=Oj{jLhf%WX?gNB`rrnL=}53!gcQQYW3V z0;wjdpIIirPQNU^ez1*W{WiLK(C(uc+mL%AHMF2Rtqep)vb}eYX)_=xF>yxTn#BoT zzayz@C{PXWyy`^EA!psUK`+<8$msanHIcJLIEf%dL?(#<6MwssmcjPVk8KA|@)0gJ z?gQ!~_fdwrQz8gh{4wP;b636!M+uySRBSAHmMXf(DJ5e_hsZbLn*9+8(Otg&W}*df zd#Gks%IX$zT6xG5U)_JOj3Cx9Z}L=KglB|_gEz`Aee*?s|6si?35+s(Zcov>4ueYwj1)n* z^^JS76NgyB^jkwOB81s-WfMk=R-3gcdwZ3mJ@9F{K|BR2n^pHQ=X!{_HitDcFGdNE z9Id;xwLTiOmi)(fe7dMCX|bXv^dmI0)b~Ei24O#{@86aj5b3#8L?!L&fSPu1^hNm_ zLq=l&Bd~brYT%rq3GB!4j!=#tW+k|%cBjD|uy*j~KcP%v;*9!=^l6Z<*nG>itcW%t z)N?KLLK+v2<4)rp9^<~rGp*@cH!_2m(xeBT1Jt3g`+FTxQyh4gL?SK1{FX7=lBCud zv)AltwJ{)fd`D{P@fo~%Z3JMrUZ}5qW9cyVa$SCl1lngc;)z4 z>S^%%UK+&|QRTa51fza+T^;+Zk|tzAfG2$<2gIRW;2arvR!|+G>iw?1h5D0YS?^RG z5f~8qxr?xCQhKsl_}Yl5&ayvIrfL8buXJhpv^JH1tZ_|)2aLK7uUOF) z(OBc+?>&c;u2y@T?T~5wAQm-ntVbSJ+${%-_jf68ssn!P} zLCj&I-;vl?qdZN5rW*%i8wmk}LkLWGGL1n3jIAQ+QV?xTtCTd%aaaP@9@BH3L=cpT zl|>843)-dM-d1Ooxk{gle=$#TOH~ZUxS|376bXNEOrhqE<78ddgZlxIg4l`| zmiSWYrEe&0BKdGMdFKGd3O#sW=Tl0_O(qCvr<{EdPDHbF|3A6=3& zq9c7^Shzop=`!J)WG5(6t)z7=;g7~HFA{xjHlvvK$XHU72;La+ZR^n5 zW54ju8{58V!4~ZFB>&8^Q})Wxfc4X7hrVi$boSw?%T#DvJl2o#)=mKuKRsGR*e-5U zDUY?{XQjp_H7ghh-p^nm$CdhuUpb>`7Jdx^T@(-Bl9hHs-Ekn2B8MK>Z3~DH?T`#d zlACRRbB@@o=>NtQja(=Sm(&#YyZFckYZQ;}Hv%)Mbh0ss$v(3EEFaM86Oik3)vus` z=GSaqJLXlSKmE-jo&67yZu?SvQTm=`Ws!dZ;052@1Z&qb^yi%+85_a80Bnrd-mX9A zw%5FPU`OXeCr^%N@cOOT-tVnwc(6>&Z9|8n!$%Kx=5B8O&$qYm9jX=b#%ObT&SWtJ zcH55djRN$ZO`GeCq5za$ir4CE`)w1eOT<^`kHXvcT6lq+I8HCarDfE*>FaPg_K)F` z{GY>R*RSCc`gOSEM!+l8PDtK=9WJA9<)i>PY;}X1@3V2=n#Pc{mTG5$eS#lYy%6GP z_9Nyrt{t9vhtcbuhowM__nFZM>MJG1T!y0s0@dYAs=BtTJ5S#RN`3DE`RBPS75sWz@W))3fajvVgQU)de+q7BkWcwv{F zdW!Y8+RL(?+LBOGAWO&_jb)o2r7YWJwNMZ>xF?N}R!g^^L-f2y)x_ z?7Vg*JwdSYDgr?M9bww1fo`yRB=90-@>{D7-41$2(D1F;$d#ccRtQ097sIkUM0BCp z$1~&sN^+qrHgeOj1GAA$yh|;5LsVyShRJ^NS905QiI7iVke#Re|BYNxJFZ%&7m+>+ z{kOUzf2f9czoyG4;H+I~`5S7u70mj?p^Znd{3RwQmV6xIltWFZ*(@_R?H?l=&ry`` zCD1oM3k`ahHDVT~23RJHra50_+MBg!9UqJttTuowb^DM-lUf5A*|vtqv)-b6(O;Ow zio`hHu{w;~FYQ|{F<+TMH99PI62iMPEE#hvyFHG^?ib5oHBat4>wfCz7wtPJ({UqQ zMTCpq_~d(JN#)1cVVFVMBfvPQ%TjmHuc!H<(XA9}-ru>8=BtxZD=El)3akE{1F%vr zd;fVbhyyh^+u?&>sW2PWMI!ajSZ@l7ix%{jw|-jf&Tuk=XDy#`TBicH2RFqTlD-jO z1dfX$5Z7LUJ#t~zLw^!5n?nAUTtOsV%2VUG}zD8}DrMVxUM)nrUwa9BFDE7eSCS1)e zMvjIf-Y3r1(TNr3#(Q2P>NRcpzq4t?Y1G^T<)~XZAeyXzeaDD}SyUe>ijV;9g=nu4 zyxk7tDwSG=wK8NCKu$PRwP51vW>EQK%x<4l4c-}{@o6WD?Sh~X0eiP#8X_Jseq=;q z9z<)>%|v=7j$&qtsxt8Da5wj0iamqs8RdJl?lqKVl{q+K^9mxl)8POjxwC=8uBw_d zuoq(nM2B0}SSNTY(7BE?KkaRCQ9wkK+Bd&IF|~=T`$*uiRNEa`L{YeWA=gAxxZDr` zk2;?GGweNGw9iuge2fJPAFz}@x#I_QIl4war6cB?gW!V5dy6pt5sbE+K|>V96Xuy4 z{6F7uSD>Ieo8;gesT0_{}crBl2q`cMP)sd2G} zuw|5;Z3@m^FrE@E3R@85G%fP^AX!+9=|!iHKJ?i?O^vR9Iu-({Ffs)*ZrVsLhum_k z4A^g42Yl<2M{dL^dXYLSg9#zFFL7vs2U?y%oniN_pe}EWEN$da`;*pNo<8-7xb>D~5iVIq<)mIY|F* z=7^C*)7k?k9TOTdEI}J2gR^JFU(FmRWA-%!RT<7E-;eKNtXq!(Y2Oy(U;0GhLn@M^ zhO6Ayp%}wxB`)+UlM>Os1hNeE&B8(eNBu4n=gbr-(55Hqb~|nIaU3;-;A7wg+uJkMj8`KvYlfb2D!Yr|MWaviTlT(abZPq?_@;>arj(o;#6NQjvTMDl z_QEah-^(rgKe)9+XF(BAn6$xfAjh!CTY`g-nw+f6wSPmdR{xP&NleLe#YS$!tcGk6 z_7Fb@0u3_^S}=fE_yNJ_OGXJMS1GktPj-YazcYXKCm}(9PzUVS5fhS2m;OzRETnEP zCNexxl~+?QGat-mZA~7ga*)o@CHl*y?_oChC%?2W{Hm-9{K2oxa%Iu_hnKlH<>$<)$6;>nTc$n(u`YY!B3%@y61~2@gzAByH`F&A3&Wxu2 zSOW2i%XSL|v(mTSb<1^G@7*{ss7$EPGN1d4}XS6n+ z`V{6PK>pGlU9@3i(C1upuYC_@$QV$O*QKlB!NWSi9kXM*QA2dOG;a~ZTn5tVQoyQn|$)RutsB;KA@dPIE>c=f_0C~JL8f4d?U zmZb}G2Uns+raULMBV2Kym{V+II~zA2#FvEK4wFN0)|^=HYxv1)hylepgt*$^1ND)0 zf)Sp~KK>VtPW7VEb0lZ?Z^7&~IPHEE`~BNb*J?#{C6r(IMf!XB<@iVb^GoTx)ZLPo zy8HG7s9ld?5}ra86;2Y6O52O)vtrlhw#z#;EJyd{Hi13P!si6~hAYy)>qYGZOKgF@ zjG4pDe$Jz5pLhC>?N>pUx&|S79VQr3;QBY6Ks6j@y1s;36IHQc-2XOaYGd=;u@29= zb^76S=h;A}e|;S;#V%t>>=!!|HE^skE00^R~%=aOT43-GTj2HqkNE*x1GtoZ54j5dla5VW5 zavH_U&8cDZF20stm|vw=Ri;Yg)g3wb{8D=T*5Gn)kwT=zE9o;4uHS9+XDH@};eT$} z7Y>OJ)+#9{%Bo@1MPifuz4i{g*4|lXyT!7-m)cADQhT|77!#li`p|on+-Y{Bg>aXB z!IcS3shg34ujU}VsAH62izJTUF90=9RjVC6l&x&hT@*kbLRcv}C`Y3D5n30{S7z%) z=m+8)&=ObOm4`$Ss>4~?TU}_!p*JC9gBAVZb;8`nr;@q$)%i7H5^e*(Wq^ZrsV(GO zu9KjuM?d#TOmz`~0q>txWq}_PngH2AVgnHpqXqB=m6Lvf_a5?Z zg_rXm{)pWaGsYj`r7?3m6oy4>jaW=p8^03} zevA!X%lZ$ng?W@8e1#5-S2xk0A1)uE10*%GzF}0gY-@vh>N2fC*?DPRI1F{!0w>=@ z4%yl<%rX56FaKq}y#Ezm{%-6Ip5#}#@Xhc1!`Q3+r?Ch4W$Xq0GWMXNrk9iJ^nWw< z)L!OGmXpE4U&da{Z^oV`cKp|${uWM;oHo4}Y31eAk=G&&nL-G!#@*C z%|hFH;g<20+f8FUHbEaXfoUIlKw1NLX^XT^Uvb@{9PE4+iq~MoB*IkR_O?j}u36I3 z##Q~Rua_5!31zQ?q?V$pQbhYNzFyO&4xs|UoXudb$FJ;C`*Q2ei(&}(+E?rGJ@{2~ zNp{CS>@HIa%P7upw;-n{`9H`fD3||oMXGzNebOy83p1FJc^OS)D2nX@yyAJ6lX35S zhE=ZUwf48nOPOYqqdTmIB>Sh^N$+`ZH4A6I1(+(6-m9=ShdFa2lP3q6Ta#NGmziiKi}(8z@d*~A9XYVkQ@hNgEMAmLNVtOHq9rHEtl-OnJGy15Xuf8(?+N2J@5@Ai>Bjchv*B#Zl497A9$x#FU%= zsTp(v#UMA{L$BqbY%YscmI%|zsB4Wy9*$+Net!C8?8Q{;cD!IaS5JD@q~DFWbWy$b z%h+@7Vy>!7{NPE=)l{$hb*rI#0pWb4%>f6>k(w`X``DQ1`uPDbCM|#TmB>8 zH=+~)5_$6Xe*C+!n5j|`?Hv4g764>0^g%b-LukUv>616}(#!JF`{G~hliY@jMd0ou z12w21AHCt~^?*xdR?a{bRjDVdlf0hG#cE7dlZMiK<`u+xhbQV(XBxJf3>Bv~ywF|; zr$)-UnL+g8fNk$ONtJqSKEi8x*0{zTNmMQ~+ts%r@!j-CC7_kBW!L$IT_?0S80%l` zCctgwZEP}@34VBG7m`@eis+{x)s@2Q4Dwex;Y4$0EuS% z?Rs_JS4)}#%3c-eU5sk6TzOM*i`n?5@o;KOTvJ~oZ@R}9GgXdgNwYveF4IJ)!HS{3}H;S+nCK*)DuPJT#bxM1FHKd{a z1B3=W%o`EK@?l>?_j^yXqcpzr^SNtc5F+E(DQ(@43%-9-9kh{U2e8sbhzyS~^~lV~ zr30O@q+Mm;xTr1<{C?**j~v9ESSfst{F=JospFnCFvxj^ygGX7)nlfTuZ|x6we~MZ z&l)ktOMAYcBe?dvQs`S*x)~ujH&s{JNgvc>?T>`pY$PGRqaAbwV2|FgGXlV+bSo#I zk)G63i~IQ6o!)%d0P8yqWt`&>ZEjz~UtbqtplQ~R5H;p5T_n@Cx-B&pKKIootI;ma zM!vR<@6TX*6$^g5zbfwhJArg#(Iz~zQH3uD)TC&$rmuz`9mQ*sBJi4|eCy+;{6~_) z;Hc)xmZ;!nSsaH}%|?2Wi2V__SFt;4B7jYfR@!_!N?ot&FfP;-%3k*BU1)-8k=~NB z?bQAW$uP=`&Q)Y`jrC|TSLIaDajPt1(5}ozd`(hR?X%09Vr+j)QeLqo#@odxTuxmG zZ4@aui(%tP&wdTvZ;6j^l1CGOo-JtSx$>e#YEA-3<5GxsaKhc_abwR zUt}%|YyGEwKLxOe3wLHiXX-c~4r#^>w4z@!_tnn>@kx9Q-QihRWA87ZCq>?G6|;aB z(v!57;OZ8zgzl&IU`T)7!WF&qFlozRS`#GNcvkYmR6w5yiMP3udJw9j%lw$wp!qw_5G&U^YZ0`4T z_XOTSMlTNl(^x+KhfYYn{-qO|-{Pd`9JXQ0^zbL{|3D@50AQGntOr)Nxh)M6*HU9+Q9{+ljwPXZAuYLzt|ltSD;_rGqKeH0AP{1n zL-~MrmXpkiT*kl0fPJ8NrR#fFzr}#&(%K}yVn7EN^>SiT)%%yCOI|JI4-Kv^`9H)c zq5m4AK*Gl7^!^&r^35F6dI_owk=V#b;>^MJHsdZ8+5@~j0`6~1|E=h9qaLY(fr5Yx zye#kjK6X`1t^W1D5hw}Bg1<%aEk9+1g)(JtH5Fq!)0$KD7d-;y7Yf&d+Xpjnw)Rc%*>3(%#N9vnVFe6$IQpf z%*>22Gses@Gjoiw6Z?Di-Q8Pv_q%`Hcekc{sx;G6rP4@h^~_JLeGciQLmje*R%&AC zu{Cb>Q~6Hn05+o5q_n!J)XBo$Dl;-eU(9dpv_?#Zl&Aa@bs#-;xP-WJdV=v62;7J4 z67Mo=%<09&#C6FPGtoV_E-k_pnWybk<@nBaF7%l2n%?rHCV)%dSr|3Zvt|7}@3Jy;i8i4O**ECvRq@E;TU-(}sa2XBIP z{1oW?oVmU}*@kAqbA%}6*tAJ5*g0y}u7tF= z0asS-pcF%}5;tIO?F5@v?N&BcG265lCO-WiW|X{A6bWp8`(F7y5a0Tq-uB++2;4sn zzk{1pyF?@LyW~Q)?*Z{?_Npk9ELbXd*-&Ykn*7iaEf^)ZFY~8QuqJt+V=Ng5tJr0D z6o<|!(xZW0Nb5Y}<+iO>)V@9vxl{)Z_@D!#_DTg1k$`xZAjNE&DIh+@l_b4o)ej!G z(HdI6J+Td^37LLf8GG|l0evpnr3tCRl#{Y8?7lq}xqxY(_sTLW(g;379?0ygPrfgQ zuWcF^c{<5TtSt}hi?p}M48~>k!DXkgmwcRLCH(6=r2?v1mmwxxkf_ZlBC^kOLF8gB zC$OX)3W~jS5iB9aYLXp7!{lW&ZHXkbsI=;qu!uFA4t8}wcP&Aha%qdqvCx^#&#}}Q z^vVjsjRH0yaq2Maf0O8u06pA8ptGdyLvY=0vezCz^idw*;Qw~mv2@s-qT2eNug$v9 zU{QA)4aD{}V%mQJ^nj`!-!VQ!hU|53Ju!?}_POAlP2~O*K{}7%;I|(n>`)%CNd8WXe35&# zhrdm^3xL{#JEYn6NM*DZd4rdg69WpQ63I=XpihhcA$#tmDer(ZgPPVtwIQI zo%mhILacQjhro02DZa2c!n zTs{uGjhw@A2ivlp@gEno_{1G(oD0d1Vn!(m#GDe~TOOutlxCCNYo-{|4Rr*ay^q7? zPc*-n>B?UDa<;F(F6Xbyw)Ql^MRCV0G|!=gK{T<4>3Y*Jtd(NJt^V5Qi*rSXvz0$k zY}2#oY~Yd+tRFS)XXzNig>U>?vP58ylz79S>@W_J;n`~LoR~7M5o2RISj(QQ1b{G5 zH0_(lT(0+>^?gxU%mxy@t3d05(9$zrOsI2;j925^)rxuxuiJa-_YY0~PbFS)AJ- zyz|1BYvHNm2T62e6=Y_hvlgVf?6@?~`r6j)AUTF-$-FT0BG{7cM@9Goe#*F~&r#l7T3_n=f;{I?8q&J{UzQY=1dgovp)Mfx z^3d4^dp>kO!iq!3gS5EkZr6$zEMO;=w>^zwCsxPIM^c8@R@ztG^VvPGWU~Penn{tj zIY00EJZ#8usZJnDClvWuj8Zb3p|F)>`L(|QD1g_0=tjQUMsnL0;eFxY{j~M|92K2@ zbi8MS9pBKnIvPH{7fH;GP+K<8DoTW?PKi!-e>>*2{dLxsJqY&4IGmN-5@GGI8Ayv` zvzXjFiDQ1SxE|KQ4bp}x2B)V!2^TYk!A`Zu^!8Ctn!94m-mUz##)2fBJ`yULUZJjC za2@kndA2}7X90Qvv;c?@ToD!>0_SNbnY?RT(|h-UVf&m6L7-;^tQ34U=$pX>ge@2k z&|4k^?EMSex*@_LRVTz(@Vs(9Y=N)@x_N|db!ykT=+}-A<-HVytFTRMKZF9Ddwl56 zptpSLQ*1wQV*t+)wE0l9?)E%QILEi55SIO^(osO)&`w8xQVQ#J(Hv1f&aY?1qFpcuViiI3V9?KqV#!d>XpjjA|UC;H?hx1^^Uc#davM)P0fKth~dB< z;RD}T`;O%m=GQXbDo}k8X+uM-M?PIoTzpO6^-FL}19;#AT%& z+C3z+@JPa0ESJcN0-O1#xt0|bE>Ye&;hdo+l%hXR!m&bCtJHCMW zz5e@{U`js2L6&=A1PTO%fN*I^f%3gIph56ofjPwB2bQMB8C08!c&#eBh=>~`y(Ucj zuo@J(2$-)3I+UL(7GtzY_;YYc;M+h_Hk(VS6@5tDdZ%4shz$a?intpXGT!k4c&R3P zP-{7Th#?wm;$ACB<|O^?q$2$EZ-RJrrUG{H25R_Mrn@2vnGkVNe#q~+^{IniHib|z z6mlAh9F4^-??OJOnIl54)}rMe&DkHuC54#SdaFdrEef{^j3u;sUn1BO_pqQP(b>Zk zR43HJ9LOv5SLKb*Y46luvuHc8OcO=$pRodYltNU|*+DB74wwZ^s{TA;Ir|o>6PC8| z?fAuT;^C%nh_GX5j8%PQaMi3trgFwv0l`|gF|g(aWcllr?X(y6@XCAJS_(JrqhtAq z2=4}s@lNu!7Ro+9vLVQyQ?q5B zf=~9WynyhnDs6kH+??+e8$Ve)DY|bO6D`lbV%l$>|1O+s=4fJ{Mqj=#-x(7T{MAYX z&%r@$WMIg03Mvi;C2G+iP-2;uvVNa851bQ)lZSn6hc?}qBpN{jJ$Y5dumb+Dg`1Sy z&-@&d0aErK5v`nR8p~B>yK#oQoJ`V&PL0%)^aN*~tb~9_dT0U}FAoFwDUVB_yfcM~ zqJKeL>7?dZ4a=UEsWhwD&txU*U6J*)}#eeKiTS*gc6l=4_HR3Od z@j1egZ7~zA(jyy_cr1o2ON}k-EKfT}nY*#{zGQU$!+&OmAk_OS0r8VTQpfc$zw>TQ^JTL9W7~UI0I$0AR zzR8sHrvput`T?p9sW8x|l=%Qw`Ziue3`krOv8t7mdC;Q2ffwxo8WJ~&M(HZU9G(0c zs$cEpW2OTYjxx{q&u61@|IJiX#+NtmmYsLp8ph$!$FG9dvx?R}LcPYg6GyV1yQR@Q z3<__}AEjq~VRMR;)rRz$eiZcc;8_tUVwO8W&>#g9gZz$Y!t4y65=W%@R#DroUz#EZ zl)nNK$RCQ0ktE_8P|^jcwPBKU%SR8A67M}L?BnkJV`rhK>}TuGPGn*-ah7bfZ%Nm? zz1H)U>g7wEo}Spe3poUvj5}>`4*dd*dFfm1Z6tYA#)T2JXl_){5f*Qj{y-(tnz=2Z zBpP$T{=UT2t*M}GU+8=bv4?_hX0x@KaJIm`l(O4xJ{kSb6NUJ zoz9K!LL{?bjbTuzhVcQbG(xE64#R`y`cm)Q=fMmVKyX@9EN9e6yDc-1CUI#~cjO-S9mZ@&-UpA#nb3Zm1SIVrB z>vW46uj1RT?uE=Hk<{cf75fM!tBkgDhNMdC1HwsY-bdYhFP`(0hrD1rKpe zcv!AKi+aV4rXAKbbpf0SJG2({diATDI;ynW)NTv45=O$mTmB{rBd5!`3NPYxl1k`B z;V*nJW6-E`EG(aP?{3~OdG*p!g4l2hwM%Y4B0F-3_*K0kSBJi7T5h+ANNK-eidC`( zUeP*evxfI(>b_~hnDYu3TlmRmLge{v@D(M~MWoGhpbB<^r)VoebkbJ%$!j=&xzOJz8HWl10>oT#8gQ842M+?ohjeT1hAW!zN|@{u)q zaS!_QAa!vczEPO`o@WQv2h1)gVc(4hMV}3`e!$d)Y8G^B235b>TaKSA@s&aV4mDV; zwOoX-Reje32*DTB6YS+28eyve@G&=K`3>@m6rkO=UfzIxmt|h%1503KDP0wPWqMGx z6O_}aJz_9@C01=3ytPddd-!FP*Sbc2Q3Z3VN{hDNbeM>$gieCH509RYwNHmGrSkRQ z#kMprevkt_zvzWU^bNwnEd?%+2Z2%&_>4F~^o(6~&npO)q|qDhAA5MC%*4P3*4Qz> z4UQ>z#2Z28%#y2Rj<=j9{Y-yE?U}P`A0fMCT6@Dav5TT~!hEeV|GlT~E~sy**9W+H z{-US0>Ew?UvWs4&3fSeZkrs8v^iJ(R^dK zo;69s^t;1{WFA@OVypjLDL<-FoYbZ<0O^6I2H1x!6{1cP zXCkO&W3N(ZGk&n=b4>ygHl=K|TJuI+6^m4Mz{R)f&3a9L8ZSnZi>;kkhb|;( zc49_*@VOOT$Vk&zvrLomu|G6skr29TpoT7vd?*pOT9s`Ea@D!F4-@HmqBVaLm*1yM zoRMT@?O+vz$-(*_8M26CjRvhJr$6^(QHdYriSKageZHEu)D`QMIhR|D$*!?Uq7#(5 z0T>IA%qX}bD+CXrOT*X&$^kLy*0EFWt8JF$^P8guN`w6@Aj@iTfMCp&a2ry6E{Nds zj!=Rxn6Fn0{?@_*l1cXhgX0WG#J5A{E>I*9bNldEd%PY4q^@uR36y)#xlk+z8bEmK zfvFBKqj9P`+{YopvrukV!Qa?9`%J%adMe(}+Y)6jFZSn#1N@Hj7wcrMokkQHi%d74cEa0Z3%;c&ToY9>n4B6g?UKSmpDCg4Bzf z8HstFB4ID;VyBR=i4pPsXs+Cm-$Pjz%#RnoL=zQOBI96CGwMy=*EgqSt(kjv&n~N5 zFv|xzx}?KLrZLts`jjO`R*w2hX^7fMWLY3JRG-|pj-*U{t1qiMBEk~S%_Y$(tKNdK zz727|VW*wyRW%x;s3q=!$D)1o$*oN+B3n1kHl>@H{_FSwuU#YOWEDjtKSR-;#~9tb za-}q}7G+(dBpBVbWTe#Evbk@driIf~!7`=l&S%<{onn%pK5-sCgixHggu)=$UYDhT z-sXZd4++FTHCg;!oquTue9X^C{C@ppfz_o|gRdZ-WoHE;#*VlWn{P+H$6Ekr(VWi- z1-7Nuso>L_$xh?$;?0V0ptfwL%&-h?z=Z2&v+s3j$aZd9dmMdFHJBpr=_!`q_k9JX zO>phiHm-*FVuEN@sj+4i@|-+6-h(yTu8BT>x+zr*Wsef!BEj|uv$|fmY+=p>={F+v z0Imz4-(TvWt%4$5l#GzvcE_n{K2ELx?oYQutBrwX7>d(@Z`nbb*9eOZB>J+4T|Klj zzzBqG*@x3KH!g}6cj1X2Mdv}6KHwk~DL zcc5H`<)~shjr#24{s3GJ zCCEjhT*;7z`_b%*ZF`Mr zcZ@dSw0b$sXbO`TAY@rG1f!$;R;Fr-n-+uwG(<|q7?5Xz1Xfvsv9$5r>4nWrGLtUl zAB%N^_oR6ky&1snv<-Q^enaWG47;50LIU36&H@lW(u2Mxu;PA{K;CM(sHR<`W|zh$_fI2{^x6X zW5wTe%4}EkYZqIS&g~!NmUc94(^KR?H0L5#fjySF(=3HTQtZOIXD}Fy!kT9g>Z@80 zM^0<<5u|{;9}k{mBF=2iQa@?rDL99f4kr#ybVY=ld3dbuzsSll+bqukf^V|B;U#*q za(|8QiB-qC!sdd$QRm5Sm7n~rMUg>r>phWSR#i8tssQpt435|)Y0xYJz0F;A>~ck- zc~9SdQD(X8C&+o_1=ODn{+Ww<&l~x3J@SW`73Ve#qKE(7K-Iy`_#e$>?hWWjE!M{z z!K4{)O6L|y>&n|0`es4E>#)kt{35F3d?E0|u3X=~sAjT9`#g-TY7ObcF96%*i(CQ$ zgZ|`q)acSbWyNJ0-_zVi^H}xUPUWabA};nc@^UZEQpI%<7GfdwsjBfv6rZQigpb3s z)VUtsWSyPQ5F{R;y3yQZ_I%+B1Ci->P!F5ZYCpkJ_n0#yx~oF`;nI`ER`*DLgrN=} zci`3!?G7k*paw+4?x9?KQVH3O9Z}Pk>h~iMzSV(1dkkvZ@=n@X7G2Z_f6sN*6ny>V|e=GmW;;J->vRn|>-8VY` z&sI@zU>(6w74wqMlnAz746-9R-StN^4SxGrIi|@fwx(-fl?9#W5uaOGo=SOiXFP^h zr`v&&k{=_@tPO*pO^QFU#ct^2bKH~JZij0&>WiIq^{+cNykKP&0Ri}>JG#s$WpOGY z$f-MBo$$F~AHQIPEZpeE~IpKq8EX9#%tF-vB~FYth(R6Af`~3=Otu% z37KvDiZr!e9WP-ksGU>=63hr!mbeTZdZM@MDoV4{G>nYmeb&b0;rnOBS=4UvOFDGfBciqoCZE+5m9oKH~cy{)xR3r>nhzG_j{wUG8;!g*PE_8YApqCm%0} z`qOv8a}c(COog$2{{!(Y@wkd zpN(x3s~%(@da2$~j?t^u{OW^ES$KGwJ;ASPs7r>}KjNEyp4$q{)+TvKzH+2#D~Ak3 z)Y^g=d+w(*OoZi2F?@KG$9Y1+2xYHDaeVMcXJ2LeF;)&rkcBtu`FRGc*3!8;Y9X0- zk#Vi%fBX0_bm#Yl9Bg}*eH*)ZtN$kDVR&4ZWT5S9-+WzvyU=jfHP%gNvG!sp>|%J? zHS5`HuixH*>SptV?C^xTenGf?LE5y!{a3rJ3B0-vy6zqv69O|?gbn%vH$iWT!UzjK z-iQ$zeO)mB!2YvdztW+5g;_mr_*2(jfQ2>xlyHvVuyY8=%$+|KYu$Fw$enLF=H7mv zYbsDdI0tJ2d)3VU@oyVtjAJS;)&C8)0}BSG{lEJoq-N>v?BHeVWc~lz^Z%baX0-hi zF`3G|Dv1ycgzeO=M}*jk27pA&7^(DzbJ&)yI~X0TvwP8iPjj!)EOLKCOqRr2mvd5R zs;K8&<$tNz{nh7gH8-8nP#^@UBSId-yfsOY5XDH%W@)OonZ?LBt>?lxt($2*!T7$I zppD1(6H_1QTvHPpNl?-_T1~Ac@{Xzj3hSJKJ_A-?;PI&oUtxSRV8lc?O)abjJb^-V z%1t#qzlHLso`rJl>xJk+(y4^g+1jB@hwRxntkQ^OTk$h7i-z6p03CNai$rONps^3A zz7nVzR`LK1Go{()gOOs#X1ulUy9M9zm}}lR)a1g85>>*gTFN-C{v9z)+(}WkVu*6z zVb$DNmIRH>9p6Wp{o++<_-+y?J9t<}Cf zId2%>+1)*OX=Q(I&z8&Zo`2+up*DNT_1v?yc!hv{_v(xl5XQy8KNRN)S+li`a~b8) zhA`Z%rLuW?Kr?w9c&`67pQ#4mG1}E2?Q@3JtDC1BVXoK}9DUZ0jP6`h9p25=%;Ri2 zI7X4qxYgoHiW+%Ud){=9mf-NXPci(^UO)iansgqxyXX&q0?lwee0ltIV?OtTI?99_ z*7{@4;Bm9AHfrCXDCpis=@Kn~&QTMV&#bbNETr z9P4m;B;ELOR64JC8w+nV4DGXt4CXU0pMH){k*X9R&!x!4&G`qsYH=E!4@F-3On?Jl z51{Cl-7vT3gmKFhjiUCPxNij5K@!ZESC+|9$zU^)o929K{R*v(Y#vYFrIgK0fGyM# zd7ob8&H56?0m@dK!3;fVr1mt;8dl$E&SpQSUSO4qB_7R;iYL_F7Kv_rTa)bOiQmk+ z`Ztut3Jx~B=3gd)3-jMG5%zzY$hHy|8`cNH6be652<-t2SqP)BTHwJ7MHIQ2f>dO! zTN&>5IvB#J)!TJ zKiT3gg{)kL$HA-wr@cGQP4kO-s$1Rck^ZcN6N47JY~?^nDz=?jZ3K%#xN%{O=0#Bw z&6Ks@*9^@kud7qZfyHgDmr*k6vQs`b4%08C0VN&;bURATnH>32T5X2(CDZg@2h@vl zkRQ^=S2O=W8r?E}v$RY0FFOoj<|wASzS<@T#m&;#D=4JD`dWH?3UOtPP)H zN205vk$ha*EyIDYL-zcf#lxE0%l)<iiaYWTo6p?lFJBs=cvFxOcM7k{9xx@tELI z0(6atkJ3U(Q>M@9<)=s&Uhx0iCZJ8@LM;(jFpJj5?He>14|>O99*70!;Q7lO;daw> zi&qkzGM!wNl`Kchop@Hs8x~J>s7=&#Xz9?{3W=M?IzW!1!@z%ulDGVX{Z~z<2u;uW z4h{zP2lBt8$^T4CQdd;o6hZUP$?O0)!DGqw%Tj4j(GAILAzVNQV@;OX5g*TQjv3{K zTXO4d+P-SLRMJ1qlg^WBdR|G;(`zd80>4`AY;rq~Zxr&qq%{IX6Uvkfs~D|g06SY6PpW=^J&&Mqob zaN1FLx|IU6VL;?*1*Ov5ts2QjbFmGHw3(XX>Z!;Kdxz5ZBq{uyaWZs_@ zJ9|rvac$=5Pk&5gvxJ*dtcj;=%H&QA1zrj(evNtHh%zUpOTlYHno9e2nXyF-syPbV|xUNbU$UYkr zj?4qsd(M9wDAihTD7H99#{H}gg&X*W;Cb`fRS+~TKpngWb1o(}Ujd+1!Q1;u<#)t21< z4)HB#eg94Z4F>jt2nMG6Ur-;8|0i0|y41(g#QQ|A--Dhc zWQu7vkWeQC4YRWHTjyj+MolzNN%t>5Jl6FO%AwCN7rdvuq4iqBM2(jH4tfwcWxxI7 zwd4Nr`+d?GY-fy{?rQMejjzUxWwweZ0nx2%V!nccoE+XSQ9`Ny4@QbMTaY|msjD|4 zjA~{0?~A$Ic&-6sdJt>w5S&y>#}I4z0eyYdE7}^qQ)THT^{_M&C*>Tsdz89&VVS$v7AFW@z`~t85?!n{u!2;w}iNGOZF1}#lCx$z>`V3bjWG5Ei?ro zIxB-L5K>rQ8f(ON6iQpApv5qs1$VrY+ai`9YCRpZd_NX1pXpOO$Q2(KdN{|~e2P3; zY^z8Z8kXH2i5ZGy!KG5E+3RG6IY%*5{z7G4s8p(=14XcvbMpLSjVyK9G)tOQD=BVYQ`{pL0mLj$# zz3TzC7}y2x8i0;qHGGN20W{@QWyHIQu_|G ziFAV_bWSYA9h?-)wO9yE+&5pRr_Yme?g0eoqlKr8>YyPBU8Xchx7QIV$-59mph*f- znUqx~^~?Fk^_52Bi*8jaRSod)=GFh!VljUopQP5WF-o$)X|y zFZ)T3C3J*Sf3cHhmVQY&*S~C(z#N{9i!E9t4+_bj23_PQoVC$wkHYnSeV#{zue7yE z%)={8+h7+p<9IHDMtEMT$K@n1?PGdJlhc0}Uv3o?yON%ahUB1$M4LEd%Fcn4DxE0g z^pOguI1fmzOL76Ic!vNuh6D}8diwp*+kQP8C*t`+tu{p-wPPvj62z$BHd(@NEC4ql zX;D?A zIAeBHfu`vEU#V_LmFsW|xvEy6D>b1?189u(78KVIZ5YGs8Dqy}f8uw9@i0?L-cI6R zMY?6$NH2skoQ>o40vZ;fITZ{_I^-LDq2BXN63!LrN`T<8XU#U!u-7+rR~q;&D-%Ma zqGwoZJn8dM;ugvX!iTJbW)k;Iz2W?DU&CnM(e$Z3;Xz;Vb}V`1+;jH@O|m-d7zR}E z;+YPi)Y426iN|e(NUVBmiXwil6CbB=tc2+*w^@D;)JtCV8Mkq?x)kWS?m;0hO6^BC zzzK#g?QevBA!k-hdx5$3N)bPwKL%(zJR&c~^yI31{##D}AM6xntiR@S0r>yUoc@oL z%{EO(^(7Is02G8GHWBC%?U9V=GFsKaHHt_bsEVMCkVDvU*`*eu@(iV9ig@9H{*u|q z_*tHSXKCJ>GU7U$rZ4FhZtL5dzWKe|Qzx4*%En*{CT!780MADgg>l~8Tal-WI5VPh zcEfoWRcTyiI!m$PQY8jv`;Ov$`p_m?s+SX@sh3XWi}K@<0?krlIBmO*v3Wu3^-I4# zD>GoWYj1Jq;ITWozka4=g&JV;(NQ@SvS1Ti8Hz=5H#xkg+Y(1`ZQIY*s^&ucrhB$L zaWs+jP!IZMG>}q;%|i*EF2ok(XO_?@wT4IG?iBpx(JJg{DorilmzUu-v5e(#BfZ7S zTkM?E9H4`nTJA6fnNu1kO_Yppg^`79zr;=~k2AwpuxvBtC2zIh8H2(t%h<2Dsj4SBT2fc{hxA%w`@&%_i8pPktUSu1lmtlCKX@I) z`pL?6cFvTOp=s%dyoPVY2|&TG*;nL@eCrR)ls)R2#@-b62ogV3? zJS{a0rlYS~xojah#Kga(pkWe1OQN{^Ev9;+WD;aCRPv67=tUD2I_^ z>d3)8SVI_mVqwFi3qnB?{2or?@3HmclTE0RPU3+rkS@GXM77M-;Pkd|n*SFJQQXwr#!|}D$^36sCjSv=C-y20vtorO z$jjQ;6eSD{sHFRtP!6b7sg=p4$!Ni>wZ)1zQhsVYuHXT|ND%hhlwS{&& zpT|!(`Gf3{)N{*uC}ldmZ9{OOzLt28uEwq^$n#ZYxw{W*4V~1v?$<4{_uIwcOv$+v zU_K&HaXt)6t-hkxHpiYUIC?)2`+TY^X=R#pW>)jT7-A_jq@O&A@~AgZHehq@WUJ?5 z3J@^ZQa!dT#xrazi;l(fIcXl|?G4iR7da&qFA}uu%o|T+V2%}xaM;|WEkK1rZWq`x zUCoja4c^^hL&XH{(1OG9{UBMS;t}J%IIQH8Dc<->r{vWZAzG{3gC2y(!&;yz;Q-pkFu(4IPMy1X;aJRl_$Q3!cFUuB_##$IE&Dyr~z zTQ#O+sTKLF#?Ea!1d%vxXE+^ z&y?2v!q33@(^w_h1QuO}4z-`$4?j!g>66WyW& zomv=d!-;=>d=iB_zFv$b0hz<1Gi`GMSR7qEh(+oSku3dZu_W6Coi@fythh`&%=Lzf zvo<

    {mNZvi*pZ(tj;cAJ2w&98#~~4=ZK0SLE1jd{@~3R#$XeZM+}}9%g^^T-ZKq z40YC18;)n?XiJBZpDt$~-k*q)cYq%vb+3ua!&@c+%4rcx?4z>_WEi9huO zttu9wPHR%d8IZ6O(Ka@FT0+b_&DznK^YCkW8q^C zYq$!IjE#+`zEg5}Q8?>^Fus6pULJ9>9tPe|B6lyc0R|8AmWpf0bN1&K7iKom?M}-I z6l~iF_G-4NxTM}17M}f+%1eF75VuEK*hC}b(8OmJKjf#q%5!a5QrP(*5BS7M_^GBD zfY=SPK*K>op!y(kPuZSpPu-rX1a{AG#O;9~D19buZvHn5w$9-DYf$_)eLVKoNQ4H{ zH$KF8R}3Mw`FPs4r{66s`{vLEd+8y{t~GG^-JPd%Sbp&=c%b$lT_G&@X<-f6exc4W zU>|U>!nJ#h`Ai{CB}ksob9lqY#p&RDyfwd3_yYP)rKYwEto4GPJ)@JQ(xvoSO1c21gj7!SssVO(;$rq4JL}jGeXlmRfl!Q#n3;ATXTdTLU zC8(_PM*7*3>y8f2Q^f)?(a(&f;oXt*jX$L)sDMG$&nha-Kc2x>LW%o}yUw}JJs8b= z$A>WNsCi0>`x6J*!Oy`c9=F5gAT*>|yUyKnMMmI;WRu@q#6%n^|2P#N^W+0eplD!y zf;fEOI5NzD(P9@=xy)vSL%!Xk=Ug0vTm{#y?PVoIvgs; zusI~k$L!7#cF)fjMZ3?2Q)R`2Spaa*PIsGZ(NDGLNtdFU@+@EBMh1(wA?tGNjPLlG z$lGM=jEKM7-w8^j&ntxbo2mHHSl8$|j5ukia@`aOFbzTnn}_|3aMs~*%4Ph*$@;8mNqeErYku*KwS0tav)QlBW1p zjV8Qx*lo2@HqXh949NX{eIF3URccr_yi)~+Q+leM=HCY()KyevxqW}gsoCrCsL08Q zrY34jI8{OFfmOZC7os7d5i07$VFZc918{sU5KL2PpBR#`@({`7qH@tkLdA}*<5#n7 zhPBz1u>F=R*OcDj-qoemREC(eLOc151{s#pY&d1ATP->VU4mA4ofYww)^e|P=1hjV zr_;g<>xPhGm0-PeIv4#Gnd{28UCQZU5v&Ti>!eE9$Lho4Cx*S26GpD zq?EmE&@28?sZBjvW?!@5)wR~Y6VTb>s>3nrj=xH{J2H;-V&%qYr|2up_Mnv9XRiEWQJT<0NaUya4p_FvtWW4!rw{A^MgqRhe%C1Jjq_&)1Yh- zT+PC#Rw$6>aimg4eF&spTq0amV;i_fxMK@-kwA?R2%*bCOraL+<0XN(%tKcTO;)Bj z_|v+%L8uJElS7j}`!tPuG&MZPi2>1#s(3RP7Vzn!shtziTyTB+;ESF8I(YO`#wBkf zXQ8q^J_U*=pO=!SD2;2r-pw24>MnGlf^(~9oy~7qEkoLl2$@)Tz*v8l6?&l zVc6b$@OqOjb2WtX{hl@r@3jsMI25Hg@z~K&A<-2st8!-isiJacf^mcwYsrUGs!?o^ zrCr#f)Q;|AmUU1!qI`z?b8On@V7z>>#jCvw)dl~~_Zsq}Zdu6-rY>S#X`|i5Pn)?S zgXi!OC49X=n+?6ZpK_?=yN*14_#Uq+o!F|U1?dT;wp$j&f6|naUDC=5=C|JiC9T=} zd9H-Eo1>7pq~Jc!`bRMk}0vuLCXmyY>C zFUitI!noY4!v7lBdSGO*p#DW6|JCjPbG}Q$^6!s0yU9AbIIvhai`!V5+yDJ0v%80> zi-V;bgZwm|;tHM8w7l&11~9OH^{L(EA22e1QHpiiK@S8D67N7=L#Vl=a+KpW(o=hU;P^lq(7zF}?WM@8t}Rpr6J{y?L^$u$ zKpJ7R^5Gh68$)h@I4oQGhPU{^!t$^D=gT8|B>qLM{?&Ece!eA6f> zGBgWwW7}>_;enA4f1T^gXl0z_VwU`+-dMFU^=l6CU!@?ZF9VbI7u5>(zxa^!e@j75 zQb}G`Nu7y}lZ%yu+0I=swh1MK6-)AKUoj*+RgxB_=fDC_C47vr%4UVequ`RZ3G44} zxTvq)G`!OGs^hOGYW8zm5H+wShe?Q+*x}uMFcg zOdDtL;r`e5prJ{^@YkCX~bq06}rdX5i4J!Zd9} zXJeh?7&l^o*~&N-`g%gG`~kd!8+d;e3e|RD3&%&kwY2#RCJ<6E$04;$9H8PfSiN(Fv7NSL zO;BELOdQ1a%O0csp)08Ilv2x>w1!v9@0@-r#sG#b z$B(qqeATYaP1$W^3ie-PHJR9Q1+EG4Fka4^Pd9D{?LjIz6Jue0>d=*39} zeSR6vuEPgj+m%rHt(T?TI_?O_rd-_tcvf!8_KrtWag8VZ?dULz?9uUS)o${uKr-SS zzeY!pyec)d>+)Vvf5fuK)qp!=$zH30t>l{|Gn1ILR#_LfxiW*HN-e7e zDvdYow|&9FsmD(BX!D=Z;FpW4G3U*F(2f|v%oIN$ns;PnvOpzJ+bHXoYl zkWM8wQcrGzR6ZO4@5GxUb}jYM>YoxUKs)sjxbXY6*Bg0DIV>wjW@qygR#hLpPdRZS z=}usDXEZ;hKBpSCt6rc_eMxi9i=|+Vp}cgCsNz z!GPb@-|zGHe?CyGf1yI6Q*Se;NH5w6y~cffM|z84IVCoO5r4$26@tGISPr%!D#{c6 z!b$m9(2x53h3Sn|?ob5q8@j?~xf=m*m`7vN2OG2L-oqgm?`HE)j_rGpt=aA)XD&ID zbT(v3)e#2^Yx1s~7OSk(5! zEv?ehXSf8`6BalW$>JNjY@j$!{~;rZH0`WqQ8+n)(_mcfBUY6%4p8V`w`w!c;{$K;4;PQir3R&zeAR*K@6t{q_a@ z%p%{CT}nM+yO}*miKc=VmP%gkC)(`3DI?!g?ij=f%ZLCCo+XOpDBw<*~Y$Ks2?Z^vwSAAIr zgK7&RZkgAl0ZGc|IMSW8D6tGUidnI?4r+@>MEctbB0_#Zq)Nt;fzCsWmY>b;2x1=-Q(9}rFv<3@qrY^?3>sJSDmbq#i z^*?NB;9{o1q^khnJ z{c4n1N{PKob|tr*;h;=q=NSyR|Dj={ofVz!AP3Nxigr?}#7D7}3Qtp8(g0mV1MXXo zlv`;sy~40F`J!o^uNe5jZ|LN@z#y~fs&e6^ zJM{_$Vak1L4KJ$Sxr-`BU*v~apvrw^>oj*YicRv-)l=4G+WGNib&{R{HrDM^^tAF? zfPAWniWJffs?X#yvTJXzs#sW`=^(NWz-%r`0d)X}Z;oe2 z%cK6ogC|vxVwvh%v`!W?VO=_h7aX2P@P^RKp^u+g1y}}}HhNs2~0>#I2fT3N<f~;GXs=+tFAuxVWftD@XcS*R{jC@>V-ah=Bav>%Ag2 zFj#J_l)RNYt9)@dG2`C3XEf*e|IqeMQNA?Wns=?Tt5(^oYL#uxWkqvL1bK6QV>-nRDlE<^5D;gvY|*|>QKV( zkpdAZ1D-^2S$bg+kp%@CPOg~}4|}?JqPn}An@sku##)Lg!-W)BYD9eLU>Ajb=ba1@ z@n)kf;+v$A=7l)~+F$HR?2?oUCHBGV;b>K8K-n4;5reP$<(Eo~NDCM3#n*b{mt@ug zGM>XWli*;c{gOMV7Oq2A_ST#&3%edv!{JILU-CO$^${9UAwnm_*x~z{W)VvvhRMA=fW%_i#$XYXZDAy%1xiv>zLP5 zA-e^SYbOdvm+^Jw3kh;xg;7o0@1}ScWv&_}mkW@3NZ4nuugh%a2r$R+$$$a1PDD?4Y!JnvScMt&dk)c45T1g0ykoXEH21<7@$?3 z%27nBDmtKVl-vAvK9`oi<8^M59ITRGe=v*;!`#HQc%9hzR_|JRkV0sO>X(!l)nIRb zSu@4T3XsGhT+24>*vA2efndcPY*KPjgIYkPAJd&ZiW!B%C6|yhFD*(U_oBDJId`U) z{nOrRGFp(zoRn}OSDqpT>MyjW*QrD<6PkS93Yx>Y0>s%Tef#}p8#P;78DFnm1Vx{U z*W;kpkeB@CYXpV9tDP3v2=pQkNF}p4oGxy~kA^X;+T~1UoNI>WVVd6vP5H_*xTrRP{)_gNpXl0gJYvH{3YK;%2}! zf1Nv@Hm^Qv=KiRW?F#8eb9To2#S~sT~ zfwDegEA4jmhCNhp*9X*-z&Xej)^JlMXdm8EbvkZXBM$_99=-(0->wI&KenG$2>*o2 z1qBD*EfhwZx(>^d%N`mknVTtUW(_F_bUSC1$g6FUQ%f-Pvs0WHtA$`C_72y*w=-!8 zRj%TeOk2Vi$xAtssB7JEuS4Hq&U-7-k@Gc@9|`W=wICpPGa$n!w7B+Ha-%1KYmg_s zK9Dl7q%2;akUys#k#r2A8}y=elPmmX#cIoU@lBi)qZly0@R~W%Cb*Pn2G@eL&7wFF zA%b7yC~3ABn_SpYKB6!Z1(aKPq2}s?cUfGW4{Z!d?(F5({o-KGaO}l!5e%SH9=9f) z%a5D~$?}fnWiXDZ=9^k!VQPI`mTg&#!A;BSU5-+JUm%=_I!c;f;XJ3F<~N6wIW#|& zh{W=fp#^PBRIoV>Tz>JGd}*kfR(QBuETCbD`=tCtw7IkZ->{4EwPWs}mgM&`;;SVB zA5Y-8w$dvezO7Q7z`=S1by^+FyvnlFH9$VFAX<9>Q$Vc0r} zU-(5%qRDf_#xwcR$mN3;U zdxR|~kzT`7xHOUJOvrK8L}AQzAP1Z=$_AXp+Mt zpzK7bZWet9%EJdL7W*mE6VF?LM#{Q$h(C3m33YT(wLfpRBy(y3<8Hs?Z#gk@?Dyun zBh1>iHBPXH``{pzuSwq$f}SPYH2Rs5YKn38K58JI45oA@+^|c<+hqD7o{-CT!m5|~ z#a?!Fq*g-ryQKZ~Gu?(C-^CXrc7L(iYRgU|u;U>T%|PcE$tDMKws zN2g53)XdOS$H1%%X$K`w$3Vw)EHO1rNkdK{O*2Z{KrKTvMnyLJPw8()d`g^(hDMBP zbO-ovDxmFayj<8{Pvt=W^_3MRV@Id|^|}hihPDny#t#3ovO*oj58scEDEyoiFi}z& zaRp26bPV+XL=77f0*qN!--_Ps`|P8$Mc1?3rzc$waopW{QbMh?sJvG9P7(RRNsSSyh4LYnAp|*miW0VCx{FkuhiQwQHIc-i zu-Sf(Q59e?atvL-Jot0*0_0!+)VWxLxey2tPz*Q_ki`G%hySm7b;d^ghK9zDj>2yL zsHpZI{tiWLTVzobZe-3DLryOY0SP!`GTj!zhM6WgF*3Ly{DMA^5V4yXiv~QF#415Q)5Z|||ZKPL8uf0vBw zrIG)kzfVT0$#~6Vh|zO_4ZXF7+D^8c3|fwS=?TI%R-)G&nEZW&F@$soCQw>$R90;j zrW>>$+2^>%q2h1K;$%j?PbZDB;uzB31)cQezpG&Q{q&iBL?QTPvqJC!prnY4ox942 z$_-=QMhdC9Q5CM5;E+6e?D$<%oJ{wW1&3)>fp4FT{8}y=pV(F(;q-fxNyTGbTxdoe zrZsEWL9j!^1x-tpWVD-FRYW8BPu8FH7|52Dcnz}LINJ}(T?j2fnaavIRvfx>1hUpa$&6pF??9TJKO)cns(ems0 z(+CMP*I%T|W^?yS$mI|TuS1T7mR;4sov(62>BEOEX+t$y3MTevK-VU)ynq9pO;?3O zS0k-K&?8KlR!@X}apG8`4dslyHhA|8F`Yqbxmr4CoBb?yfm!(l3=y}$yGe?eT{ggS zClP{|u1CLaSa%a8$Yv_Q&XQBA-STrs`=_nLJbBl4?uTmeGC=8`D51uqURt4j2jsK+ z#n%Z%(7P}0Vumj{#~+q9j4XjaRNq6b^g_}&WiB(tX+?6E!nrNkeS*kC zzkTQWh zb5w^)Y;~>|`iBPxXjs0#iA$T{ zS@cqe{4JE|d9VHlNX4|pJ}28ly|Qm;M7zb5z*3bj@2s;Ai7V)zYB;UdtzMf=mtTav zlj1D=yGe`AuZ({)X+0l?5B`;_Pmn-BT>r1Rs%mELWNc;b_)ncBTg6%tTN%a2CZPc_ zph-`&+ze(h1Ks#s@@FOndA&J|WSEG)MQ@u`&1mqXjmdLO|Mx9K69?f^Ow2aNzU>(s zw`*fi5D~|r;MEp4?KJl>`|-vk@Aua&x?k+AGLoX2%${PE*^3n5l4x&47T>?0h(}c3 zyhpA<`O!lZpn;UAwj?)qLo7JfblD7WzPf-JHcexBn)oHn9oN6aI_Z+5UqFHtLk<$l z=(Sx-jp2#Q+kLpyUM))I4tT`!C~64WY6%QsxTBVBDxR+HL%x%HmM-l%$2;MaCTOSj zN~fK(Hw4>-R5uDMWn701Mm_$lmZQA3gW+=2pyi(?0``fq#dki{f^|g>!rIS@5rAyG(6`j*>V4-Qo4wl)e zd=1yeQO7uBNAH!DLslCm*nB~V6Z(}9<~8iNeB!0TN&@%<9uW&5@GYPX9+mM!0(Ee! z1jME{6?+!rFK_6ctEr|mQO!nV$*H$GFX8@6#7pp+M$3R&isFlYUQ zb3KjFOFOq6?UBKuRdZnq8qL6VL!*UZ)~EDS*2QLjjq9sGp@8IZ5D); zOoD1A)Dn8Z>Y=lsLVOzLAwN0T{zMqk73wxI=Jq4Z*$&kT=)+cU#^o64oui%INhb&Qy-PJ8E#yC$f8em%t zqy0Edb?icFk2UI-e!tw=h9c4a$|yHUl$+S@?;&$-(hayZ%mKIHxvc`CiZcX=h_c`Z zodU;GTX3E}q;6_NSN7i-^2U6Rk==63fJyWbvlql)!o~9JQOINVsI{T>Ac@m-uQ!n8 zy7W&(QujqE+jDQ?Y6+UOe=}3FG5B9v0s{eo|5r{icKzRYaHQooaCCCeH+1?NF{+cc zZISp9hfQBs4Jg+%nwmArReogZAUB%>o6nFiQ&{{iA$e=U(NyEQ8ri~I`yNHlmGZhl zxQS($!h_-7saX-ib1^k_;knP=)b#ns;eJ4Pe_X)#OW{u|!ltexoOzL+j4d0OinD9X!4AmYtv33JDKOei>reJ){KKyP_TzvPi(YI$?Jirh4O36Q?#B^9AR`ahc430< z8`(s-Z{`TqOL{TCk|~AdPn!2z45MA>n6VCMNiw)zm9`MdLGRGYmoRJuyH@Y3Y<{Lb zZU3wz3JutrSf|PrxF!{EPq^yLx|&DT;WN<3Ymo)^dEPR?9kh)ccdS@K8AA|CsT>hJ zYGPyXrd_|2?jiwKcJYF}M>!=d4l$5=qw9u7@yyjWtn=?g+>QBr}_q zuh~tv85!T7 zd40dYc95+oFaTb_g=&ZZ17KY}aegRQ6smp6cx`#IdI}TQK|5JkOM^Dr5C{XZVfpZV zg21zNcvtRWCN3SrG-}rDj+V*G8b@9=ei*@B|;8T;n z#|DG8Z45uuKJ0X6%PBQOTX5p-{lVTGu3dL z^n;FwQSNZ_#%?z@QM4RUH7X2Mb1<8`yDm{DJGN5YW*Z89x^vIt&1L zDE7w2Va&vtYepTmZqfniSEM{m260cVL? zaS3a?yPbO4#1_LV)LWgWUCZ>IFa`?@{7#%^elR3)UA?&8Q-8q$VZr&SNZTx4ImGH$ z{0ftLUIhU*I7t^{$ih}z7F0xKlDrmR^m9{f^*S>=l4FG5I_TGfmftFsqEM`m-ll)4 zAeWlNW7k#$Rvc94Qm_XnM>Q|ndyyD4^uiTY9^}XwsL$Uc?~%8MN>%pqgQ2*45gSBd zLS?m=94t*!xy3Y5-WZ||Y5u-^V4TRhxJqaR)z&=?(RIvhS<+V5pqa0)u~Au8r7*~Z zaogN+zSkT4V{OP{eaDGBW69}+s1EX@YHREWKle6 z6|_t45i)$(jKC+ZDo~eb6aI#xbD$G6Ovzy}7f$*Tjr8?HCDkER$nuIlscm@RR!2+B z?ZsG4>0^>H1iw~1km!Uxw!@2KA~Ctzq9Amhs<^LvRWf+T<4yAG3*+FB9yLRe*GdxSP6N3jmo3DzqlSOFvN zSP?min3}iLN`B?4+3i3{7lr?&a4D>g-HnvYVFsh9a`F=Hu(#-Fjr%TkK|p9+w9Y_= zr1BG{oGe%)^}J#v-_JgMTKo#~6m$V!p$~Ktm^n2lLwvOX8>N^L)oJ)jxfn;j81`&( z3`jY9%SkDina(~5)fu)Q$`1xfm{bHG*3zN;6c@gP zWrNg!D8OG=U2KtD5bS5Qh-MiG30GVUt|1MP{pPf~5hBUjnZDWC+)>T%tuf0qeb<;bCQAl&YJS_U`|q+gE?6-2-kLXlz!vfr;c0M8lcvo=UNSC+ zzg4*$n>_xaBIkjQ_pq-4?^d!z8-r4Q_nCXoUeWEk;Chn@8Kc^U^XIU}M{)WY-u7|J zcy|bHSZIx1Qui6#Z&+gU&dx^BkUOg25wDQbeH@b!hiSvM*WO^)hX{`R=miv4JnOrZ zQ8d(G{jyTgC0mH)`uNA?3+CPJJPqfIMJ$30fGvA zha_1#)%S4OEU|gc3nF)sxGg0Nf*nXtzSpE?EQ<{8P$`Qijy z$2LOlT%Q6A9r0b+GWnbDZJ5Tv8Na|{)KWXjOg)c>1Hh>bFwTWx^ZVr3{Z%*mtAE=V z3JPwK?@4F1+@JzBk?}lXBWr_KZcE%*3QgY@W*cLVBm~Q@Pz>4`AV41s;o(f9OPPnt|dQ;%{8d}pATWZ(NDb}5@>onG#WcDx&Nhf(TH zCoHgj#nFTy*ptJ1aKd*)W=reU5*vscOzW`>Ernqkgpx~p+Qz8diyVqJY53s|*DnE* zqxM~(Ras+L(4}R)yr>2&p!Y}@a7HKOwiPpRNSB`#96lKebg1Vfj^7y1VpyIY+sxk$ zw`G|uXjuxsICK`aaWNkYX443`*BYg`Vec8#%68zS{w{*Hi-6lS`~U*V|BEAX|5k?l z6CVoNvMBT@+{O@S!>9+6ph`MkYLGLCc*G<|g`*j8BxRNhr8cGd<1Xvc>*_bAuR_33 zynVlf+q9R>e$yhE%z4__PIEuY9DjB7v;o(JqAOD!>$znZB2g+=snid}F~5TV_Qgnu|xga&2HZXZF&&s0U>2E+RwNd0zbA5+(=nsBr zofdmkDuy5Wl5>(8kYT9ANMJkbHapKOOs17)!8;!L?dUoxJiz^ zoe+A|V2|bO35@knzQjY%T8b}{`#O+GiBsRMr}<1s8GOtXcWgo5laq~k`T9QJGkgwW zmE2OLAH!Z>$!6xeo6?)Z%LJ?Xnj1gXnimLsewXY|6Kyct7Dj6uF}Q zi(u$4o|LSkO?H$bnt+^^f8~cjb`xVE1#PJdvtAgf{FLY?oIo^5!=?Xa7-JVPbp(ZO zHW}9|W_gO8Hh`=os!<*s zLRFdd*oFXhCq%oAmam6f=kGIXNo{q$SsG$E$!Y#j(;LGCwr!EPqB-Bie@S(XcN@Y3 z1V2^myTyi}t66UlUvPErk+{+~{p|{8C0T;o3vwOcqF{b3CD6Y&=IJwV;RA3UpiWTc zg0hd=`5T0*TPBY1540l2dWejdri1o`l2GYajgl_GR6*-P)%*%yUfJ>tqH1A-jW%3N z`Io1}x-mc{yN6JfMlA}~Uoi_BIsLEI2Wf|&s7f6HJdT9zF0jWJgeS=hEcr=UmK7k> zJE%W_GyVIe8+5TGb3-ad=)jUcl)36~_40Zq0Y{cbFar&~_zKv>&*G&X+3;((hukq(b-W(*^Ncr}Nb+j4|J%&Bd|%V`j%l zY3SKNJwDY%X+o6L;|k*-B_jAMVeG)$;EzWR=Wv{iZ6%gH%@D$6dUvE8^GB+Mfv3tL zU#LeUEJ^>&wx-XrJhK>p=fZYTed;Sz`>yIEv4G|(_P{jyHYoB8&@TO^^gwRz;sHP! zNnLn{e2$PWlbe(dLPk_ka-`(OrCa#r`^dAddyZEy24c_qI}gExmb|{LKeJ_B(sP1f zxr5IQcbxVOx4q-@Hy^7}(G3A^%A$0&Mi-AC|D0oL4b$)ait&U->;8#*>44#Afsnlk zqQ=48DfpgY+IOU63k{|k1umDrw_}UQlNC|Zu2?7C?UGPOkaB05$9X><7)5%+kY zqdH(HV3rDrKTGmTJStUUJ4uv+J&^r*Ecv7&QK<>{a?jn26RDf_J!jrJznNBS>=Qb5 z5}M=p?RV0^+W39xZE2_}!DsTRB^!5K2OTGZPpG>c&~8Or=g66d?*X!oH+i|~x@is9 z%9CjTDQA1`>@Z2`xUy6Z2e0>r)D#JoaLW%QekMtF&vzO7dT8{)n=*4~l)$rCkFMaG ziW*v@1j=5>Pv1ZjW&~Zas%b|i8Ui<)#Pn1OxQ3I+U3VRw@6)Ysnh(#1zirlaTYt5; z00sgAf%q@1uKge25U|p>u{8eAI?w;Cb~ZfyAdnxRT&IjEe6xfGFbgwN2mnQ5DzY$u zBY9)mguhI>v|;4bJmh@cOAJTw0pd-%op8}?8K=QUdarXl((*axZT)(EKW7E9>QCe< z%1;QifM0@=vWAf?!v#KdI1J-6*|xC<4(_OX;cQ(#xFfX8I7zQv2WN~;L-DxcWUL^> z0SZHiEPv*{TK|vs- zqi+8*=qK5|;fSL}lW47VB_M=cb>n=!o>Ew3Nrp{7T{r;2u=k)tACQy`3h6(0aGLcb zO4{UNf9m24QE7@*U`>B0LFJa7r=~g(rgP((m!F0-bn=m-W9LJk7l6QlxM9WI_PY)n z?6&`>D?U~R_tGgSnxS|aqh86lAh3?*5A4*RdF!a8Ec;f(KYeQdICz0XkF#2-Vtw$P z2vUdSPKMBA2g4dcb-n7^#`i51%k2xefs=nk&i0z)YoPj++KJc`v6)oz`hV~dg0X+d zxKBZ(!P0R``C*RihavpAKiLyR2xd;d&L11rF?Pmyq-LJ4qCESdwRT#PTVMpo_#lgB(l;uu zo7(6*IXf8tZ~MBoDzYldCMzZe13VFEB41>@N&pdl;Acybl5kne&+2-)rnN088hyKe zlo~y@&~;P^xm0jH=SDb%ypd3zPVe>^#yrB6cB_? zHDHULK5Qt;52IyjNMZI0A9wH+)=*`x+R2DehQp#6HI}B)9X)(Qv>8dNJ-mw!L9(e( zQ?-{Iv<4sa-9UsLoul~<#%6J~5LtDlE^Qg0tFwmc9HUcf!Wq{!VdGuLUOFGOMZA#c zmZ-Z;I1GC{(h+WRbsvNeJ0odjZL=KpK2iF>9XG^KPxLabJhJOOP0;P#W$V|jZ$F0e z_|8*uwH&Ik!&8qj)er&LER8U5%{OE9kWQ3XzQ)4EaxO}EZ@)MX-vwK{_n203Omgo! zzlq2#D1708ted2<(Ff?aT{zy)N^e0r!+VAX*f=heZZ^L92+|)cud_@B zucU4ohfRS8y~8SMlb=62%x^%Qs60#0-c)xq3Kb6LIX~q@*qZCsuxcw-sVfW%^#U~F z>NrU3L`AVMarZ~c$j;j$uZ!MsQL!Bw^9+%<&sX}nQJozHC&Y2IPmsF z+dql`eZZ(Fupdb=Fi2!@H3bC#&`tk~^K*l?Z{8^qz>uc!H89?LYfX*837`4G)|qqR zXf>2?_VrKmn&*PY?B)rmc}Zk|w4&SA!{FfH_lJ7HPh4WCd_V5YeNL8xGQ# z<8k}%_EGCGG3Wcuii?CAAn!OSPeqKeF;odbwu(+dG5J62}cCN<8IY-&%k3JKfs9u zne=AJz^P_6sN z;J>3cRfA+%IG%);qlFYhHfUnJcF(V~C{yHFEGNR1r+wrFm#GRYNQGrYAshyJSdSA! zDj;Udgt|y@KrtpNbQQV*V78jwM76ta@Fm44J7x|w-u!4D`Mqyr+q9^huG5KvjM}(% z1gDI1y?eTQyPw%j9lrKf9f4GK?twDutASzcezC5DtoXkR2e~yc`u(`rGja7RK>)3T zK)8D#z#IJmE3i8&2So7{fdQX~wLV7Q-#w%$;@%XS3Ce0nHQz4okO-~E3u_~lqroM? zxZ?%{K}WEc7Zxha^_!1DNqBKc*>L?&5=CB#s}f#nA(Z;Bm?3Jx`A}7;o7nOtF$Xce zVk_9vVWmw2W5vEI?V7VnX6;4}FsC4iR7qkQjNMD%F3Uv013Bf|lFHB1dj2~(JgF5L z5>)njZ5E~+(_kG-A;Qp#H!IowwK^J2^FXvBJZ;tJW~f!B)qU=6aYHtainDj7O{FF? zig>&d`2{6dYUA{Z1Qw=qy@jO5{67am4}D=KBI`ZH7h9cuI?b^Xd2|xw5z%}=EUEciSc8)3h-sJiG3QUoZF@>F1p!B zG{+!^UycOG1UEC~P$|!ZER@~&{OCyXcE>1`Ozb69o*qZbsj3|)Pt{06^6NB5l{45$ zGK~QRNJ^w^=D=eDR`hi~0`iI}DGSm+V-@%$5}EWoOJ;2a`aS{5^+$^G zAaBxBtYvn(K|j;NlAT&WC!C{8tO*fiOW=ez^XMBrS}QdAHHm|6+k%44;_WCkPPXE1 zKy#AnAPIhk5^)8W65|XpR$=_SrIPF*;tD+`?(!r2t&?ayE1Kt^#Nw;ORs)@8U#EG;uSQNy(!e>Q{1NaGby;~OaeCr>C za2p>KdmA3MkHj-*3VftZ7zga~oeQDR_k<0t3&eGf)CkO?ENtFN87C7eD^BFx+~&gy z7o)JNNWVFw8ScPdrSW-(v1&l8stK;wv5 zdrZ8uPoJozTS@_yPP~dZ)j?&}u36XN#{292)0|x&4fn<>$??QVP|l3%c#!cjsZF~9 z-fL|RY1Rf1hmc_M($Hq4uPtOXN?WQ>A!Bdmn6Hx&a6zx7eBklNa|J`gv)TI3Y*ESe zOjW#_gi6}dkU~~g%X*|Is#&eEFIJOQpcsu^GRnRiM=@4JB6rU73CeYMj3Li>`1_%9 z3W$^BU~z_AsKepn;a;@lqRwS{JY)+kd(q{h5`evo`+=_AYUFH2uqge3NgYf3b4yOv z4e0)KzaqWoY3u|!s>T9K`QF<4*-2iJR$vDvV@)Lk^QKSIThItvpy&~nD}K;a4$XYL z?;Y36bca{C5|65+1)j&Em*{i8tPc}3o}as9Mr1B5Fo0MMe1))u=CA9zUi5+=6F&a) zkVyZ1zbV!goUnQx5xrHq&5oAIx z(T!}O1yaE?XqPeRJQQ+!=vD~{b4|+_rtu=N+Be88EC&IGm>|XN`cU^0%{fe`t0Xx6$SrYI zS&4kJ-Q~TPq6@-nW03Yp$~B!aFPPCU!DQbO z@ZTXiShj=LMV}B-ALfb|ME#d((a}oLzs0%Ru(@5TOYZ+j?&nxlm)O(hyFqdbvungx zFOqc9U=oU8#PFzA7orv_*JL1?Xz>Mx`A}p>mZ8*(Bhu*QP`^ODVD}8s4F~7te1*hjv-AdRKpw0CrU4g#(oBJa$UZQYRe5bo=@xBErQT0CB}HkY-x}!M`&_87w zWoeAFgUYjL(S{@DbaJiZ+R^SDrY3dTDq||;@FgUfeD#n^9CQ1oD&ax8@Iu{zb^lz( z2CzA5g-&I@mO}eEktHbZ(NVOcg3uKBsFieJ=WX-DRL~+ngpqT$sJJL2AGkX@T_Lz~ z)#VqsY;h^n_WS|SLhEv}Q5CtwkPwzbm4tk%h<~JO&(;j%^=b(LV`I&}WUZChawbkY zAwH~sqv%eVd4mFL3)-Usb9Rw0;Vv>#pQAOlh5%$=E+!1_kBp(;*%oSu-C8N4S zqj6!K*;qhk<~sdELX(zhf`$fP^`$qMt9h>Tc#i&*CXSfz0()$uE}ZFYeTeBTjy5M8 z2Mbfy5GGhi2#yrobDZgfUqApjOeS;-(+Zh|-3|rW?NJZk_HI{AGBu~Ehm~X`Liw(- zR(^=f#N`h-W3HJSq#C-%mJr+viWeb3=}Q!}w@^Q@<+o-pOcj^%wsTD9mvki!Du`3ql6Aw0IP-FpErMrhq-eMVR>Lk%DX^$? z(TumchKtG2{e!kZ43m+%Gj_f~Nlw`9^stB3d~=qvBw7RUrrsO&WZ#)^_=na9X=u&SzMCE}PK?ES~q{5z0wm} zLD+1rIV3h$Q=2moWf0Qfq~Q2#H~QFSRN57ZWaCy2qL_~!}GpBuYGL%=dw*- zQ7#_=HXjkk;*5w`hKT5Xd0u)Gj2y8PIxqipYkvAqs8}rgiFxv zu$sMePq{Z7afwff0w0SzlkkHpB%mjI*sb96Z;~2| zMJiGGU#yJ#t4ZtM&C36*JNyMo3?KN{l{g!7PytC8W|X)%zCxvPYLxZg)k=Aba%FqV zBo}>ib>W@Z#jP^exT2+uK2eM=BK7d-ZpgT%MqPLP+q>1zF@=lARw+zG^-lsc4m?}; zInVA>?>xs=KJDKCct6D%K-i7~M(S;VRSCfCPcog(Korj8*dqJtgv7e?sus*XaaR>S zHSQ5A)rgP$A2_gDNYG)pc{>tNuiHOhKfaB8)X~|P4Ab7U?70gy?=?t7CC{aJDi6_^ z#^W5PPh3^tH1pW4tI{$Qo6HnSr4?IA95bxXwL0h%TU8V+JKX2#F00}x4k9RViZ7~+ zQE)V;(Wn9K$qmeo?1|fA-JKR}9#eI=oXo~(%g&OHu#U#|%WJAXu#h3yUz-f=)$_P{ z*cwZ^3yU2x+7l==PiGraSSN%5>eA>#>cW(O(GmR99FeBKk`nSXOa=x8qha;RC*@Kno%-rWua6J-Whm4kg0$_$New+qVkKy?Wz%Y?eq?#D?>}Yo3B4+x2XKa+TP77NE=U#>iy+M+$4BHP2 zOV4cmC?COC9choej`27l-;k$$3)`H&L8Ac+0 zN&HAgWLtiuPpXk_nn5dyCIZn!b8;vtBZ)mEs#`+`o!AjdIwO1@3RxGM{qc_3XI zL`_%M;XvR9C}UQ$zA_pJDTI}I1(!kKP~@j8ePrIe;~|nQ?>eG)6?Sz4Z;kJ9k3Rae za!7L@6XRXChO+J{vn1KcqHa}?U!f}x!nqG7fBWT>9Ab`y_mPcHkop@L4KOx|LHi#( zv(;%MUyW15_(Kl^yAajEJSn8Zd*!!U;*Z~8!I z*Q~wdLND77Y=8Xiu$7Cru<`95KtMQuVbb*9j!37!8VvuhDy^cn?K~X{w@had^(wfr z+>q(;(AytTJU1MHfN=3diXbJXAU6j^+WV2LF&Dp2O1D#@xR)OvWCPA?{tHj`L=soG zkGI^dP_KbbiSX>)5J28!}mSsCRf|EecAXW@7(?tnS?1Gh>oJYBC zNe;UvIuP{HeAjrm+?%nmfqvl zQO`wUq2B3aHjdJbyFCg5*CiVm{8eqKr$iiRXN5{6DYImg$O7@<%{c0Wds>p7SeP{- zfftewuVNbU5{A+b5{IM$i9r%u*`BRv=2vY=GNQsAj)(MAxEw5kgC@woinp$)43^&1 zYYgj2wUf^yA9CB22`t(MX40Z>7*bE=%veAD-t@kT*I?_TRl-=X2xpWPm2(E?b) zXLuwYfcc+y3mNNd!Svhn5aX=@rqoq4ZS48_sE@y;{3_K7@!_=z4}O+RW`}soNBIvS zxLr@nRiqB9oyF`XViLZ*sSdd^Es+!Tk0z7{g<=o-;;N1;K`Gbyj|<~suLwN;Eyldg z6|d<)fPfGnfPl>Y|B12xyB4QpEsM;L=z~U~dPbn2OMZW1rfwhgRi;GK6`l|cW{Pgp$A6b1` z(B@HCYzYD|RYWmouWHcRY&FHmmqghME6#Z3a6O zg6rLRAFL~`r5Rkrl*0%ryv9$|Q8S{P$I+gPVQMf6#$v26 z2!3;Pn)k{%qZO!;rsC?z_dlPgK@jh9O|K`{Kf|I(P3-XqUh66_@o_7Ed@oPkyyONjwO?D!Iep7?$J@AlCfR3dm)pgz zSu%x0PJSx(5^f^=!Rm{mP9{-X0fc_-krZLzZWk5M;7tt7dh#3z9fP9^s-{<3ZrEFo zO1^SP(hTpgdTRcPKakbFBBQ+HWu{EmdWR9#iyuLJbp)QX zp7bGcVdFQg#&4V8C2?sw0lf?_=OD_a!fFqmyu)41rV?aktFfgS5kelG_A0@R#rBae?xDvam*ULJk2gjYh0EA)Eo5Or_#y{l*ssUGq$` zd7pnXOZqf^W1&F;0ga*q0V({Ov-Y3)nfmUCt%^F_YdNl=zV(92YgdO$N)%1aYeXuI zeZfRT?|iNzFM*piAfvH#H>4w~Ltd%8Cx-_%H-yy$n%`#IKB1x?z`OyruSLkj8Y-4gcm$wIEW3 z=X(ip&=BLjfw}-|laY_R45MtBqxlmPDFZn(cXdXGS!XtZr97!FLp(kDo@9o-4yyw> zcaHQix?1G1LP<`ug9e4|+0vn#4D-=31Ldt`WJhuFV^(q`uhl^+bK=2J|ER!qTF}s; zg2kEH)ukj=Ir^g8*4+GU5aaZNGhSCIS?5?4t4o+fD+balZ*5Iiw_dIRggLqQPp^;;6yq5XS~e)DyR<$%1M?`cg|nQr+IvZi3Ol%O)Je zu<@&=2NMbjjJLU6=9oa;CB9XeBn6SaIjKZFqHIAB7ALmY zN!f^UOEh_XG{ChndMA2OOKIMnqp^8l1v}-es!D(gLCqv_6ec5CDV5xgEyW&F=mZg ziXRpifFv4&_4F|HM$L|0CUESVAU@igM^%iP+Vob!K{6!~_WHQS0-vN z>tRoxo^?#(jOW*2q=kaD$jw+_K~a3@&>0dMT5eA@>)JjjAYKn0)|MH9OQFDu0|mWct%5y)4OJxWDrBc%Q`Cy3NaV z&D0fd@Sn)p@RcTQ3?z`pOi-er1AEGMth)+#V86)nyEl48?>#(p7zc0E!F>C3PXX#n zOM@42Yyk{5WNqQDZp&1<6Kt{MZs^zIZKY5e{UENyQ;l-nr7Cb=M``PzxINSHr=FEY zf_C>q#IAEl$b;NP`BZ~+tVvtJ?xypRjq4IMV_@V`sCNY!k`|OHpV3T&z#}$I4^OZ2 zjHa}eDi-oY%Qfp86hT?pt{r3H;?Z8G(WWVln1IQ(i5B7r81^=l(zN4#3zkaEE-+Yd zBDrtq=Yz2Is$}x4(4jkP2PHjr7C1}vJBy6urM%@uDKm)Pa8G<6O@i>Okz;&K`xK<{ zj1@Sg-bo46kQdnXY;PQJ6jR_3df*^|->D6xCez)ym!COLAXdC;EIxhnUqtJLTPzuL zA%Qd#*cVWZIN{ZV4>T4;Q6U^^P?BvSH1dhWAE@!O88I1O&I>w%(kE|cqTgjbQ^JcZ zZrJ?%$(&>yBFZ2rc*BBEB3H*N@C)_MBRR}_R--lE(4m*sF*}ekNP-JPx8@SwD=hqv zp=F8mv-dxSGj1U9jLGA+4akk)ezIE-l0s*4{B#lxA(yT()i7wry)K+qP}nwr$(C zZQE<@c~6|_?)lC{M@;lLKdLG!Bl1^eU3upn58^4fq@r<+YQ}L%-P-nDWpYXWI`AB1 zeyhU&O@;bKOzUe6-m|(J8~@<=Kt7gF{OgYSmk{4?U>DSv-v*g4h#Z*WiduoKJvj!I zvP{A$FgcI(L{cf2swn$WF?U}(Y%wB|VA5jpG0e{A_HWx1Y$uy=Esc(?lG81nwr^$Nl+{Q` z=oQGxlr^-~9hr$!;a|J(`bL)?az&<6v@w;nXhaG8Lfz49uTkZ6x1{#bN1VTn$7~=!KsF@j z_%2C)%F+8XTWW};KVy(RYAi)hMDD9kMCOA;u)tb%(UqH!8M^Q)ZNpM3+JigVA5dkdx{<|PlwS-g7=`4^eC~F#$~A{>cDwS> zzYY)0B7=+Q3^+z1l4`+gUHu2OZva9bK(WiA^qSB7AoKJ)(tZvt>yuio^*f>-6vrId zYuxdtc(3E?uTA{$r!>l6{r+n>ZEOmbJQ+LyfOQoBfaHHeg_xPxnmAe*(f-e&8J(%r zBMPLQHTCoC%pGSh#-6G_oIm4^ZTK$MW-HEf8A)Nu4YDP=vOa^|UQeXJ5{cKc9#aAZ%7Cri2n29c4?yzQwQSw1-o&&D9rSO?m_IkY%#)jta5p6g5Om5e~aFh zXkR8MMZ+=i-O)@`pFgAdfbu3BR;k+r`fl?iSMMzdep==Xzp%c-?V(3id=N zUk2_kKS=3E@|jgxg}Em!!Po#N?8xp$Z{ZyfB6tOrW>dfx%Mt=WZhEOL7QMc zl;`0k(pQ+AUj~rnX)R-4zYqgq$B`L(`*pQ&L7g1&cJWFmg7-HLqe z`OuCfJJ!iYDe8>^Viwcj{u%24N zYWnB^^eOI~iCY-i1x20YN?prZUx7vw(sS{!Xt2tHRQ#3H_Sx$#si4@3Vm@X4qjDm{ zg6#+gQqoX&dincv!W{B0R+wae1||M29vtM8A?GK5N_t|{FfXUHw=^U&FsF^>1|8|7 z!QTb1eD^HS?p(yYHK4dg>#iS?9+`-wDLTeYdje3Qlxa$N_*noVfZc!cQna+XN$I?o z-9dVa@q5E7-V0OFruGW2D&`ZYzqtqFXGO7p%jRZb#|XyDvT(Pof`LRT>XSL49EOfi z4_X6Zg&fASq(05{ev#{pR)i{jm?9e$6xHg&P-2>=9~HA5k;Mf^T5?6m5b-s)-A#Y| zczti=@x``pzd4SW%$q2)e!$~^&ANJ?4LJ-40{-@gokKqO`TeF%YRE^YkYD%Qb`*dH zGj)Fmeus@&1WyVXOoO`#gXE!%XT9K5ZUE8uTxuWQZL=q^WP6Sch zUG(3p2UsPq4`Yg3rXDyj7|sxNw`G9_a%{3n&--L&3OIH>x%cB{|y0+F9^5jto6>k>X z`d-746irx68zHsu!7|s4s#@P`kx$V>&nu?a2R(4!sj_vTBv+l>HF&70K^3RVcZ3|J z`ZY^o;e3XyD(AmYC*&_)?ojcQQQELXpdXIbK| zezEwbAfvc7Fw0PG9w<)}B;H^`1rrZo=eor0dS^2fP90I3w40;XsfWWg;718K5&yl_ zKM*TcOeQ`kCtmdMV#p&%uRM_#p{ z&xqd|HH*LL)uhbrMx3m(w!$QJKS90fzK;WdAoN!Y=B{M=9m-?y;AnF zX`bRCWAt4~X&()IDco0)P3CyhUmDeQ$wUcvg#KMCrT7>|*Hamx822c0;7wJ{&7UzC z*<5}}d7@PHnIt1?+2<$b+&7px%0ahvhnVB!wnKnufj>2=HdAUFBr3vtKKCV@bX7ji z6Ps4_(a$0xwJ?|TVQzxe7cy_|JGUbUz39G?2e0YhxqR;1oc(g9V=QA^jgVRWwIqq1 zh+sMJ(ebwgRy4Ai?T9|=q`c&;SOR*(v4LR*JY?9C>5D1n{vv1&Pv_IE=Ng{;YQk_g z&z1UKKhtdLjCy6*jN%}JtM){=Og!Z312jv$q>9wNJp-?KNRbCMr~-SInXM>1Cy`4^ zTtm0{@^*MOT1|Z<9UUG35{AW2I-BWD7jh1 z0*N(9hULRd#}vyd35Aj@v0?8};#6zQukCq=YeXX&47hlm z@{!c&7e1(u0~wvwI~*d(;qnsbg9m8EJsaij9vdHWdXJuQ28gUxp;2V7)NGKaidxNQ za~Nw?n+t8Ft~ouEKkPjaBaV!y14Z6>s}$_AQCmahKt-;tpX5=yllIj^J&F_l$=K7} zn{Fq4{W&Az#ovWk^r4bX1T1G>g;Y3p?1MB`}` zIAT7Ved!a-IdMofxxTGk-%q1&T`MQ2s7k^xK8W zT)NK*!)Way-{uI8FMJo_lb0;UsLEI2h-+|ZD|WtB!w5(#u%@RE(lRKmo7yskl0OG% z2FU1$H4)V{eY2W8e8JaEZZa+@zMqd?do-11^}G zy}AhGWsFq_eO5^v@|@Mh+5T2}4D4*X**YHX1+**{=$M(xzNlJpKw~Ub}e@rm9s#v0;%KtAOBzZ`*J&qK-tt7XNH2g1f^U_Rksg2L66S@ z_cM%AIOUtGvV2Q+r9l^aht0);HVZ@Nt`S-_REAjfDU_9^Q#BQYD!$ApgxXQrk`>b?ay=`Tai?F0s1&K4$QmoF4z& zoGxea+C2|v@mznJb9j$O?U4e-n4vXF8NGlZh+^tm#EY_nn!#&w1DXSA=_;G>p>q}D zDsEcB-KW&N8gJ}_@^#4eJEX>3JLSMq#XZm={EbSwu#dPh5jeRN6-a?#dg4Ux3+&4C zg!3Z^u;*Cf*2^L*)n~7Ik?k2xO~{n?1lD}P4-6AQY)acV+%y$KpJ*m*@BGi5iz7C3 z%(3DDX)0ZzyAG7psBN3XN+9v4^7^I-}b8^CtGv52mUB!!J_PM6|2ESNO3uH2a)T3AZo z1_=Mu%5PZSuUNyDP609rAp@JMXTw1SwWWtb7VlK@q-x1;%lhrAjEIXT>(M-gYe5f4 zChsrFB{~^GB5u9ij@yR&AQb{A6(mAc2Pu6E8OmP!(%OoppifxGq0jJE5g3s!;ll>b zm$^{SGnqZzYy(ibMvh4T%0cScYi?Z9yy08}#5vz(1=7?|2FFVidNQRC5g-1}FU3}& z)_=F^K}&r2R>BrLel3HkHE3A^OWDS50yO5DD*AJCwWNOx2EZPBk1c>HH4M1ZE;gWx*tVf?D_r^&U zy658eW@gUqtn}7rPTvsp1oG+LLUTtl*US*5m-dzpH_tdV2x}x%(dMG}3`m-iGgc=uxnng~+_EmNI>+Zav3fq34 zi_MIRwa;B^h!)i&wwZ(F)9GcpWb#8|HJ)_+bTWu>J!G!rEOmi>qlPrl;2K+X$8(C<&soWy*amGdESyZ92ySd6&Qe~wY^LwJ&B zX*O74yEN4z6;z#<#`ynb8AThs*{MlwWpDg4xIack*~c-$oJ2uI804$qP+ zr<}V>*O+OODEqufWPo*KuX!V-k>6g-<+>{5>^3o8R4;w}-~SO$V?|iEhXe!wU;^^r zOHmX5_hI}$Q`D4o{*|J}yOe6HLnYBbQCRriLb?#a_yB5Xh=8z^Z&4C@-@FsMRj`#| zYSx;XcM^q#`~&z)akPU5jihV7J#(Go^_uH-yUYD`_4bwrkZZ6qP#A;_O4HFEp+#qF zv=I`-4R_0QyjGI<3M#x>#e(68-ARgJTBQ`RQ@n-p;MIqTs8o5`ABW{jqx=_RdZU#E z#;%eJ?xR2fw?c-%gI-E#(*b$7(JTfcQDc+X)_cYB67cVGs+!26#!y=bQUeU%x9BRy z_KYBbr<$b+PIbKAnj}s*Z3a~898xaMr%#?iLn;MzS0Q0c@uV(QNe%yWg4(W}rWkcE zXT1<6kfg~)dVSI7nL=t9{_=uNk~7F#P=1wGH(4~P1-0gI-sP8~5a`1b(b3U33+CQR zg>Ch>`5c5HU!OwVDE{RArypyE0UeHD+?XQVEvf@PJPr@dA!0WXR5^pSY~=^tYEY4K z2bg21pRlJ=@Z^5j5*q~BT7_phgLnS$)%Vh=B8Wphw zG3)A*N1mkfCjMk}&oNkG~7d|Z}p^eqrw!d>DUl*eO0 z$jUZ+{f{-J@}#feEq3XpUcz4)JEru~GHEoHT_zqB3w4|ZdrW3yGm8_5jzF5z&}^jB ziPDa`MB1gyf*+t7D{y_l+W%)N*}tqHtd8MjPe1?wAJG4v74(14`CnF0O45|v5Cekf z+Ygj<{VaIB6S6wUVi-~wtz(jf;XQFAuEH-~mqNLe3On}HWA;_X!^?KTV!r{xIpi|8 zI@p0uB+0tk+NnXoK5~n&O><=Ur@KMVAMY`_GNm;FqHgw9vlJaT?d5X7EB#$7Qo%n> z45@!~Ysj6=Fe*Lu(QGGDHRJgvVqfGqBC3BnclrcPcr)ymAlYowgYzuaaE3$3z?mVU z?j5s~6G1vq<(int1#O#3>ZX`x%;RY$Imkp2k0)gXADD=fa?XMtaqGYcXS;HRavpvn zV1+Dv5ZO|q4^u0^n08R?z0K~XOU#tFnbilb+6fz*inhSUy}UU%@(Oev0LSLkoutQZ z8}ssQ$E>jz5lp@ohRzNZrv9q(t-!48xBr^*tSUX$8~6pbSe({tP~P-XOiKyKjC5im zO_H_?#_t^3^Fip^KcXLybRfw}h*5L^#gmjEzkHACpduR?w4jj{+RCO-D?qSUe{wj* zd4hR&e?b3Nyp*>)G9mo`fETHM&-vfsh0gl_jTe-~|ACjA`5T!gX055YN70ywUjV=4 z2V1TG4PItO5~Hv=X}tcy3!bgfNKh0v-7S-hodoF(bZE6+B_l^xCn3h^q*6pru`>F@ zS3Vx162%omxTPSCvL6UnD=F-d?VmAOUlJ89DrBkMm_)Lhc1WX*7O`MSnj1#;0Lx{U zJU>fGY9fp3gD&COu)cAwptcow(KV@>=qT04Dy|6O23qBT!#{&bMfvCqG^$7@CgRJ* z2`U@XCC#v4oQsoMl0)VrZJ%ls!UPpGaS+^9rGVi)^=h9*~0Ahevje44TdDIKdWPl3T%MgfT3Y1l*#dnfrF2ajT_Mu`=)4bMEB~NB_$_8 z`1|62UabWZ6%GcZT&+Ai&AjCDa&K}r_WXQ*ff(XaAV%XOhytxq+!%|7c}Z`n8LNWv z#OR`VhRW9vY^lcT)mn2pdxuWI33ZguyHfSYW!+@OuRZD5eZ<|tJFUhwS9aIlum+D( z?RnMx4%WZG>te4-2er|Z+(AQMocjcrw_pkib&g;f8sobU*{ilUm@3g?NG4k?G`2#C zlYKZalp+|-=ZcTz7MQftl0LfSgOxlcnRkzMhiYviJ6U-46yGD9OS_G{nVk2KQ*~`? z#Eyyjc9 zgh6wV>cnYJ#~@!}dC|A4T9tM6+qW(=j^8`X2Xh01C6j9}WEWT~l37MW>VKH`N z8(%PW<7q%Lb>r%{FeHCIO=5_Z4|2RHU$i>XZ>EmO%C9r6(}pcciNP)%)7ji}ZXWJE z+i~-COyG)6R!NADYIia2+0q<4rk5H~36Y~kjj4_#u}R4eahz|F)%X*32AI2^Vr#r| zhxW#^25an3NTec=XIOh2;E?$XD4RQj?DLRaWDI5!Yt3Lx`GS509L(uRSOF%kK5~jk zf$=E^J5`{|m1CRrkk6_PcC1316=RnJ=)MkjNOH7!9t3wew8{-&ic!tm z9>Kj3e)dQZ3uZ<#gs!>->^_THrd8+^;FMPM1&sekCB7g`@A?l&0DxJX|AWRq|J$Yi zKhhyT+>%!?eQ%A)^q4-zfsmp%!y!mB{2}+nkdz6lvFrno0Nd!3;*NtRWU@hpu9VVN zODwepNL#21*~;4${zSJDMYt|(;$Pe8#GXB; zylP>imYQ{vgBU@U>1q-<{+=>j+LX}`==hmaji`axGpgg)=SW08y&{|KT)vf}K zO%!21uX`fFos(d7j>KqW$eEgMo^(KknL25o0aInqJ7-)^6D3x&12jHtNK>H2l*eUb zHHJmKI@M}ih)XS_9kxP7Cs-6vL>3!S%Bp;n+Ytq_(M1;ekQ?wO`;FFT1kLgQ7@PHu zK#oF}rva^;z9=ow&0%PWf*+wQKHNimc#i1s5k6R1@emdeC;N@($M7KjJ<(5o_<05E zZFxBNE~7odu^qZ4`<%Zff1%|RhQH0I$+f`erc+g^GCh?Cr9}l zaYTAaGN`V0%9e{#$(l#34pqwzzCDh#@-ei zwB=WX=WZ9fYY071se5R*Cd*kBj*PD5SYyeH>M2uOMqzB*f2NAsnecRJSoC|UJ=1SG zt)JKizT1e>qaI?ZM62{fno=*7R8P{wB6VBpqq6(X=FcP3E*NxPv)h<>E)yhD!HHRRNMeebB;)hu<}j_D^9Te zGk4Bzjc};WeI70YJVDk4P+I2!Dy2R^b$w(?ouQl+IvVXLIDt7dtG1e>?c=(xMhaPL z-oH&vifEeZ3W>ZW0sRKa#$ZWV_476s!-$WF>b+j?6h)tkD;E&TKG7V1KhUIHH|Uz*el~OsRCG zS7vsx?jWbJhGkiI;Tfwo$Um*T#T{-LEZr5V!`*c?X^>uVU{OB=o z1N%CR2h3$B9)zR+IpipUV{`#u=6^FKB7K#mJNCcn?^}-gV460nS^mL)`a>vN9aADR zWR;<@Wsf|#9u3mE4`@Atw;lb{4jN`pJ+m7s@4(F$7D7K?f)ZGXvNJi%=0fMW{Bf=d z$YDv@TE)6vz=f!^HD87ktQx3(0dT7@M^yPrSgI?e-6Kk!7-khtC3wZYFZN#Ug2tv# zyv8@C>z&teQS=LyIwt1sv0Jv*otRUBbU;%l@*J5gL9VW8;2VMsyKSuhly=Tue?mRY zUauO}MEMJULf&voGATFsL^|2wi*#1UVE-hEQWkZs>v^7PoIraNDUSYUs4LwI=IJrY z+F!JlyDC$9X+>K~zfDOtrJFbL@>iIP^!sN1Wc3;svUn+xGBx@KCxdh;abmMx|3J~( z4S`f9cp*r*>~}PeFqzS)r-*g*p)ly0kMx>is=|#Dn7(i7&;HbsPem#n*zIv`mqJ|E zM#$~Ktk*^DSNOMY%&$^sS8j}QwjK?{5)W58h)!pKGeNlROd~KGa=8R|4_0p-!;s4- z&!tqv&N2{1XD;5(v1n;6QgWJI|(eWmTo=FR@Rw|Yz3T|MHWh%n>ILKWidCC;=36?==b?bY=A9iBjS z2dbMq@q+HbAF_U@Wqa|4d%Oo&|8o1|Pu~3nxhMYk*#q#U>El|2N4B!tN(+C5>u$lCT-j#LuU)MpOFs{aq4fbIDAR)Y2P6f zJc1K(B-Y^2T=4ms&J~?e2rRSh1Lf7?HgVtCS66sR+n0PvB1# zB760W_Ts)ZE7AdAgfetVLx{7Vf(!#ExKYNqQFb z$S9nPg+w-Q?#%7U^|5}j4%{(f&qcL#0UhDo)Gg$Obi4%S*3fSyfVE|J`UGUqOs<4! zbL!)cBGzMaP8du^=`tyGR(I>CO$Qx4E?dkZ zPxbF~11ug5EuP%?HFGOlmo_}vu%)5}`(cn^94LI5oMwZJ8pxPI<_#+SR!JY{B?Sp-JOUk6S4Jw2up5bOL$Jwxw5As+9oeH!P zCsnVB(p^yX>l{j$*9sruisABpe`tbQtpgpO9lL}5q#j3CLaWY#=da6ZY}b(_i>R>G z7DdBOS;Q<<2kxHw0tZJWX<4FQe&fnv8R(qk=-An2k+|f-(&r%*`_|#>@wKc}}2JRfTR7w;Zgrxo~*WbXnY#dry$ip7>_w zL5&LMVX5L$MM}f~(lvz9b+;GP8rerVcs<}|ZC0ChYFwL_vlT@kRJes!cEB0vI9~Rc zOKd~9T&3v#Ztrxu+DVd|F-vLKCDp@JnXb}l%a1>EbSXVeo86LA7|)3r#>b9T+0)S) z;Lr_RG;PH(>B{jcsCN#vEuKj9lIr#macBXf6YV)(ua%aDkhGnoqmyExpm-$&_Z+H5 zANr=ch;9(oG`dWpm>y58O&)$+8OAG?uC^ORC5PMOs)zigBa1ZdrgnGy%|7DdcLl#W zKDai#!JZs8nxpZE3)!Ew9>a}^r=#}kGR4Y>KH;ZDb*YkM8A+z>SM1f$(BAn5o-o#* z*U#+&XU>dSCg~em%Vks)8Utxr9qLm^TJH(N;Te2|VJ#BIEQlo#qhP{NglqL{k;z?y zy@R_VV-B0{+EF)5CaTEhF~i&JLKZ>Q`j3m>uQ>X>cYj#lw$Rrs#y?vt%WXAi+bTFB z&oq2ar)y|a7Ujmh%MS_QD#9mCHQ0z7IRk~O4STMN0Us+TY2n(pH|Mb5lxOABql}{9>gyM2bcp$X1yO?&D|gR<2w*$X#Uy z-r_qRe*R+8WF9xXF$`pUM2KVsD5L+~QaBM+d&qjH6}>L`6UC>tP>B&w|H@rf5p9PMqoXsO)4|}9W*W=D%%oQt9$=M}c&E5f7MaYTYQVU>Z{Z!V!4gIj z)fUA+#MGSr;Gf%wN_bKRTKWRpkD~Ps4oN$8bqAD6$LW|xx-p)$!OL8yG@9_eX6Oqt zv!N4eS!Hu3LFLf*L_Sd+*cqHQG_qj(#h+6NHup)cZw#-ZtgvPbH_18~Zu)vTM$28Y zQ0DF6r7-UBmCw>6o3WiBtw!@R$$cwpUoiCk+kcI`Ac{Zy@}&63bKu!PGJ})KJ-CHN(bi z1Qw%QWu9m-9yJZE5FkNhYZ3Zwr^xD&wP>Lf!4lRR>hro+uDHXZ;bxkYAyew;E7&`4 zy7#kLKD*iYc#>uC|P@4ux!5urNLx9Zu)l%F* zKsIxH&YC{QwC`K+aK_58;5XIoL{oQ^Pl{gHqwQ-kUS$bh zYrCg=T%aDXuZ#gFDaY%BF4Z~3?wO`2b|2|?DtpLmfg{e-+-4NXZ9cC9JWw}bMAhh@ zbWvrwjXUzAY=3`mmkv$5eandYS_>G}t#y^Wz)h7u%%Ph}_fgEUW{sgr7J6fRcpm)l z4)iy#`Ly$#hB-A5>*CJ^#hUv;*C`IS?C(GE&lJ5g_Xe&&jY6()WPBpxN$z@fmD&uU zowo`Xo0Ff6+HOd)1yLq(0zLjAz|H=mX>|Gz0^@CL$9mXQ&^kO}r1JU~9JTz!ZZu79 z+X$WK^xF;F8wpyygSx$rP=HlDAlv$P=JjqgC$Vv8*XGV{(M1!w=$VE(POdsr88n<# z8$R7E`tgOAu3-*k2)W=M5i?8W;g->o)CkF}?S?x1Vb8k$<_s_I(oRkmumCCTGu z9ae$#Pt-Ttg`l;!7QZ-ZIU=e{M-*1RKvlEE+JU@fi~^sAVYkFS4xzhZo=w8=@w?dt z?1{VQ^Qep1%+DSq?~)p~2#yPutW@j0Ib!c{Si8a+B3HVCc3mO6t5Wz^177t>d?iaR zGMgTw$`RaGh^aLYYH%t?l;3o7FZnj3ztkGV`)`(3R!=Spv3q0U+PK|WV6-{Yu=^mzM6C!NR|-{K|P^H^SIy7ftozi93KX=eA#ZI z)b1YmCu?z}@4(*$UZN=o$l4l(`-q@^AO-tzPxl|^q4-V@A%xau&(c466QSWuS?*MG zT0gZ+*&0s{Oc9<;59rJvAck`RNmR zIw6vrzsXN!Yi-2rxL)*BO*$*@CRkK{g2tYs@b?$3r$$%l;Ftj zj&kK>M~9P#CDZ{8-pQK|X!2O2+lPC@E}u_K6t8M=TZ-L_A-{CX&&iy9gAs-sVDAj* zdID(s%#k6sBS054;Hx(5>KVk<1dj%;%GF0z|K~KTKI#?i^V5K5WPPshQhnefJ`wur z*$3^BTM%`7S4G*!;pw7FujO8$cztunW}9mfKa1N1hUZ)GwMT8xbrCYXO zDNc3Uhj+xtWE!S%-iYHjtmb+jq+gmW6?6vg+@r3E#-4vvJSJ~{-~%MpD&^neB#x@w z>Dte3uSCs15VIkA1A3J2R;__%KZvOvVqYlmuPnU+9F20CLL5l(Y8|DHUl^{|Nvypx ze6lHT>l5VT&dX!OZ&M`3=s#7O8jCNjOnDIW^(?*# zUV4TG-$ZV`$vzazMom8y2-_`ibM>*5g1~NEBToJ%gU7JHH4|6Rll(}Wgs`W0ZND`hC|%|L zJ7*1L!KCu}6YQZq9wT?wU7_wd^g;OfQ2f)(vsm6Cpz5m}RpFQT52|Z;cCH)Xq+VHX zLs$t28?+kSZnlW>f~)=zOb?Qr*C|EoN5RD6vV%#zN#R;Q2*6Z z;DjcrkpTn%fC2jNMT7iLGr`38-*!TXqO3d+BSKDNKh-;+?`Y6My2P^~JRUEm1`%Z7 zA2iu}l~f1XRJsjU#otVDKY#v74lf0sc=z+?ndvO{nX9w4FK__u!Cq)?y6evRvHaxx zh6otiSPTxcGhnb4s~RKf>S!0>Jdw(3VIzTU-E1;Kwh|$*lrS?3MM>c#bR$M&7`Sg0 zEzSWii(`cYp-~17YcrhM;Mn$en|lEg#7%0jkPHRNiCmg94=D0-8-_fhgs3)M`z0F& zinXpB-Tk*K!8P(2(`M@!gH3!;mzj_n5xlcB6OvqVd*|-CO(U0u%e@XG-Wd%joX8g^AvP z6U^fRfjHh6m zx+keoF3kel8`T4j*RjD`;gtz;;l%$z)Yfa5k}pD}ZbM2f3>H~wbV_W<*uby$T=#sx zxoX=pD3#sYmbef?(BU<;CcTm46aqWZr>@z?*&k5LNt1=BNkNis7+7^oO!MHb}o{gE*48%A=-c>tXJSXZ^uFd(;<7=1TkGd9lSAJ3*yN3iOFrATI`a=4Zl59le;B;*uiUc5AOHXv z{|pKJH-GJaW4J`+e-@rhq@hKC5Q{gIrX``=(ri(yPaHM0Nb(0M--o7~l50d@Yu~6D zS$JsTAJChxTc>PZ!Q@}X^hpXHr<%67W_{h7P-18N6k4%FR- zDgwrLU>=!i4vZsi-i13to)$?M$bo2lX9?^|KRIn;xHX6k+m1T~bVTGj?VzMcH-qqC zT{1YIvbl(Jt|{CitvTmP)yXuJpz&_U z#@cF9E!$V6a^^PfIE53o-`X}h&u+Dz>Y1^(`es@D`yf-TFlz0xWXzG*WZkrXW1T*m zDA&#LRau~}<~(+2b^+625*5BXtD2M8YO=YNs>4iU({`9*UbX@=w&`M3dcc|5rOt1e zv7=M=DN$!!)oR7(DmgZiq^dEG>oevodnUrk1v|E^Bb0-g$3d;X_Li!pi)Qo3TDgY# z?2fLpvms~AO|-z=7llSamD6(731`XG$NrRY*wMtUNo;1PU{RgrcPu$@4l_BCa_Cdi zA?pB<>U0NU zkQ!Cb>5QFfwW08kh%SK+b&sevz8Oq8&p$6pjyBB@S={`Qy&#?=ZqF$|-c!$HxhgaI&G<1@LLm(^9sM zlgn1Va-h6t7fSE~_1W$0?!yhWjo#fw#<0sL*V`gFpG1;K6G$c-m&+oTfE7M|jdJgR zaxVmsx9fPXWEoTqq2^c8+(2RlFi7GBFi4U*Ji=$pHy}(x3^J2_3K2PCXsA=Wm`9ya zdbi|XzTH6x0#`ov-lJ$B!!ZvKhk)0He3C`$A&il&=CfovNCtvWa4t1Wqq&6+dZ4gk zsC}|OnWhy7=$-wFqwgVQ3c7_+gf8Ltbqedq!AcWm#pjqpOv?3?OVRTPZ+Gl0(UP!6 z&GLlyBG153bky$P0d)Sp;@xbEsLwjyQ=nAUgf~*c%X7i~jG=l2iPELIghC)i@(4aq zs`tDiZ;Wl>;XUFhIaY`4G#?@5en;u?-*isCyooYM(W}IVO8je*N}}#5Gveb# zlCmOx?xEprt4~-n*>TxrDQA&SyS+kl(Aj5Q!p{gqR-)Q=izsQ~NM(8UyEcgKMU0wO zO8S8yqgOzj3YYxNDOt^=SernMoDE)*~5q+4&; zNp({aW%Au&d=Ig29R4MD{&IbcQvoFw9adx$!Le4qsYP&x_^$O45N z0K|MZI)U9vv98yxuPJJO-s%;_0Y_MB)@Dl$ zlX5(Ynr}p56RWROLJn5miFt^W)UypA^-Ze<+3-IQvLuOG2CA{*#`{Jm7_q`eU)}^S zIg4~kJhF{^s@tM!CA$+8O_T8ag#41qR(K*vRA(iru6YHv&J&Y(!cNpU;ot+v1z+7q zHH$w#-mR-?=PZb`C4*x&H#hMHPjRV<3yV)#RrL%Ih6jhH`=*MKr^F zS>~L1N3aOxG8FOlJixpP4PlT`sz5seBqNtf09<4M0f~8p)7iw0j2*St58DQ84eE9A zQo%%7!#)nPb8~F_+!&w3CZMceI>@&K$2iTRu;MO(pe4-sv8l-c0Y;+q?1Wwu#{5 z7=PF)!o(8u7y{!8`7=2@Ea(OhRIM^~X;PT(HjS3F7>K(e0wu`WaO;7apY=d=3+x@O^WcUY8r(4WMq1Uo zOott{M+$LJaQtuVz~UZ3@LDGNykPf9Vc(^v(LHrKF{6nhkefZ zu-2+s^`&Z!-}u)Uqh`(VCs55>hU2V#NU|=+(>BpLESVo|lPe~4-EEd zL{Ssf{-8x&qMzNO)Ry8BJ4K6CItBYd13{m5*ScX;k%qhlB*$OIWtS034IYAt6Z)f( zl+wYytg!-wCbHvg0ddaYIz?M!U9G==5Y0LJ9h8dI0?+W z@NMQPzWQ7E({0Ld0m8$LtyB{rF}Z%4%?Q<)Mo+9Wei)%%Kqq?-%giG&fknO#I#wmx zEY?PN@d!JuKTwor_swMZe$BBq5fx+&!g40HlS0Cr$|-GRZk#dnJyM!LxAX<=b&Tm;IKFX;)g*-sC$HU&hzLsp z5n~ZH%9^z+1FE$Bk*-Kx2+9(X5F?h{m$?UrDPs$Xsnc&D$Opnu$`?85Cr^dQw3q40 zjm>J5`?b&aAKiz0*1=`Bme+8Tn@R^cctMX_SyY-b*4ud9+#t_c%WWRpoyHZ#MS>9ef{}J z{SQyU!qmor_8&t!3m01((YSe-0S18RF8J5Jum34R_%}XEBBqn3AzI^r_0(dycdtcg zn2=_6@^1DEwUKG>R?)&OVyfC@B+fvGHMLWdy(4e6CnNp7*JoUEg%}*m=X8R)?U+^UkNg^~{8AVMh-CBdN)d|4z@$ zfBCD+4o;RghQ6;6U1YoDeBiI06!xt{~bs^HKms zK)S#7nyihzla)KGkAJ>S?M1Lk`#B#9l3-&{9Nvzz9F@@|q>T1sjgDFEy){T4 zcZvxhBTYID5U%Cv8HukGlkInCq)X9|>qH3Y86^lEqFgTW53|;h_PS>LRZ5Ezit>vH zx;a|57^Bm1Y>$qL)P?m>lIXNYFV1Dsk48njk|f?2x0H6{rBf;bN{M!QWM_W%!N_id z)%0OxP4EECX%*Q_kIZq5J@tE7_I~#+36y7{R+z7TC$_H~FLhJ#Sw_TCuHVp$YhEjYK$O|6 zLVrOPaUsfBB52xfL#3=WcW{I(s!Rs8;!bsx8|6;TYg)v4#f_DjgLg@ICBINYvSPrt zU+yVms$^!(T+zVbbAl zV;6rWTZ<;rXT6DunIC9~FR`$=l&4_XDSzP)${dA%v&cpR;Ra&L*H4RE2LEEDg7CnF zT{cPQahbxs+m1Kt7wM$QBo?yP?%(i5dD`Iev=SFTA|1uh8-@BHzf1yc+7`RO;(8Im zn*X%kH0WI@MKHI-8%y8a$E~@p1=1U~7Qv!3YA)khSDs0EWz$#q@XfQj?l>paA)~B% zg}Wbuw3EC1_CyOI{et&ke{Habl?{oqNnvB?;C%MV4B%Z0)yCc#h=BHNb8v#Jvq`Kl zWcrRI%4C$2OX}T+@X+>_VCP$yl?Q!9j|CNHA_g5OE_vES=jCvD4O5#~!??kHilgUt z3$fD-iLfbtRloldC# z((C^-8diI8M_EPt9N8Q*Vi3D*mn=<2HpVF>lvGYFW^4sQnuIVx4=bIK@N6B$pQdO6 zDwUK@ZIx^bHnWt$$*grLsF4iHFSww8Dfpnjcc07kwP#^#takE*A04&Ndb|F(c26tw z`#dS?1@XZ2lN{)Sm9!aD-{kbBjezw~9F~Ue$`+y4Q@nd5L3a8SLze6!3bUWsUnbET zY*X>(j!^X=47eG4>4f%{pQ)SVwkL5=sqCa+ubbfBRlh4`c}(5vqK(r}t8za)hlK8; zE;&JNCpjUI>L(cA=S539dN;OYRB}_S-ASu@+Pp(~X9>t7y>osS8m3p(QJiC>cn>sB z0bQL;o@fQ3J>VF?sh8cw=m|WYJ1`ubYyLjMf{TPX8V7f!T7RZ-3Z^ERUGGh8ALTph z2+CZq6Q<54K5znXzMI2z!y|9D)w9_&=Ty554s_jC&1pT8!S3j5${z3Dl&7%3a)=V@ zuOysA8Z@M&?@~-cM=cKB3bf2);FZ2U7)&*oC>$RR`;%>@qt8SiJ%dkDZ0?h&i!HP2 zqYBN!m|?Vv9qO(FN*Cnzrjb>nwj@v{zfnq*&Z3EM{;nZsVmXj|*^g#;@1xlqV1}6P z+qwyrbMzMqz(5}(v$xZfT0mN4mL@{nL2lhBIa5q)z84~vJFieDLq9O=7y+I{zrW1} z`G_@HX(rdHBZ=&4gvv9bb`H=?AJ04r=R2lzPC>*xP*k^4a`1an^4d((GE$=(BZ#2b zT+r~P4539yZT?LSZcW2a zp5l2V_z3maezAVgCcBei$0hVGfBjeS8K=nQqTL17B76owH)wt8`LiG)pp0n{aX!zzN zgr>c<%V0x#Z!)-XN`T)1Pm{)au}o}nuw!b>!LIA;x!=aqgN1jjc}6oS7AsgOKtn-+ z2yydtwyAnrNWnJsyCzSg_H}1j>#txNI!^Wnm_jWt>r|M_WZ>0XOb|Fw%%x{A-T|V8 zTbyi@f}QR)v`emRe6uYlZhYU%`R&f9(?S(^v&VrzXx+I`QtzCO{3P$EY6>Z1{XG-~ zZ3fuQc2J9FS~;usNb_`GY~Qd&7`7#hz0bMEsDY|S3MTd{Kc{_NxZY6$)XTTSfowe& zW1!ZHnbTHQVi!YjEQ)


    ^v7e>L((hxz&GsuhDi7w} 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%0A~2TOX$v8!iDM-U5$dQi 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 OfXNR-ju5&-< Date: Mon, 18 Dec 2017 12:02:13 -0500 Subject: [PATCH 79/85] Remove unneeded type specifier in lambda --- test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java index fffb220d27..58a51c35b5 100644 --- a/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java +++ b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java @@ -89,7 +89,7 @@ public class LoadDetachedPluginsTest { @LocalData public void upgradeFromJenkins2WithDependency() { VersionNumber since = new VersionNumber("2.0"); - rr.then((JenkinsRule r) -> { + rr.then(r -> { List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since); assertThat("Plugins have been detached since the pre-upgrade version", detachedPlugins.size(), greaterThan(1)); From 7fc4815e117eda7e598599fd4bddc97362afaf95 Mon Sep 17 00:00:00 2001 From: Devin Nusbaum Date: Mon, 18 Dec 2017 12:40:20 -0500 Subject: [PATCH 80/85] Do not downgrade installed plugins when loading detached plugins --- core/src/main/java/hudson/PluginManager.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 14bb1c1e0d..d6b02ad845 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -650,7 +650,17 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas continue; } - String artifactId = dependencyToken.split(":")[0]; + String[] artifactIdVersionPair = dependencyToken.split(":"); + String artifactId = artifactIdVersionPair[0]; + VersionNumber dependencyVersion = new VersionNumber(artifactIdVersionPair[1]); + + PluginManager manager = Jenkins.getActiveInstance().getPluginManager(); + VersionNumber installedVersion = manager.getPluginVersion(manager.rootDir, artifactId); + if (installedVersion != null && installedVersion.isNewerThan(dependencyVersion)) { + // Do not downgrade dependencies that are already installed. + continue; + } + URL dependencyURL = context.getResource(fromPath + "/" + artifactId + ".hpi"); if (dependencyURL == null) { From 01f70d4290859628ded4890fd6ac26aa6271893e Mon Sep 17 00:00:00 2001 From: Devin Nusbaum Date: Mon, 18 Dec 2017 13:14:24 -0500 Subject: [PATCH 81/85] Don't copy the bundled dependency if the current version is already installed --- core/src/main/java/hudson/PluginManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index d6b02ad845..28c845a7f7 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -656,7 +656,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas PluginManager manager = Jenkins.getActiveInstance().getPluginManager(); VersionNumber installedVersion = manager.getPluginVersion(manager.rootDir, artifactId); - if (installedVersion != null && installedVersion.isNewerThan(dependencyVersion)) { + if (installedVersion != null && !installedVersion.isOlderThan(dependencyVersion)) { // Do not downgrade dependencies that are already installed. continue; } From eb77e5f20d150c272755435201120dfd8317e180 Mon Sep 17 00:00:00 2001 From: Devin Nusbaum Date: Mon, 18 Dec 2017 13:16:20 -0500 Subject: [PATCH 82/85] Upgrade installed optional dependencies if necessary --- core/src/main/java/hudson/PluginManager.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 28c845a7f7..f9a7cff84d 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -645,17 +645,23 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas ServletContext context = Jenkins.getActiveInstance().servletContext; for (String dependencyToken : dependencyTokens) { - if (dependencyToken.endsWith(";resolution:=optional")) { - // ignore optional dependencies - continue; + String optionalDepTokenSuffix = ";resolution:=optional"; + boolean isOptional = dependencyToken.endsWith(optionalDepTokenSuffix); + if (isOptional) { + dependencyToken = dependencyToken.substring(0, dependencyToken.length() - optionalDepTokenSuffix.length()); } String[] artifactIdVersionPair = dependencyToken.split(":"); String artifactId = artifactIdVersionPair[0]; VersionNumber dependencyVersion = new VersionNumber(artifactIdVersionPair[1]); - - PluginManager manager = Jenkins.getActiveInstance().getPluginManager(); + PluginManager manager = Jenkins.getInstance().getPluginManager(); VersionNumber installedVersion = manager.getPluginVersion(manager.rootDir, artifactId); + + if (isOptional && installedVersion == null) { + // Ignore uninstalled optional dependencies. + continue; + } + if (installedVersion != null && !installedVersion.isOlderThan(dependencyVersion)) { // Do not downgrade dependencies that are already installed. continue; From 94b1ba978213a0929fc90808c22c0d9b749a8c17 Mon Sep 17 00:00:00 2001 From: Devin Nusbaum Date: Mon, 18 Dec 2017 14:21:30 -0500 Subject: [PATCH 83/85] Add test asserting that outdated dependencies of detached plugins are updated --- .../install/LoadDetachedPluginsTest.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java index 58a51c35b5..856e5263ba 100644 --- a/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java +++ b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java @@ -31,7 +31,6 @@ import hudson.PluginManager; import hudson.PluginManagerUtil; import hudson.PluginWrapper; import hudson.util.VersionNumber; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -87,7 +86,7 @@ public class LoadDetachedPluginsTest { @Issue("JENKINS-48604") @Test @LocalData - public void upgradeFromJenkins2WithDependency() { + public void upgradeFromJenkins2WithNewerDependency() { VersionNumber since = new VersionNumber("2.0"); rr.then(r -> { List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since); @@ -98,7 +97,25 @@ public class LoadDetachedPluginsTest { Plugin scriptSecurity = r.jenkins.getPlugin("script-security"); assertThat("Script-security should be installed", scriptSecurity, notNullValue()); assertThat("Dependencies of detached plugins should not be downgraded", - scriptSecurity.getWrapper().getVersionNumber(), greaterThan(new VersionNumber("1.18.1"))); + scriptSecurity.getWrapper().getVersionNumber(), equalTo(new VersionNumber("1.34"))); + assertNoFailedPlugins(r); + }); + } + + @Test + @LocalData + public void upgradeFromJenkins2WithOlderDependency() { + VersionNumber since = new VersionNumber("2.0"); + rr.then(r -> { + List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since); + assertThat("Plugins have been detached since the pre-upgrade version", + detachedPlugins.size(), greaterThan(1)); + assertThat("Plugins detached between the pre-upgrade version and the current version should be installed", + getInstalledDetachedPlugins(r, detachedPlugins).size(), equalTo(detachedPlugins.size())); + Plugin scriptSecurity = r.jenkins.getPlugin("script-security"); + assertThat("Script-security should be installed", scriptSecurity, notNullValue()); + assertThat("Dependencies of detached plugins should be upgraded to the required version", + scriptSecurity.getWrapper().getVersionNumber(), equalTo(new VersionNumber("1.18.1"))); assertNoFailedPlugins(r); }); } From b98ba8c8ffde0c792da0dfda1eb308cefce9599b Mon Sep 17 00:00:00 2001 From: Devin Nusbaum Date: Mon, 18 Dec 2017 16:12:29 -0500 Subject: [PATCH 84/85] Update local data for tests --- ...upgradeFromJenkins2WithNewerDependency.zip} | Bin .../upgradeFromJenkins2WithOlderDependency.zip | Bin 0 -> 168835 bytes 2 files changed, 0 insertions(+), 0 deletions(-) rename test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/{upgradeFromJenkins2WithDependency.zip => upgradeFromJenkins2WithNewerDependency.zip} (100%) create mode 100644 test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithOlderDependency.zip diff --git a/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithDependency.zip b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithNewerDependency.zip similarity index 100% rename from test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithDependency.zip rename to test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithNewerDependency.zip diff --git a/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithOlderDependency.zip b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithOlderDependency.zip new file mode 100644 index 0000000000000000000000000000000000000000..b85561cf0dd5f0bed6182727cd246cc1404263bb GIT binary patch literal 168835 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 Date: Mon, 18 Dec 2017 16:35:23 -0500 Subject: [PATCH 85/85] Revert "Upgrade installed optional dependencies if necessary" This reverts commit eb77e5f20d150c272755435201120dfd8317e180. --- core/src/main/java/hudson/PluginManager.java | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index f9a7cff84d..28c845a7f7 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -645,23 +645,17 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas ServletContext context = Jenkins.getActiveInstance().servletContext; for (String dependencyToken : dependencyTokens) { - String optionalDepTokenSuffix = ";resolution:=optional"; - boolean isOptional = dependencyToken.endsWith(optionalDepTokenSuffix); - if (isOptional) { - dependencyToken = dependencyToken.substring(0, dependencyToken.length() - optionalDepTokenSuffix.length()); + if (dependencyToken.endsWith(";resolution:=optional")) { + // ignore optional dependencies + continue; } String[] artifactIdVersionPair = dependencyToken.split(":"); String artifactId = artifactIdVersionPair[0]; VersionNumber dependencyVersion = new VersionNumber(artifactIdVersionPair[1]); - PluginManager manager = Jenkins.getInstance().getPluginManager(); + + PluginManager manager = Jenkins.getActiveInstance().getPluginManager(); VersionNumber installedVersion = manager.getPluginVersion(manager.rootDir, artifactId); - - if (isOptional && installedVersion == null) { - // Ignore uninstalled optional dependencies. - continue; - } - if (installedVersion != null && !installedVersion.isOlderThan(dependencyVersion)) { // Do not downgrade dependencies that are already installed. continue;