From e5046911c57e60a1d6d8aca9b21bd9093b0f3763 Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Wed, 1 Aug 2018 00:56:12 +0200 Subject: [PATCH 01/11] [SECURITY-790] --- core/src/main/java/hudson/scheduler/CronTab.java | 2 +- core/src/test/java/hudson/scheduler/CronTabTest.java | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/hudson/scheduler/CronTab.java b/core/src/main/java/hudson/scheduler/CronTab.java index 14c67b9034..120b8bc2a1 100644 --- a/core/src/main/java/hudson/scheduler/CronTab.java +++ b/core/src/main/java/hudson/scheduler/CronTab.java @@ -204,7 +204,7 @@ public final class CronTab { } void setTo(Calendar c, int i) { - c.set(field,i-offset); + c.set(field,Math.min(i-offset, c.getActualMaximum(field))); } void clear(Calendar c) { diff --git a/core/src/test/java/hudson/scheduler/CronTabTest.java b/core/src/test/java/hudson/scheduler/CronTabTest.java index e2acaf53ba..cae0fdc7ed 100644 --- a/core/src/test/java/hudson/scheduler/CronTabTest.java +++ b/core/src/test/java/hudson/scheduler/CronTabTest.java @@ -314,4 +314,11 @@ public class CronTabTest { assertEquals("[35, 56]", times.toString()); } + @Issue("SECURITY-790") + @Test(timeout = 1000L) public void testLongMonths() throws Exception { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.MONTH, Calendar.JULY); + new CronTab("0 0 31 7 *").floor(cal); // would infinite loop + } + } From ef9583a24abc4de157e1570cb32d7a273d327f36 Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Wed, 1 Aug 2018 00:58:04 +0200 Subject: [PATCH 02/11] [SECURITY-996] --- .../java/hudson/security/SecurityRealm.java | 3 + .../TokenBasedRememberMeServices2.java | 44 +++++++--- ...enBasedRememberMeServices2Test_SEC996.java | 88 +++++++++++++++++++ 3 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test_SEC996.java diff --git a/core/src/main/java/hudson/security/SecurityRealm.java b/core/src/main/java/hudson/security/SecurityRealm.java index 6cb69906e0..219a317eda 100644 --- a/core/src/main/java/hudson/security/SecurityRealm.java +++ b/core/src/main/java/hudson/security/SecurityRealm.java @@ -304,6 +304,9 @@ public abstract class SecurityRealm extends AbstractDescribableImpl0 ? req.getContextPath() : "/"); rsp.addCookie(cookie); diff --git a/core/src/main/java/hudson/security/TokenBasedRememberMeServices2.java b/core/src/main/java/hudson/security/TokenBasedRememberMeServices2.java index 9b81ae5b19..ff0f374ca4 100644 --- a/core/src/main/java/hudson/security/TokenBasedRememberMeServices2.java +++ b/core/src/main/java/hudson/security/TokenBasedRememberMeServices2.java @@ -123,19 +123,39 @@ public class TokenBasedRememberMeServices2 extends TokenBasedRememberMeServices @Override public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { - try { - return super.autoLogin(request, response); - } catch (Exception e) { - cancelCookie(request, response, "Failed to handle remember-me cookie: "+Functions.printThrowable(e)); + if(Jenkins.getInstance().isDisableRememberMe()){ + cancelCookie(request, response, null); return null; + }else { + try { + return super.autoLogin(request, response); + } catch (Exception e) { + cancelCookie(request, response, "Failed to handle remember-me cookie: " + Functions.printThrowable(e)); + return null; + } } } - @Override - protected Cookie makeValidCookie(String tokenValueBase64, HttpServletRequest request, long maxAge) { - Cookie cookie = super.makeValidCookie(tokenValueBase64, request, maxAge); + @Override + protected Cookie makeValidCookie(String tokenValueBase64, HttpServletRequest request, long maxAge) { + Cookie cookie = super.makeValidCookie(tokenValueBase64, request, maxAge); + secureCookie(cookie, request); + return cookie; + } + + @Override + protected Cookie makeCancelCookie(HttpServletRequest request) { + Cookie cookie = super.makeCancelCookie(request); + secureCookie(cookie, request); + return cookie; + } + + /** + * Force always the http-only flag and depending on the request, put also the secure flag. + */ + private void secureCookie(Cookie cookie, HttpServletRequest request){ // if we can mark the cookie HTTP only, do so to protect this cookie even in case of XSS vulnerability. - if (SET_HTTP_ONLY!=null) { + if (SET_HTTP_ONLY!=null) { try { SET_HTTP_ONLY.invoke(cookie,true); } catch (IllegalAccessException e) { @@ -148,12 +168,10 @@ public class TokenBasedRememberMeServices2 extends TokenBasedRememberMeServices // if the user is running Jenkins over HTTPS, we also want to prevent the cookie from leaking in HTTP. // whether the login is done over HTTPS or not would be a good enough approximation of whether Jenkins runs in // HTTPS or not, so use that. - if (request.isSecure()) - cookie.setSecure(true); - return cookie; - } + cookie.setSecure(request.isSecure()); + } - /** + /** * Used to compute the token signature securely. */ private static final HMACConfidentialKey MAC = new HMACConfidentialKey(TokenBasedRememberMeServices.class,"mac"); diff --git a/test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test_SEC996.java b/test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test_SEC996.java new file mode 100644 index 0000000000..b2fa21ce6c --- /dev/null +++ b/test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test_SEC996.java @@ -0,0 +1,88 @@ +/* + * The MIT License + * + * Copyright (c) 2018, 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 com.gargoylesoftware.htmlunit.util.Cookie; +import com.gargoylesoftware.htmlunit.xml.XmlPage; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.text.IsEmptyString.isEmptyString; +import static org.hamcrest.xml.HasXPath.hasXPath; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +public class TokenBasedRememberMeServices2Test_SEC996 { + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + @Issue("SECURITY-996") + public void rememberMeToken_shouldNotBeRead_ifOptionIsDisabled() throws Exception { + j.jenkins.setDisableRememberMe(false); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + + Cookie rememberMeCookie = null; + { + JenkinsRule.WebClient wc = j.createWebClient(); + wc.login("alice", "alice", true); + + + // we should see a remember me cookie + rememberMeCookie = getRememberMeCookie(wc); + assertNotNull(rememberMeCookie); + assertThat(rememberMeCookie.getValue(), not(isEmptyString())); + } + + j.jenkins.setDisableRememberMe(true); + { + JenkinsRule.WebClient wc = j.createWebClient(); + + wc.getCookieManager().addCookie(rememberMeCookie); + + // the application should not use the cookie to connect + XmlPage page = (XmlPage) wc.goTo("whoAmI/api/xml", "application/xml"); + assertThat(page, hasXPath("//name", not(is("alice")))); + } + + j.jenkins.setDisableRememberMe(false); + { + JenkinsRule.WebClient wc = j.createWebClient(); + + wc.getCookieManager().addCookie(rememberMeCookie); + + // if we reactivate the remember me feature, it's ok + XmlPage page = (XmlPage) wc.goTo("whoAmI/api/xml", "application/xml"); + assertThat(page, hasXPath("//name", is("alice"))); + } + } + + private Cookie getRememberMeCookie(JenkinsRule.WebClient wc) { + return wc.getCookieManager().getCookie(TokenBasedRememberMeServices2.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY); + } +} From 832144e32297fea7734a44bd2064f6ddd4562874 Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Wed, 1 Aug 2018 00:58:46 +0200 Subject: [PATCH 03/11] [SECURITY-896] --- .../security/BasicAuthenticationFilter.java | 11 +- .../jenkins/security/ApiTokenProperty.java | 3 +- .../jenkins/security/BasicApiTokenHelper.java | 88 ++++++++ .../BasicHeaderApiTokenAuthenticator.java | 27 +-- ...eaderApiTokenAuthenticatorTest_SEC986.java | 200 ++++++++++++++++++ 5 files changed, 308 insertions(+), 21 deletions(-) create mode 100644 core/src/main/java/jenkins/security/BasicApiTokenHelper.java create mode 100644 test/src/test/java/jenkins/security/BasicHeaderApiTokenAuthenticatorTest_SEC986.java diff --git a/core/src/main/java/hudson/security/BasicAuthenticationFilter.java b/core/src/main/java/hudson/security/BasicAuthenticationFilter.java index 11709bcf58..12deb034f5 100644 --- a/core/src/main/java/hudson/security/BasicAuthenticationFilter.java +++ b/core/src/main/java/hudson/security/BasicAuthenticationFilter.java @@ -26,7 +26,7 @@ package hudson.security; import hudson.model.User; import jenkins.model.Jenkins; import hudson.util.Scrambler; -import jenkins.security.ApiTokenProperty; +import jenkins.security.BasicApiTokenHelper; import org.acegisecurity.context.SecurityContextHolder; import javax.servlet.Filter; @@ -132,12 +132,9 @@ public class BasicAuthenticationFilter implements Filter { return; } - {// attempt to authenticate as API token - // create is true as the user may not have been saved and the default api token may be in use. - // validation of the user will be performed against the underlying realm in impersonate. - User u = User.getById(username, true); - ApiTokenProperty t = u.getProperty(ApiTokenProperty.class); - if (t!=null && t.matchesPassword(password)) { + { + User u = BasicApiTokenHelper.isConnectingUsingApiToken(username, password); + if(u != null){ SecurityContextHolder.getContext().setAuthentication(u.impersonate()); try { chain.doFilter(request,response); diff --git a/core/src/main/java/jenkins/security/ApiTokenProperty.java b/core/src/main/java/jenkins/security/ApiTokenProperty.java index dd64d0e9d0..fc87f3c411 100644 --- a/core/src/main/java/jenkins/security/ApiTokenProperty.java +++ b/core/src/main/java/jenkins/security/ApiTokenProperty.java @@ -200,5 +200,6 @@ public class ApiTokenProperty extends UserProperty { /** * We don't want an API key that's too long, so cut the length to 16 (which produces 32-letter MAC code in hexdump) */ - private static final HMACConfidentialKey API_KEY_SEED = new HMACConfidentialKey(ApiTokenProperty.class,"seed",16); + @Restricted(NoExternalUse.class) + public static final HMACConfidentialKey API_KEY_SEED = new HMACConfidentialKey(ApiTokenProperty.class,"seed",16); } diff --git a/core/src/main/java/jenkins/security/BasicApiTokenHelper.java b/core/src/main/java/jenkins/security/BasicApiTokenHelper.java new file mode 100644 index 0000000000..bafece0e57 --- /dev/null +++ b/core/src/main/java/jenkins/security/BasicApiTokenHelper.java @@ -0,0 +1,88 @@ +/* + * The MIT License + * + * Copyright (c) 2018, 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.Util; +import hudson.model.User; +import jenkins.model.GlobalConfiguration; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.CheckForNull; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +@Restricted(NoExternalUse.class) +public class BasicApiTokenHelper { + public static @CheckForNull User isConnectingUsingApiToken(String username, String tokenValue){ + User user = User.getById(username, false); + if(user == null){ + if(isLegacyTokenGeneratedOnUserCreation()){ + String generatedTokenOnCreation = Util.getDigestOf(ApiTokenProperty.API_KEY_SEED.mac(username)); + boolean areTokenEqual = MessageDigest.isEqual( + generatedTokenOnCreation.getBytes(StandardCharsets.US_ASCII), + tokenValue.getBytes(StandardCharsets.US_ASCII) + ); + if(areTokenEqual){ + // directly return the user freshly created + // and no need to check its token as the generated token + // will be the same as the one we checked just above + return User.getById(username, true); + } + } + }else{ + ApiTokenProperty t = user.getProperty(ApiTokenProperty.class); + if (t!=null && t.matchesPassword(tokenValue)) { + return user; + } + } + + return null; + } + + //TODO could be simplified when the patch is merged in 2.129+ + @SuppressWarnings("unchecked") + static boolean isLegacyTokenGeneratedOnUserCreation(){ + Class clazz; + try { + clazz = (Class) BasicApiTokenHelper.class.getClassLoader().loadClass("jenkins.security.apitoken.ApiTokenPropertyConfiguration"); + } catch (ClassNotFoundException e) { + // case the new api token system is not in place + return true; + } + + Object apiTokenConfiguration = GlobalConfiguration.all().get(clazz); + boolean tokenGenerationOnCreationEnabled; + try { + Method isTokenGenerationOnCreationEnabled = clazz.getDeclaredMethod("isTokenGenerationOnCreationEnabled"); + tokenGenerationOnCreationEnabled = (boolean) isTokenGenerationOnCreationEnabled.invoke(apiTokenConfiguration); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + return false; + } + + return tokenGenerationOnCreationEnabled; + } +} diff --git a/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java b/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java index a192dddc5f..27fdb4f047 100644 --- a/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java +++ b/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java @@ -23,19 +23,20 @@ import static java.util.logging.Level.*; public class BasicHeaderApiTokenAuthenticator extends BasicHeaderAuthenticator { @Override public Authentication authenticate(HttpServletRequest req, HttpServletResponse rsp, String username, String password) throws ServletException { - // attempt to authenticate as API token - User u = User.getById(username, true); - ApiTokenProperty t = u.getProperty(ApiTokenProperty.class); - if (t!=null && t.matchesPassword(password)) { - try { - return u.impersonate(); - } 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. - LOGGER.log(WARNING, "API token matched for user "+username+" but the impersonation failed",x); - throw new ServletException(x); - } catch (DataAccessException x) { - throw new ServletException(x); + User u = BasicApiTokenHelper.isConnectingUsingApiToken(username, password); + if(u != null) { + ApiTokenProperty t = u.getProperty(ApiTokenProperty.class); + if (t != null && t.matchesPassword(password)) { + try { + return u.impersonate(); + } 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. + LOGGER.log(WARNING, "API token matched for user " + username + " but the impersonation failed", x); + throw new ServletException(x); + } catch (DataAccessException x) { + throw new ServletException(x); + } } } return null; diff --git a/test/src/test/java/jenkins/security/BasicHeaderApiTokenAuthenticatorTest_SEC986.java b/test/src/test/java/jenkins/security/BasicHeaderApiTokenAuthenticatorTest_SEC986.java new file mode 100644 index 0000000000..f72c407b70 --- /dev/null +++ b/test/src/test/java/jenkins/security/BasicHeaderApiTokenAuthenticatorTest_SEC986.java @@ -0,0 +1,200 @@ +/* + * The MIT License + * + * Copyright (c) 2018, 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.WebRequest; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.html.HtmlTextInput; +import com.gargoylesoftware.htmlunit.xml.XmlPage; +import hudson.ExtensionComponent; +import hudson.model.User; +import jenkins.ExtensionFilter; +import jenkins.model.Jenkins; +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runners.model.Statement; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; +import org.jvnet.hudson.test.RestartableJenkinsRule; +import org.jvnet.hudson.test.TestExtension; + +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.xml.HasXPath.hasXPath; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; + +public class BasicHeaderApiTokenAuthenticatorTest_SEC986 { + @Rule + public RestartableJenkinsRule rr = new RestartableJenkinsRule(); + + @Test + public void legacyToken_regularCase() { + AtomicReference token = new AtomicReference<>(); + rr.addStep(new Statement() { + @Override + public void evaluate() throws Throwable { + assumeLegacyTokenCreation(); + + configureSecurity(); + + { + JenkinsRule.WebClient wc = rr.j.createWebClient(); + // default SecurityListener will save the user when adding the LastGrantedAuthoritiesProperty + // and so the user is persisted + wc.login("user1"); + HtmlPage page = wc.goTo("user/user1/configure"); + String tokenValue = ((HtmlTextInput) page.getDocumentElement().querySelector("#apiToken")).getText(); + token.set(tokenValue); + } + } + }); + rr.addStep(new Statement() { + @Override + public void evaluate() throws Throwable { + User user = User.getById("user1", false); + assertNotNull(user); + + JenkinsRule.WebClient wc = rr.j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + + { // for invalid token, no effect + WebRequest request = new WebRequest(new URL(rr.j.jenkins.getRootUrl() + "whoAmI/api/xml")); + request.setAdditionalHeader("Authorization", base64("user1", "invalid-token")); + assertThat(wc.getPage(request).getWebResponse().getStatusCode(), equalTo(401)); + } + { // for invalid user, no effect + WebRequest request = new WebRequest(new URL(rr.j.jenkins.getRootUrl() + "whoAmI/api/xml")); + request.setAdditionalHeader("Authorization", base64("user-not-valid", token.get())); + assertThat(wc.getPage(request).getWebResponse().getStatusCode(), equalTo(401)); + } + + assertNull(User.getById("user-not-valid", false)); + + { // valid user with valid token, ok + WebRequest request = new WebRequest(new URL(rr.j.jenkins.getRootUrl() + "whoAmI/api/xml")); + request.setAdditionalHeader("Authorization", base64("user1", token.get())); + XmlPage xmlPage = wc.getPage(request); + assertThat(xmlPage, hasXPath("//name", is("user1"))); + } + } + }); + } + + /* + * The user is not saved after login without the default SecurityListener#fireAuthenticated + */ + @Test + public void legacyToken_withoutLastGrantedAuthorities() { + AtomicReference token = new AtomicReference<>(); + rr.addStep(new Statement() { + @Override + public void evaluate() throws Throwable { + assumeLegacyTokenCreation(); + + configureSecurity(); + + { + JenkinsRule.WebClient wc = rr.j.createWebClient(); + wc.login("user1"); + HtmlPage page = wc.goTo("user/user1/configure"); + String tokenValue = ((HtmlTextInput) page.getDocumentElement().querySelector("#apiToken")).getText(); + token.set(tokenValue); + } + } + }); + rr.addStep(new Statement() { + @Override + public void evaluate() throws Throwable { + User user = User.getById("user1", false); + assertNull(user); + + JenkinsRule.WebClient wc = rr.j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + + { // for invalid token, no effect + WebRequest request = new WebRequest(new URL(rr.j.jenkins.getRootUrl() + "whoAmI/api/xml")); + request.setAdditionalHeader("Authorization", base64("user1", "invalid-token")); + assertThat(wc.getPage(request).getWebResponse().getStatusCode(), equalTo(401)); + } + { // for invalid user, no effect + WebRequest request = new WebRequest(new URL(rr.j.jenkins.getRootUrl() + "whoAmI/api/xml")); + request.setAdditionalHeader("Authorization", base64("user-not-valid", token.get())); + assertThat(wc.getPage(request).getWebResponse().getStatusCode(), equalTo(401)); + } + + assertNull(User.getById("user1", false)); + assertNull(User.getById("user-not-valid", false)); + + { // valid user with valid token, ok + WebRequest request = new WebRequest(new URL(rr.j.jenkins.getRootUrl() + "whoAmI/api/xml")); + request.setAdditionalHeader("Authorization", base64("user1", token.get())); + XmlPage xmlPage = wc.getPage(request); + assertThat(xmlPage, hasXPath("//name", is("user1"))); + } + } + }); + rr.addStep(new Statement() { + @Override + public void evaluate() throws Throwable { + User user = User.getById("user1", false); + assertNull(user); + } + }); + } + + @TestExtension("legacyToken_withoutLastGrantedAuthorities") + public static class RemoveDefaultSecurityListener extends ExtensionFilter { + @Override + public boolean allows(Class type, ExtensionComponent component) { + return !SecurityListener.class.isAssignableFrom(type); + } + } + + private void configureSecurity() throws Exception { + rr.j.jenkins.setSecurityRealm(rr.j.createDummySecurityRealm()); + rr.j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.ADMINISTER).everywhere().toEveryone()); + rr.j.jenkins.save(); + } + + @SuppressWarnings("unchecked") + private void assumeLegacyTokenCreation() { + Assume.assumeTrue( + "New API token system present AND legacy token is not generated on user creation", + BasicApiTokenHelper.isLegacyTokenGeneratedOnUserCreation() + ); + } + + private String base64(String login, String password) { + return "Basic " + Base64.getEncoder().encodeToString((login + ":" + password).getBytes(StandardCharsets.UTF_8)); + } +} From e7bf396555fd89ebc53ee060c9e6f41ee06f9fca Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Mon, 25 Jun 2018 17:12:13 -0400 Subject: [PATCH 04/11] JENKINS-52159: Queue.maintain - tolerate misbehaving sorter.sortBuildableItems() Any plugin that implements the sort step could throw an exception which would break the maintain() step. As sorting is really an optional step, it really should not be fatal. Instead, we wrap the step in a try block, log it, and move on with life. (cherry picked from commit 1083a3f5c648e87464e25815779690eed1fb3296) --- core/src/main/java/hudson/model/Queue.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index 8aad4b0acc..0556b1c318 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -1569,8 +1569,15 @@ public class Queue extends ResourceController implements Saveable { } } - if (s != null) - s.sortBuildableItems(buildables); + if (s != null) { + try { + s.sortBuildableItems(buildables); + } catch (Throwable e) { + // We don't really care if the sort doesn't sort anything, we still should + // continue to do our job. We'll complain about it and continue. + LOGGER.log(Level.WARNING, "s.sortBuildableItems() threw Throwable: {0}", e); + } + } // Ensure that identification of blocked tasks is using the live state: JENKINS-27708 & JENKINS-27871 updateSnapshot(); From 4cb86a5c3055d39cf836a7d662f53e316a6a1cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Wed, 1 Aug 2018 09:33:31 +0200 Subject: [PATCH 05/11] [FIXED JENKINS-52417] Restrict Beta API removed in weekly: FilePath.copyFromRemotely() --- core/src/main/java/hudson/FilePath.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index aeb0c08c57..051a3561ba 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -128,6 +128,7 @@ import org.apache.tools.zip.ZipFile; import org.jenkinsci.remoting.RoleChecker; import org.jenkinsci.remoting.RoleSensitive; import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.Stapler; @@ -920,11 +921,10 @@ public final class FilePath implements Serializable { } /** - * Copies the content of a URL to a remote file. - * Unlike {@link #copyFrom} this will not transfer content over a Remoting channel. - * @since 2.119 + * Kept to maintain binary compatibility - restricted to discourage further use as it is removed in newer versions. */ - @Restricted(Beta.class) + @Restricted(DoNotUse.class) + @RestrictedSince("2.121.3") public void copyFromRemotely(URL url) throws IOException, InterruptedException { act(new CopyFromRemotely(url)); } From f42fe0fc63c9cab33cce361bde16c12ac035efa4 Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Thu, 7 Jun 2018 18:15:50 +0200 Subject: [PATCH 06/11] [JENKINS-51777] Don't let tar entries escape target dir (cherry picked from commit 7438abb88fc7d9bbd5f2b265e8fb191179a3c553) --- core/src/main/java/hudson/FilePath.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 051a3561ba..7ec95e032f 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -2447,6 +2447,10 @@ public final class FilePath implements Serializable { TarArchiveEntry te; while ((te = t.getNextTarEntry()) != null) { File f = new File(baseDir, te.getName()); + if (!f.toPath().normalize().startsWith(baseDir.toPath())) { + throw new IOException( + "Tar " + name + " contains illegal file name that breaks out of the target directory: " + te.getName()); + } if (te.isDirectory()) { mkdirs(f); } else { From 43e8049892f6298b087679f7b2540b09ccde14b2 Mon Sep 17 00:00:00 2001 From: James Nord Date: Mon, 18 Jun 2018 22:29:21 +0100 Subject: [PATCH 07/11] Merge pull request #3496 from jtnord/JENKINS-JENKINS-28683 [JENKINS-28683] (A)PeriodicWork suuport for dynamically loaded plugins. (cherry picked from commit 1e8c07a4ea963bc19dccd51b5d39ee814bd37f0b) --- .../main/java/hudson/model/AperiodicWork.java | 38 +++++++++++- .../main/java/hudson/model/PeriodicWork.java | 41 ++++++++++++- .../java/hudson/model/AperiodicWorkTest.java | 59 +++++++++++++++++++ .../java/hudson/model/PeriodicWorkTest.java | 54 +++++++++++++++++ 4 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 test/src/test/java/hudson/model/AperiodicWorkTest.java create mode 100644 test/src/test/java/hudson/model/PeriodicWorkTest.java diff --git a/core/src/main/java/hudson/model/AperiodicWork.java b/core/src/main/java/hudson/model/AperiodicWork.java index fbd4f2c4f7..3c5385c077 100644 --- a/core/src/main/java/hudson/model/AperiodicWork.java +++ b/core/src/main/java/hudson/model/AperiodicWork.java @@ -24,12 +24,15 @@ package hudson.model; import hudson.ExtensionList; +import hudson.ExtensionListListener; import hudson.ExtensionPoint; import hudson.init.Initializer; import hudson.triggers.SafeTimerTask; import jenkins.util.Timer; +import java.util.HashSet; import java.util.Random; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; @@ -92,11 +95,17 @@ public abstract class AperiodicWork extends SafeTimerTask implements ExtensionPo @Initializer(after= JOB_LOADED) public static void init() { // start all AperidocWorks + ExtensionList extensionList = all(); + extensionList.addListener(new AperiodicWorkExtensionListListener(extensionList)); for (AperiodicWork p : AperiodicWork.all()) { - Timer.get().schedule(p, p.getInitialDelay(), TimeUnit.MILLISECONDS); + scheduleAperiodWork(p); } } + private static void scheduleAperiodWork(AperiodicWork ap) { + Timer.get().schedule(ap, ap.getInitialDelay(), TimeUnit.MILLISECONDS); + } + protected abstract void doAperiodicRun(); /** @@ -107,4 +116,31 @@ public abstract class AperiodicWork extends SafeTimerTask implements ExtensionPo } private static final Random RANDOM = new Random(); + + /** + * ExtensionListener that will kick off any new AperiodWork extensions from plugins that are dynamically + * loaded. + */ + private static class AperiodicWorkExtensionListListener extends ExtensionListListener { + + private final Set registered = new HashSet<>(); + + AperiodicWorkExtensionListListener(ExtensionList initiallyRegistered) { + for (AperiodicWork p : initiallyRegistered) { + registered.add(p); + } + } + + @Override + public void onChange() { + synchronized (registered) { + for (AperiodicWork p : AperiodicWork.all()) { + if (!registered.contains(p)) { + scheduleAperiodWork(p); + registered.add(p); + } + } + } + } + } } diff --git a/core/src/main/java/hudson/model/PeriodicWork.java b/core/src/main/java/hudson/model/PeriodicWork.java index aab8342672..1079553551 100644 --- a/core/src/main/java/hudson/model/PeriodicWork.java +++ b/core/src/main/java/hudson/model/PeriodicWork.java @@ -23,6 +23,7 @@ */ package hudson.model; +import hudson.ExtensionListListener; import hudson.init.Initializer; import hudson.triggers.SafeTimerTask; import hudson.ExtensionPoint; @@ -30,6 +31,8 @@ import hudson.Extension; import hudson.ExtensionList; import jenkins.util.Timer; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import java.util.Random; @@ -99,15 +102,49 @@ public abstract class PeriodicWork extends SafeTimerTask implements ExtensionPoi @Initializer(after= JOB_LOADED) public static void init() { // start all PeriodicWorks - for (PeriodicWork p : PeriodicWork.all()) { - Timer.get().scheduleAtFixedRate(p, p.getInitialDelay(), p.getRecurrencePeriod(), TimeUnit.MILLISECONDS); + ExtensionList extensionList = all(); + extensionList.addListener(new PeriodicWorkExtensionListListener(extensionList)); + for (PeriodicWork p : extensionList) { + schedulePeriodicWork(p); } } + private static void schedulePeriodicWork(PeriodicWork p) { + Timer.get().scheduleAtFixedRate(p, p.getInitialDelay(), p.getRecurrencePeriod(), TimeUnit.MILLISECONDS); + } + // time constants protected static final long MIN = 1000*60; protected static final long HOUR =60*MIN; protected static final long DAY = 24*HOUR; private static final Random RANDOM = new Random(); + + /** + * ExtensionListener that will kick off any new AperiodWork extensions from plugins that are dynamically + * loaded. + */ + private static class PeriodicWorkExtensionListListener extends ExtensionListListener { + + private final Set registered = new HashSet<>(); + + PeriodicWorkExtensionListListener(ExtensionList initiallyRegistered) { + for (PeriodicWork p : initiallyRegistered) { + registered.add(p); + } + } + + @Override + public void onChange() { + synchronized (registered) { + for (PeriodicWork p : PeriodicWork.all()) { + // it is possibly to programatically remove Extensions but that is rarely used. + if (!registered.contains(p)) { + schedulePeriodicWork(p); + registered.add(p); + } + } + } + } + } } diff --git a/test/src/test/java/hudson/model/AperiodicWorkTest.java b/test/src/test/java/hudson/model/AperiodicWorkTest.java new file mode 100644 index 0000000000..5b81e05c8f --- /dev/null +++ b/test/src/test/java/hudson/model/AperiodicWorkTest.java @@ -0,0 +1,59 @@ +package hudson.model; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import hudson.ExtensionList; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +public class AperiodicWorkTest { + + @Rule + public JenkinsRule jr = new JenkinsRule(); + + + @Test + public void newExtensionsAreScheduled() throws Exception { + TestAperiodicWork tapw = new TestAperiodicWork(); + + int size = AperiodicWork.all().size(); + ExtensionList.lookup(AperiodicWork.class).add(tapw); + + assertThat("we have one new AperiodicWork", AperiodicWork.all(), hasSize(size+1)); + assertThat("The task was run within 15 seconds", tapw.doneSignal.await(15, TimeUnit.SECONDS), is(true)); + } + + private static class TestAperiodicWork extends AperiodicWork { + + CountDownLatch doneSignal = new CountDownLatch(1); + + @Override + public long getRecurrencePeriod() { + // should make this only ever run once initially for testing. + return Long.MAX_VALUE; + } + + @Override + public long getInitialDelay() { + // Don't delay just run it! + return 0L; + } + + @Override + public AperiodicWork getNewInstance() { + return this; + } + + @Override + protected void doAperiodicRun() { + doneSignal.countDown(); + } + } +} \ No newline at end of file diff --git a/test/src/test/java/hudson/model/PeriodicWorkTest.java b/test/src/test/java/hudson/model/PeriodicWorkTest.java new file mode 100644 index 0000000000..fc91dabfde --- /dev/null +++ b/test/src/test/java/hudson/model/PeriodicWorkTest.java @@ -0,0 +1,54 @@ +package hudson.model; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import hudson.ExtensionList; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +public class PeriodicWorkTest { + + @Rule + public JenkinsRule jr = new JenkinsRule(); + + + @Test + public void newExtensionsAreScheduled() throws Exception { + TestPeriodicWork tpw = new TestPeriodicWork(); + + int size = PeriodicWork.all().size(); + ExtensionList.lookup(PeriodicWork.class).add(tpw); + + assertThat("we have one new PeriodicWork", PeriodicWork.all(), hasSize(size+1)); + assertThat("The task was run within 15 seconds", tpw.doneSignal.await(15, TimeUnit.SECONDS), is(true)); + } + + private static class TestPeriodicWork extends PeriodicWork { + + CountDownLatch doneSignal = new CountDownLatch(1); + + @Override + public long getRecurrencePeriod() { + // should make this only ever run once initially for testing. + return Long.MAX_VALUE; + } + + @Override + public long getInitialDelay() { + // Don't delay just run it! + return 0L; + } + + @Override + protected void doRun() throws Exception { + doneSignal.countDown(); + } + } +} \ No newline at end of file From 8efcbd8fe073931d0841837e367b080c6b11af9c Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Fri, 1 Jun 2018 19:42:14 +0200 Subject: [PATCH 08/11] [JENKINS-51603] - Use CheckForNull instead of Nullable (cherry picked from commit d79c6afd2163a30b549314c7c1185bf8bd1fd149) --- core/src/main/java/hudson/slaves/JNLPLauncher.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/hudson/slaves/JNLPLauncher.java b/core/src/main/java/hudson/slaves/JNLPLauncher.java index eeafc2bb7f..8307e1c71b 100644 --- a/core/src/main/java/hudson/slaves/JNLPLauncher.java +++ b/core/src/main/java/hudson/slaves/JNLPLauncher.java @@ -31,6 +31,7 @@ import hudson.model.DescriptorVisibilityFilter; import hudson.model.TaskListener; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; + import jenkins.model.Jenkins; import jenkins.slaves.RemotingWorkDirSettings; import org.jenkinsci.Symbol; @@ -70,7 +71,7 @@ public class JNLPLauncher extends ComputerLauncher { private RemotingWorkDirSettings workDirSettings; @DataBoundConstructor - public JNLPLauncher(@CheckForNull String tunnel, @CheckForNull String vmargs, @Nonnull RemotingWorkDirSettings workDirSettings) { + public JNLPLauncher(@CheckForNull String tunnel, @CheckForNull String vmargs, @CheckForNull RemotingWorkDirSettings workDirSettings) { this.tunnel = Util.fixEmptyAndTrim(tunnel); this.vmargs = Util.fixEmptyAndTrim(vmargs); this.workDirSettings = workDirSettings; From a94769781ed03ed30200fa677347d02c44876c53 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 4 Jun 2018 16:07:12 -0400 Subject: [PATCH 09/11] [JENKINS-51603] The proper way to make a parameter optional is to use @DataBoundSetter. (cherry picked from commit 98b37699152595f35fce7387be0ee1717596650d) --- .../main/java/hudson/slaves/JNLPLauncher.java | 38 +++++++++++++------ .../java/hudson/slaves/JNLPLauncherTest.java | 9 +++-- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/hudson/slaves/JNLPLauncher.java b/core/src/main/java/hudson/slaves/JNLPLauncher.java index 8307e1c71b..bed08613d8 100644 --- a/core/src/main/java/hudson/slaves/JNLPLauncher.java +++ b/core/src/main/java/hudson/slaves/JNLPLauncher.java @@ -38,6 +38,7 @@ import org.jenkinsci.Symbol; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; /** * {@link ComputerLauncher} via JNLP. @@ -68,21 +69,29 @@ public class JNLPLauncher extends ComputerLauncher { public final String vmargs; @Nonnull - private RemotingWorkDirSettings workDirSettings; - - @DataBoundConstructor + private RemotingWorkDirSettings workDirSettings = RemotingWorkDirSettings.getEnabledDefaults(); + + /** + * Constructor. + * @param tunnel Tunnel settings + * @param vmargs JVM arguments + * @param workDirSettings Settings for Work Directory management in Remoting. + * If {@code null}, {@link RemotingWorkDirSettings#getEnabledDefaults()} + * will be used to enable work directories by default in new agents. + * @since 2.68 + */ + @Deprecated public JNLPLauncher(@CheckForNull String tunnel, @CheckForNull String vmargs, @CheckForNull RemotingWorkDirSettings workDirSettings) { - this.tunnel = Util.fixEmptyAndTrim(tunnel); - this.vmargs = Util.fixEmptyAndTrim(vmargs); - this.workDirSettings = workDirSettings; + this(tunnel, vmargs); + if (workDirSettings != null) { + setWorkDirSettings(workDirSettings); + } } - @Deprecated - public JNLPLauncher(String tunnel, String vmargs) { - // TODO: Enable workDir by default in API? Otherwise classes inheriting from JNLPLauncher - // will need to enable the feature by default as well. - // https://github.com/search?q=org%3Ajenkinsci+%22extends+JNLPLauncher%22&type=Code - this(tunnel, vmargs, RemotingWorkDirSettings.getDisabledDefaults()); + @DataBoundConstructor + public JNLPLauncher(@CheckForNull String tunnel, @CheckForNull String vmargs) { + this.tunnel = Util.fixEmptyAndTrim(tunnel); + this.vmargs = Util.fixEmptyAndTrim(vmargs); } /** @@ -122,6 +131,11 @@ public class JNLPLauncher extends ComputerLauncher { public RemotingWorkDirSettings getWorkDirSettings() { return workDirSettings; } + + @DataBoundSetter + public final void setWorkDirSettings(@Nonnull RemotingWorkDirSettings workDirSettings) { + this.workDirSettings = workDirSettings; + } @Override public boolean isLaunchSupported() { diff --git a/test/src/test/java/hudson/slaves/JNLPLauncherTest.java b/test/src/test/java/hudson/slaves/JNLPLauncherTest.java index 2d34ae63eb..702673bbda 100644 --- a/test/src/test/java/hudson/slaves/JNLPLauncherTest.java +++ b/test/src/test/java/hudson/slaves/JNLPLauncherTest.java @@ -148,10 +148,9 @@ public class JNLPLauncherTest { @Test @Issue("JENKINS-44112") + @SuppressWarnings("deprecation") public void testDefaults() throws Exception { - String errorMsg = "Work directory should be disabled for agents created via old API"; - assertTrue(errorMsg, new JNLPLauncher().getWorkDirSettings().isDisabled()); - assertTrue(errorMsg, new JNLPLauncher(null, null).getWorkDirSettings().isDisabled()); + assertTrue("Work directory should be disabled for agents created via old API", new JNLPLauncher().getWorkDirSettings().isDisabled()); } @Test @@ -286,8 +285,12 @@ public class JNLPLauncherTest { DumbSlave s = j.createSlave(); JNLPLauncher original = new JNLPLauncher("a", "b"); s.setLauncher(original); + j.assertEqualDataBoundBeans(((JNLPLauncher) s.getLauncher()).getWorkDirSettings(), RemotingWorkDirSettings.getEnabledDefaults()); + RemotingWorkDirSettings custom = new RemotingWorkDirSettings(false, null, "custom", false); + ((JNLPLauncher) s.getLauncher()).setWorkDirSettings(custom); HtmlPage p = j.createWebClient().getPage(s, "configure"); j.submit(p.getFormByName("config")); j.assertEqualBeans(original,s.getLauncher(),"tunnel,vmargs"); + j.assertEqualDataBoundBeans(((JNLPLauncher) s.getLauncher()).getWorkDirSettings(), custom); } } From 37d3a2e5ee48e1bf9242a79707973e6f074bcc38 Mon Sep 17 00:00:00 2001 From: Pavel Avgustinov Date: Fri, 8 Jun 2018 23:31:46 +0100 Subject: [PATCH 10/11] Fix potential deadlock between queue maintenance and asynchronous execution (#3354) [JENKINS-46248] - Fix potential deadlock between queue maintenance and asynchronous execution (cherry picked from commit 01b1f1ddbcad7ed8ea781e41ba4bd8d890f13c67) --- core/src/main/java/hudson/model/Executor.java | 3 +- .../model/queue/AsynchronousExecution.java | 28 ++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/hudson/model/Executor.java b/core/src/main/java/hudson/model/Executor.java index 8c02d8ff64..3b160ae927 100644 --- a/core/src/main/java/hudson/model/Executor.java +++ b/core/src/main/java/hudson/model/Executor.java @@ -431,11 +431,12 @@ public class Executor extends Thread implements ModelObject { } catch (AsynchronousExecution x) { lock.writeLock().lock(); try { - x.setExecutor(this); + x.setExecutorWithoutCompleting(this); this.asynchronousExecution = x; } finally { lock.writeLock().unlock(); } + x.maybeComplete(); } catch (Throwable e) { problems = e; } finally { diff --git a/core/src/main/java/jenkins/model/queue/AsynchronousExecution.java b/core/src/main/java/jenkins/model/queue/AsynchronousExecution.java index 7ed0816572..c99dd40de6 100644 --- a/core/src/main/java/jenkins/model/queue/AsynchronousExecution.java +++ b/core/src/main/java/jenkins/model/queue/AsynchronousExecution.java @@ -39,6 +39,7 @@ import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; import jenkins.model.Jenkins; import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; /** @@ -61,7 +62,7 @@ public abstract class AsynchronousExecution extends RuntimeException { /** * Initially null, and usually stays null. - * If {@link #completed} is called before {@link #setExecutor}, then either {@link #NULL} for success, or the error. + * If {@link #completed} is called before {@link #setExecutorWithoutCompleting}, then either {@link #NULL} for success, or the error. */ @GuardedBy("this") private @CheckForNull Throwable result; @@ -98,7 +99,7 @@ public abstract class AsynchronousExecution extends RuntimeException { /** * Obtains the associated executor. - * @return Associated Executor. May be {@code null} if {@link #setExecutor(hudson.model.Executor)} + * @return Associated Executor. May be {@code null} if {@link #setExecutorWithoutCompleting(hudson.model.Executor)} * has not been called yet. */ @CheckForNull @@ -106,13 +107,26 @@ public abstract class AsynchronousExecution extends RuntimeException { return executor; } + /** + * Set the executor without notifying it about task completion. + * The caller must also call {@link #maybeComplete()} + * after releasing any problematic locks. + */ @Restricted(NoExternalUse.class) - public synchronized final void setExecutor(@Nonnull Executor executor) { - assert this.executor==null; - + public synchronized final void setExecutorWithoutCompleting(@Nonnull Executor executor) { + assert this.executor == null; this.executor = executor; - if (result!=null) { - executor.completedAsynchronous( result!=NULL ? result : null ); + } + + /** + * If there is a pending completion notification, deliver it to the executor. + * Must be called after {@link #setExecutorWithoutCompleting(Executor)}. + */ + @Restricted(NoExternalUse.class) + public synchronized final void maybeComplete() { + assert this.executor != null; + if (result != null) { + executor.completedAsynchronous(result != NULL ? result : null); result = null; } } From 0077685929982496995c7fe4bb04ce80c6e0d78a Mon Sep 17 00:00:00 2001 From: ValHaris Date: Sat, 14 Jul 2018 16:53:07 +0200 Subject: [PATCH 11/11] [JENKINS-52534] Add java.time.Ser to security whitelisted classes (JEP-200) (#3542) (cherry picked from commit dfd5dcbec2dcbe2d9aa84a4901754119ad298950) --- core/src/main/resources/jenkins/security/whitelisted-classes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/resources/jenkins/security/whitelisted-classes.txt b/core/src/main/resources/jenkins/security/whitelisted-classes.txt index 3ad58670b6..3ddb8d841a 100644 --- a/core/src/main/resources/jenkins/security/whitelisted-classes.txt +++ b/core/src/main/resources/jenkins/security/whitelisted-classes.txt @@ -59,6 +59,7 @@ java.math.BigInteger java.net.URI java.net.URL java.security.KeyRep +java.time.Ser java.util.ArrayDeque java.util.ArrayList java.util.Arrays$ArrayList