diff --git a/bom/pom.xml b/bom/pom.xml index 991070392674..d57d6aced5e3 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -191,7 +191,7 @@ THE SOFTWARE. com.github.jnr jnr-posix - 3.1.11 + 3.1.12 org.kohsuke @@ -288,6 +288,11 @@ THE SOFTWARE. robust-http-client 1.2 + + org.jenkins-ci + symbol-annotation + 1.1 + com.sun.mail jakarta.mail diff --git a/core/pom.xml b/core/pom.xml index a1e62367dde5..7af7df22c56d 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -479,10 +479,9 @@ THE SOFTWARE. org.jvnet.robust-http-client robust-http-client - + org.jenkins-ci symbol-annotation - 1.1 diff --git a/core/src/main/java/hudson/model/NullTaskListener.java b/core/src/main/java/hudson/model/NullTaskListener.java new file mode 100644 index 000000000000..5d0454c512b7 --- /dev/null +++ b/core/src/main/java/hudson/model/NullTaskListener.java @@ -0,0 +1,42 @@ +/* + * The MIT License + * + * Copyright 2021 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 java.io.PrintStream; +import org.apache.commons.io.output.NullPrintStream; + +/** + * @see TaskListener#NULL + */ +class NullTaskListener implements TaskListener { + + private static final long serialVersionUID = 1L; + + @Override + public PrintStream getLogger() { + return new NullPrintStream(); + } + +} diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index a15dedce5a6e..64af981be6fc 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -3103,7 +3103,12 @@ public static Queue getInstance() { */ @Initializer(after=JOB_CONFIG_ADAPTED) public static void init(Jenkins h) { - h.getQueue().load(); + Queue queue = h.getQueue(); + Item[] items = queue.getItems(); + if (items.length > 0) { + LOGGER.warning(() -> "Loading queue will discard previously scheduled items: " + Arrays.toString(items)); + } + queue.load(); } /** diff --git a/core/src/main/java/hudson/model/TaskListener.java b/core/src/main/java/hudson/model/TaskListener.java index 4b05ede48c0f..a466613d027f 100644 --- a/core/src/main/java/hudson/model/TaskListener.java +++ b/core/src/main/java/hudson/model/TaskListener.java @@ -27,7 +27,6 @@ import hudson.console.ConsoleNote; import hudson.console.HyperlinkNote; import hudson.remoting.Channel; -import hudson.util.NullStream; import hudson.util.StreamTaskListener; import java.io.IOException; import java.io.OutputStreamWriter; @@ -155,5 +154,5 @@ default PrintWriter fatalError(String format, Object... args) { /** * {@link TaskListener} that discards the output. */ - TaskListener NULL = new StreamTaskListener(new NullStream()); + TaskListener NULL = new NullTaskListener(); } diff --git a/core/src/main/java/hudson/slaves/Channels.java b/core/src/main/java/hudson/slaves/Channels.java index 9f0a6c80921f..9fb5cab11bb6 100644 --- a/core/src/main/java/hudson/slaves/Channels.java +++ b/core/src/main/java/hudson/slaves/Channels.java @@ -27,6 +27,7 @@ import hudson.Launcher.LocalLauncher; import hudson.Proc; import hudson.model.Computer; +import hudson.model.Executor; import hudson.model.TaskListener; import hudson.remoting.Channel; import hudson.remoting.ChannelBuilder; @@ -108,8 +109,10 @@ public synchronized void join() throws InterruptedException { }; cb.withHeaderStream(header); + Executor executor = Executor.currentExecutor(); + Object context = executor != null ? executor.getOwner() : proc; for (ChannelConfigurator cc : ChannelConfigurator.all()) { - cc.onChannelBuilding(cb,null); // TODO: what to pass as a context? + cc.onChannelBuilding(cb, context); } return cb.build(in,out); @@ -145,8 +148,10 @@ public synchronized void join() throws InterruptedException { }; cb.withHeaderStream(header); + Executor executor = Executor.currentExecutor(); + Object context = executor != null ? executor.getOwner() : proc; for (ChannelConfigurator cc : ChannelConfigurator.all()) { - cc.onChannelBuilding(cb,null); // TODO: what to pass as a context? + cc.onChannelBuilding(cb, context); } return cb.build(proc.getInputStream(),proc.getOutputStream()); diff --git a/core/src/main/java/jenkins/formelementpath/FormElementPathPageDecorator.java b/core/src/main/java/jenkins/formelementpath/FormElementPathPageDecorator.java new file mode 100644 index 000000000000..07efd8f0ddd1 --- /dev/null +++ b/core/src/main/java/jenkins/formelementpath/FormElementPathPageDecorator.java @@ -0,0 +1,20 @@ +package jenkins.formelementpath; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.Extension; +import hudson.Main; +import hudson.model.PageDecorator; +import jenkins.util.SystemProperties; + +@Extension +public class FormElementPathPageDecorator extends PageDecorator { + + @SuppressFBWarnings("MS_SHOULD_BE_FINAL") + private static /*almost final */ boolean ENABLED = Main.isUnitTest || + SystemProperties.getBoolean(FormElementPathPageDecorator.class.getName() + ".enabled"); + + public boolean isEnabled() { + return ENABLED; + } + +} diff --git a/core/src/main/java/jenkins/security/ChannelConfigurator.java b/core/src/main/java/jenkins/security/ChannelConfigurator.java index 4bd9f0638f21..77923f50fe48 100644 --- a/core/src/main/java/jenkins/security/ChannelConfigurator.java +++ b/core/src/main/java/jenkins/security/ChannelConfigurator.java @@ -3,9 +3,13 @@ import edu.umd.cs.findbugs.annotations.Nullable; import hudson.ExtensionList; import hudson.ExtensionPoint; +import hudson.Proc; import hudson.remoting.Channel; import hudson.remoting.ChannelBuilder; +import hudson.slaves.Channels; import hudson.slaves.SlaveComputer; +import java.io.OutputStream; +import java.util.concurrent.ExecutorService; /** * Intercepts the new creation of {@link Channel} and tweak its configuration. @@ -29,6 +33,8 @@ public abstract class ChannelConfigurator implements ExtensionPoint { *
*
{@link SlaveComputer} *
When a channel is being established to talk to a agent. + *
{@link Proc} + *
When {@link Channels#forProcess(String, ExecutorService, Process, OutputStream)} or overloads are used without a contextual {@link SlaveComputer}. *
*/ public void onChannelBuilding(ChannelBuilder builder, @Nullable Object context) {} diff --git a/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java b/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java index ef4955213db7..c591827ec8ee 100644 --- a/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java +++ b/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java @@ -26,12 +26,17 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; +import hudson.ExtensionList; import hudson.remoting.ChannelBuilder; +import hudson.remoting.Command; +import hudson.remoting.Request; import java.io.File; +import java.lang.reflect.Field; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.ReflectiveFilePathFilter; import jenkins.security.ChannelConfigurator; +import jenkins.telemetry.impl.SlaveToMasterFileCallableUsage; import jenkins.util.SystemProperties; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -59,6 +64,17 @@ protected boolean op(String op, File f) throws SecurityException { LOGGER.log(Level.FINE, "agent allowed to {0} {1}", new Object[] {op, f}); return true; } else { + try { + Field current = Request.class.getDeclaredField("CURRENT"); + current.setAccessible(true); + Field createdAt = Command.class.getDeclaredField("createdAt"); + createdAt.setAccessible(true); + Throwable trace = (Throwable) createdAt.get(((ThreadLocal) current.get(null)).get()); + ExtensionList.lookupSingleton(SlaveToMasterFileCallableUsage.class).recordTrace(trace); + LOGGER.log(Level.WARNING, "Permitting agent-to-controller '" + op + "' on '" + f + "'. This is deprecated and will soon be rejected. Learn more: https://www.jenkins.io/redirect/permitted-agent-to-controller-file-access", trace); + } catch (Exception x) { + LOGGER.log(Level.WARNING, null, x); + } return false; } } diff --git a/core/src/main/java/jenkins/telemetry/impl/SlaveToMasterFileCallableUsage.java b/core/src/main/java/jenkins/telemetry/impl/SlaveToMasterFileCallableUsage.java new file mode 100644 index 000000000000..dd628b767773 --- /dev/null +++ b/core/src/main/java/jenkins/telemetry/impl/SlaveToMasterFileCallableUsage.java @@ -0,0 +1,75 @@ +/* + * The MIT License + * + * Copyright 2021 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.telemetry.impl; + +import hudson.Extension; +import hudson.Functions; +import java.time.LocalDate; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; +import jenkins.SlaveToMasterFileCallable; +import jenkins.security.s2m.DefaultFilePathFilter; +import jenkins.telemetry.Telemetry; +import net.sf.json.JSONObject; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Records when {@link DefaultFilePathFilter} found {@link SlaveToMasterFileCallable} or similar being used. + */ +@Extension +@Restricted(NoExternalUse.class) +public class SlaveToMasterFileCallableUsage extends Telemetry { + + private Set traces = new TreeSet<>(); + + @Override + public String getDisplayName() { + return "Access to files on controllers from code running on an agent"; + } + + @Override + public LocalDate getStart() { + return LocalDate.of(2021, 11, 4); // https://www.jenkins.io/security/advisory/2021-11-04/ + } + + @Override + public LocalDate getEnd() { + return LocalDate.of(2022, 3, 1); + } + + @Override + public synchronized JSONObject createContent() { + JSONObject json = JSONObject.fromObject(Collections.singletonMap("traces", traces)); + traces.clear(); + return json; + } + + public synchronized void recordTrace(Throwable trace) { + traces.add(Functions.printThrowable(trace).replaceAll("@[a-f0-9]+", "@…")); + } + +} diff --git a/core/src/main/resources/jenkins/formelementpath/FormElementPathPageDecorator/footer.jelly b/core/src/main/resources/jenkins/formelementpath/FormElementPathPageDecorator/footer.jelly new file mode 100644 index 000000000000..1b92a93950fe --- /dev/null +++ b/core/src/main/resources/jenkins/formelementpath/FormElementPathPageDecorator/footer.jelly @@ -0,0 +1,6 @@ + + + + + + diff --git a/core/src/main/resources/jenkins/formelementpath/form-element-path.js b/core/src/main/resources/jenkins/formelementpath/form-element-path.js new file mode 100644 index 000000000000..0bba9171438e --- /dev/null +++ b/core/src/main/resources/jenkins/formelementpath/form-element-path.js @@ -0,0 +1,196 @@ +/** + * Adds a 'path' attribute to form elements in the DOM. + * This is useful for providing stable selectors for UI testing. + * + * Instead of selecting by xpath with something like div/span/input[text() = 'Name'] + * You can use the path attribute: /org-jenkinsci-plugins-workflow-libs-FolderLibraries/libraries/name + */ +document.addEventListener("DOMContentLoaded", function(){ + // most of this is copied from hudson-behaviour.js + function buildFormTree(form) { + form.formDom = {}; // root object + + var doms = []; // DOMs that we added 'formDom' for. + doms.push(form); + + function addProperty(parent, name, value) { + name = shortenName(name); + if (parent[name] != null) { + if (parent[name].push == null) // is this array? + parent[name] = [parent[name]]; + parent[name].push(value); + } else { + parent[name] = value; + } + } + + // find the grouping parent node, which will have @name. + // then return the corresponding object in the map + function findParent(e) { + var p = findFormParent(e, form); + if (p == null) return {}; + + var m = p.formDom; + if (m == null) { + // this is a new grouping node + doms.push(p); + p.formDom = m = {}; + addProperty(findParent(p), p.getAttribute("name"), p); + } + return m; + } + + var jsonElement = null; + + for (var i = 0; i < form.elements.length; i++) { + var e = form.elements[i]; + if (e.name == "json") { + jsonElement = e; + continue; + } + if (e.tagName == "FIELDSET") + continue; + if (e.tagName == "SELECT" && e.multiple) { + addProperty(findParent(e), e.name, e); + continue; + } + + var p; + var type = e.getAttribute("type"); + if (type == null) type = ""; + switch (type.toLowerCase()) { + case "button": + var element + // modern buttons aren't wrapped in spans + if (e.classList.contains('jenkins-button')) { + element = e + } else { + p = findParent(e); + element = e.parentNode.parentNode; // YUI's surrounding that has interesting classes + } + var name = null; + ["repeatable-add", "repeatable-delete", "hetero-list-add", "expand-button", "advanced-button", "apply-button", "validate-button"] + .forEach(function (clazz) { + if (element.classList.contains(clazz)) { + name = clazz; + } + }); + if (name == null) { + if (name == null) { + element = element.parentNode.previousSibling; + if (element != null && element.classList && element.classList.contains('repeatable-insertion-point')) { + name = "hetero-list-add"; + } + } + } + if (name != null) { + addProperty(p, name, e); + } + break; + case "submit": + break; + case "checkbox": + case "radio": + p = findParent(e); + if (e.groupingNode) { + e.formDom = {}; + } + addProperty(p, e.name, e); + break; + case "file": + // to support structured form submission with file uploads, + // rename form field names to unique ones, and leave this name mapping information + // in JSON. this behavior is backward incompatible, so only do + // this when + p = findParent(e); + if (e.getAttribute("jsonAware") != null) { + var on = e.getAttribute("originalName"); + if (on != null) { + addProperty(p, on, e); + } else { + addProperty(p, e.name, e); + } + } + break; + // otherwise fall through + default: + p = findParent(e); + addProperty(p, e.name, e); + break; + } + } + + function annotate(e, path) { + e.setAttribute("path", path); + var o = e.formDom || {}; + for (var key in o) { + var v = o[key]; + + function child(v, i) { + var suffix = null; + var newKey = key; + if (v.parentNode.className && v.parentNode.className.indexOf("one-each") > -1 && v.parentNode.className.indexOf("honor-order") > -1) { + suffix = v.getAttribute("descriptorId").split(".").pop() + } else if (v.getAttribute("type") == "radio") { + suffix = v.value + while (newKey.substring(0, 8) == 'removeme') + newKey = newKey.substring(newKey.indexOf('_', 8) + 1); + } else if (v.getAttribute("suffix") != null) { + suffix = v.getAttribute("suffix") + } else { + if (i > 0) + suffix = i; + } + if (suffix == null) suffix = ""; + else suffix = '[' + suffix + ']'; + + annotate(v, path + "/" + newKey + suffix); + } + + if (v instanceof Array) { + var i = 0; + v.forEach(function (v) { + child(v, i++) + }) + } else { + child(v, 0) + } + } + + } + + annotate(form, ""); + + // clean up + for (i = 0; i < doms.length; i++) + doms[i].formDom = null; + + return true; + } + + function applyAll() { + document.querySelectorAll("FORM").forEach(function (e) { + buildFormTree(e); + }) + } + + /* JavaScript sometimes re-arranges the DOM and doesn't call layout callback + * known cases: YUI buttons, CodeMirror. + * We run apply twice to work around this, once immediately so that most cases work and the tests don't need to wait, + * and once to catch the edge cases. + */ + function hardenedApplyAll () { + applyAll(); + + setTimeout(function () { + applyAll(); + }, 1000); + } + + hardenedApplyAll(); + + layoutUpdateCallback.add(hardenedApplyAll) + + // expose this globally so that Selenium can call it + window.recomputeFormElementPath = hardenedApplyAll; +}); diff --git a/core/src/main/resources/jenkins/telemetry/impl/SlaveToMasterFileCallableUsage/description.jelly b/core/src/main/resources/jenkins/telemetry/impl/SlaveToMasterFileCallableUsage/description.jelly new file mode 100644 index 000000000000..83f02a200bf1 --- /dev/null +++ b/core/src/main/resources/jenkins/telemetry/impl/SlaveToMasterFileCallableUsage/description.jelly @@ -0,0 +1,13 @@ + + + Jenkins controllers construct a remote-procedure-call (RPC) channel to agents to instruct them to perform work. + This channel is bidirectional and in a handful of cases agents made requests of the controller. + This was always tricky to secure, + and recently + the category of usages which involved access to files was more tightly restricted than before; + Jenkins developers are considering disabling this kind of usage entirely. + Since it is difficult to determine via static analysis or even manual code inspection which plugins are using this system, + we are collecting information on how widely it is used. + The data includes names of Java classes mainly in Jenkins core and plugins as well as method names and line numbers. + It does not include the names of files being accessed or anything else not determined by versions of software components in use. + diff --git a/core/src/main/resources/lib/form/repeatable/repeatable.js b/core/src/main/resources/lib/form/repeatable/repeatable.js index 1bd4efddf698..3260051d1795 100644 --- a/core/src/main/resources/lib/form/repeatable/repeatable.js +++ b/core/src/main/resources/lib/form/repeatable/repeatable.js @@ -121,8 +121,10 @@ var repeatableSupport = { a.onComplete.subscribe(function() { var p = n.parentNode; p.removeChild(n); - if (p.tag) + if (p.tag) { p.tag.update(); + } + layoutUpdateCallback.call(); }); a.animate(); }, @@ -145,6 +147,7 @@ var repeatableSupport = { updateOptionalBlock(input, false); } } + layoutUpdateCallback.call(); } }; @@ -185,7 +188,7 @@ Behaviour.specify("INPUT.repeatable-delete", 'repeatable', 0, function(e) { e = be = null; // avoid memory leak }); - // radio buttons in repeatable content +// radio buttons in repeatable content // Needs to run before the radioBlock behavior so that names are already unique. Behaviour.specify("DIV.repeated-chunk", 'repeatable', -200, function(d) { var inputs = d.getElementsByTagName('INPUT'); diff --git a/pom.xml b/pom.xml index d5bd491675e0..231f7a20bad3 100644 --- a/pom.xml +++ b/pom.xml @@ -70,7 +70,7 @@ THE SOFTWARE. - 2.322 + 2.323 -SNAPSHOT