diff --git a/.gitattributes b/.gitattributes index 63ca2fc710ef2..85a9c64aa951e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -39,4 +39,4 @@ # Yarn # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored -/war/.yarn/plugins/** binary +/.yarn/plugins/** binary diff --git a/.gitignore b/.gitignore index f7314d0646ec5..41747ca0cd230 100644 --- a/.gitignore +++ b/.gitignore @@ -65,9 +65,7 @@ junit.xml # Yarn # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored -.pnp.* .yarn/* -.yarnrc.yml !.yarn/patches !.yarn/plugins !.yarn/sdks @@ -78,7 +76,4 @@ node/ node_modules/ # Generated JavaScript Bundles -jsbundles - -# In case someone accidentally runs npm install instead of yarn install -package-lock.json +war/src/main/webapp/jsbundles/ diff --git a/.prettierignore b/.prettierignore index 042e5d268c0c7..72610f04b4197 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,13 +7,11 @@ node/ .git -.yarnrc.yml - # libraries / external deps / generated files -war/src/main/js/plugin-setup-wizard/bootstrap-detached.js +src/main/js/plugin-setup-wizard/bootstrap-detached.js war/src/main/webapp/scripts/yui war/src/main/webapp/jsbundles/ -war/src/main/scss/_bootstrap.scss +src/main/scss/_bootstrap.scss # test files that we don't need formatted test/src/test/resources diff --git a/.stylelintrc.js b/.stylelintrc.js index 711c7c77f83a3..cfea225e3035d 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -1,7 +1,7 @@ module.exports = { extends: "stylelint-config-standard", customSyntax: "postcss-scss", - ignoreFiles: ["war/src/main/scss/_bootstrap.scss"], + ignoreFiles: ["src/main/scss/_bootstrap.scss"], rules: { "no-descending-specificity": null, "selector-class-pattern": "[a-z]", diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000000000..5004f58b1bfdb --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,2 @@ +enableGlobalCache: false +nodeLinker: node-modules diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1be9d4f3f9cfe..98650aa99de63 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ This page provides information about contributing code to the Jenkins core codeb 3. Install the necessary development tools. In order to develop Jenkins, you need the following: - Java Development Kit (JDK) 17 or 21. In the Jenkins project we usually use [Eclipse Temurin](https://adoptium.net/) or [OpenJDK](https://openjdk.java.net/), but you can use other JDKs as well. - - Apache Maven 3.8.1 or above. You can [download Maven here](https://maven.apache.org/download.cgi). + - Apache Maven 3.9.6 or above. You can [download Maven here](https://maven.apache.org/download.cgi). In the Jenkins project we usually use the most recent Maven release. - Any IDE which supports importing Maven projects. 4. Set up your development environment as described in [Preparing for Plugin Development](https://www.jenkins.io/doc/developer/tutorial/prepare/) @@ -53,12 +53,12 @@ MAVEN_OPTS='--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/ja ### Running the Yarn frontend build > [!TIP] -> If you already have Node.js installed, you do not need to change your path. Start using `yarn` by enabling [Corepack](https://yarnpkg.com/corepack) with `corepack enable`, if it isn't already; this will add the `yarn` binary to your PATH. +> If you already have Node.js installed, you do not need to change your path. Start using Yarn by enabling [Corepack](https://yarnpkg.com/corepack) with `corepack enable`, if it isn't already; this will add the `yarn` binary to your path. To run the Yarn frontend build, after [building the WAR file](#building-the-war-file), add the downloaded versions of Node and Yarn to your path: ```sh -export PATH=$PWD/node:$PWD/node/yarn/dist/bin:$PATH +export PATH=$PWD/node:$PWD/node/node_modules/corepack/shims:$PATH ``` Then you can run Yarn with e.g. diff --git a/ath.sh b/ath.sh index 1816e037466bf..7009d692b8d0f 100644 --- a/ath.sh +++ b/ath.sh @@ -6,7 +6,7 @@ set -o xtrace cd "$(dirname "$0")" # https://github.com/jenkinsci/acceptance-test-harness/releases -export ATH_VERSION=5997.v2a_1a_696620a_0 +export ATH_VERSION=6038.v190f938efc87 if [[ $# -eq 0 ]]; then export JDK=17 diff --git a/bom/pom.xml b/bom/pom.xml index 47321614432c4..0065b595ae9ac 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -88,7 +88,7 @@ THE SOFTWARE. com.google.guava guava - 33.3.0-jre + 33.3.1-jre @@ -250,7 +250,7 @@ THE SOFTWARE. org.jenkins-ci annotation-indexer - 1.17 + 1.18 org.jenkins-ci @@ -260,27 +260,27 @@ THE SOFTWARE. org.jenkins-ci crypto-util - 1.9 + 1.10 org.jenkins-ci memory-monitor - 1.12 + 1.13 org.jenkins-ci symbol-annotation - 1.24 + 1.25 org.jenkins-ci task-reactor - 1.8 + 1.9 org.jenkins-ci version-number - 1.11 + 1.12 org.jenkins-ci.main @@ -352,6 +352,12 @@ THE SOFTWARE. stapler-groovy ${stapler.version} + + + org.ow2.asm + asm + 9.7.1 + org.samba.jcifs jcifs diff --git a/cli/pom.xml b/cli/pom.xml index 6309811a2e8bc..2473629d23cc8 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -15,7 +15,7 @@ https://github.com/jenkinsci/jenkins - 2.13.2 + 2.14.0 diff --git a/core/src/main/java/hudson/logging/LogRecorder.java b/core/src/main/java/hudson/logging/LogRecorder.java index ad4c40028547b..c11c1bbfa57ef 100644 --- a/core/src/main/java/hudson/logging/LogRecorder.java +++ b/core/src/main/java/hudson/logging/LogRecorder.java @@ -556,7 +556,7 @@ public void delete() throws IOException { loggers.forEach(Target::disable); getParent().getRecorders().forEach(logRecorder -> logRecorder.getLoggers().forEach(Target::enable)); - SaveableListener.fireOnChange(this, getConfigFile()); + SaveableListener.fireOnDeleted(this, getConfigFile()); } /** diff --git a/core/src/main/java/hudson/markup/MarkupFormatter.java b/core/src/main/java/hudson/markup/MarkupFormatter.java index a3bf53a739a19..95af637af9749 100644 --- a/core/src/main/java/hudson/markup/MarkupFormatter.java +++ b/core/src/main/java/hudson/markup/MarkupFormatter.java @@ -62,9 +62,14 @@ * This is an extension point in Hudson, allowing plugins to implement different markup formatters. * *

- * Implement the following methods to enable and control CodeMirror syntax highlighting - * public String getCodeMirrorMode() // return null to disable CodeMirror dynamically - * public String getCodeMirrorConfig() + * Implement the following methods to enable and control CodeMirror syntax highlighting: + *

    + *
  • public String getCodeMirrorMode() (return null to disable CodeMirror dynamically)
  • + *
  • + * public String getCodeMirrorConfig() (JSON snippet without surrounding curly braces, e.g., "mode": "text/css". + * Historically this allowed invalid JSON, but since TODO it needs to be properly quoted etc. + *
  • + *
* *

Views

*

diff --git a/core/src/main/java/hudson/model/AbstractItem.java b/core/src/main/java/hudson/model/AbstractItem.java index 8ba7aafe15cdc..f31316c316569 100644 --- a/core/src/main/java/hudson/model/AbstractItem.java +++ b/core/src/main/java/hudson/model/AbstractItem.java @@ -59,8 +59,6 @@ import java.util.ListIterator; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.xml.transform.Source; import javax.xml.transform.TransformerException; import javax.xml.transform.sax.SAXSource; @@ -70,6 +68,7 @@ import jenkins.model.Jenkins; import jenkins.model.Loadable; import jenkins.model.queue.ItemDeletion; +import jenkins.security.ExtendedReadRedaction; import jenkins.security.NotReallyRoleSensitiveCallable; import jenkins.security.stapler.StaplerNotDispatchable; import jenkins.util.SystemProperties; @@ -815,6 +814,7 @@ public void delete() throws IOException, InterruptedException { ItemDeletion.deregister(this); } } + SaveableListener.fireOnDeleted(this, getConfigFile()); getParent().onDeleted(AbstractItem.this); Jenkins.get().rebuildDependencyGraphAsync(); } @@ -870,11 +870,11 @@ private void doConfigDotXmlImpl(StaplerRequest2 req, StaplerResponse2 rsp) rsp.sendError(SC_BAD_REQUEST); } - static final Pattern SECRET_PATTERN = Pattern.compile(">(" + Secret.ENCRYPTED_VALUE_PATTERN + ")<"); /** * Writes {@code config.xml} to the specified output stream. * The user must have at least {@link #EXTENDED_READ}. - * If he lacks {@link #CONFIGURE}, then any {@link Secret}s detected will be masked out. + * If he lacks {@link #CONFIGURE}, then any {@link Secret}s or other sensitive information detected will be masked out. + * @see jenkins.security.ExtendedReadRedaction */ @Restricted(NoExternalUse.class) @@ -886,15 +886,13 @@ public void writeConfigDotXml(OutputStream os) throws IOException { } else { String encoding = configFile.sniffEncoding(); String xml = Files.readString(Util.fileToPath(configFile.getFile()), Charset.forName(encoding)); - Matcher matcher = SECRET_PATTERN.matcher(xml); - StringBuilder cleanXml = new StringBuilder(); - while (matcher.find()) { - if (Secret.decrypt(matcher.group(1)) != null) { - matcher.appendReplacement(cleanXml, ">********<"); - } + + for (ExtendedReadRedaction redaction : ExtendedReadRedaction.all()) { + LOGGER.log(Level.FINE, () -> "Applying redaction " + redaction.getClass().getName()); + xml = redaction.apply(xml); } - matcher.appendTail(cleanXml); - org.apache.commons.io.IOUtils.write(cleanXml.toString(), os, encoding); + + org.apache.commons.io.IOUtils.write(xml, os, encoding); } } diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index 081f73dd16256..1047fe97000c1 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -41,7 +41,6 @@ import hudson.console.AnnotatedLargeText; import hudson.init.Initializer; import hudson.model.Descriptor.FormException; -import hudson.model.Queue.FlyweightTask; import hudson.model.labels.LabelAtom; import hudson.model.queue.WorkUnit; import hudson.node_monitors.AbstractDiskSpaceMonitor; @@ -106,6 +105,9 @@ import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import jenkins.model.DisplayExecutor; +import jenkins.model.IComputer; +import jenkins.model.IDisplayExecutor; import jenkins.model.Jenkins; import jenkins.security.ImpersonatingExecutorService; import jenkins.security.MasterToSlaveCallable; @@ -116,8 +118,6 @@ import jenkins.util.SystemProperties; import jenkins.widgets.HasWidgets; import net.jcip.annotations.GuardedBy; -import org.jenkins.ui.icon.Icon; -import org.jenkins.ui.icon.IconSet; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -150,7 +150,7 @@ * if a {@link Node} is configured (probably temporarily) with 0 executors, * you won't have a {@link Computer} object for it (except for the built-in node, * which always gets its {@link Computer} in case we have no static executors and - * we need to run a {@link FlyweightTask} - see JENKINS-7291 for more discussion.) + * we need to run a {@link Queue.FlyweightTask} - see JENKINS-7291 for more discussion.) * * Also, even if you remove a {@link Node}, it takes time for the corresponding * {@link Computer} to be removed, if some builds are already in progress on that @@ -164,7 +164,7 @@ * @author Kohsuke Kawaguchi */ @ExportedBean -public /*transient*/ abstract class Computer extends Actionable implements AccessControlled, ExecutorListener, DescriptorByNameOwner, StaplerProxy, HasWidgets { +public /*transient*/ abstract class Computer extends Actionable implements AccessControlled, IComputer, ExecutorListener, DescriptorByNameOwner, StaplerProxy, HasWidgets { private final CopyOnWriteArrayList executors = new CopyOnWriteArrayList<>(); // TODO: @@ -351,12 +351,6 @@ public AnnotatedLargeText getLogText() { return new AnnotatedLargeText<>(getLogFile(), Charset.defaultCharset(), false, this); } - @NonNull - @Override - public ACL getACL() { - return Jenkins.get().getAuthorizationStrategy().getACL(this); - } - /** * If the computer was offline (either temporarily or not), * this method will return the cause. @@ -369,14 +363,13 @@ public OfflineCause getOfflineCause() { return offlineCause; } - /** - * If the computer was offline (either temporarily or not), - * this method will return the cause as a string (without user info). - * - * @return - * empty string if the system was put offline without given a cause. - */ + @Override + public boolean hasOfflineCause() { + return offlineCause != null; + } + @Exported + @Override public String getOfflineCauseReason() { if (offlineCause == null) { return ""; @@ -581,9 +574,6 @@ public int getNumExecutors() { return numExecutors; } - /** - * Returns {@link Node#getNodeName() the name of the node}. - */ public @NonNull String getName() { return nodeName != null ? nodeName : ""; } @@ -628,6 +618,7 @@ public BuildTimelineWidget getTimeline() { } @Exported + @Override public boolean isOffline() { return temporarilyOffline || getChannel() == null; } @@ -645,12 +636,6 @@ public boolean isManualLaunchAllowed() { return getRetentionStrategy().isManualLaunchAllowed(this); } - - /** - * Is a {@link #connect(boolean)} operation in progress? - */ - public abstract boolean isConnecting(); - /** * Returns true if this computer is supposed to be launched via inbound protocol. * @deprecated since 2008-05-18. @@ -662,14 +647,8 @@ public boolean isJnlpAgent() { return false; } - /** - * Returns true if this computer can be launched by Hudson proactively and automatically. - * - *

- * For example, inbound agents return {@code false} from this, because the launch process - * needs to be initiated from the agent side. - */ @Exported + @Override public boolean isLaunchSupported() { return true; } @@ -727,14 +706,8 @@ public void setTemporarilyOffline(boolean temporarilyOffline, OfflineCause cause } } - /** - * Returns the icon for this computer. - * - * It is both the recommended and default implementation to serve different icons based on {@link #isOffline} - * - * @see #getIconClassName() - */ @Exported + @Override public String getIcon() { // The machine was taken offline by someone if (isTemporarilyOffline() && getOfflineCause() instanceof OfflineCause.UserCause) return "symbol-computer-disconnected"; @@ -748,19 +721,15 @@ public String getIcon() { } /** - * Returns the class name that will be used to lookup the icon. + * {@inheritDoc} * - * This class name will be added as a class tag to the html img tags where the icon should - * show up followed by a size specifier given by {@link Icon#toNormalizedIconSizeClass(String)} - * The conversion of class tag to src tag is registered through {@link IconSet#addIcon(Icon)} - * - * It is both the recommended and default implementation to serve different icons based on {@link #isOffline} - * - * @see #getIcon() + *

+ * It is both the recommended and default implementation to serve different icons based on {@link #isOffline}. */ @Exported + @Override public String getIconClassName() { - return getIcon(); + return IComputer.super.getIconClassName(); } public String getIconAltText() { @@ -780,6 +749,8 @@ public String getCaption() { return Messages.Computer_Caption(nodeName); } + @Override + @NonNull public String getUrl() { return "computer/" + Util.fullEncode(getName()) + "/"; } @@ -947,19 +918,18 @@ public int countIdle() { return n; } - /** - * Returns the number of {@link Executor}s that are doing some work right now. - */ + @Override public final int countBusy() { return countExecutors() - countIdle(); } /** - * Returns the current size of the executor pool for this computer. + * {@inheritDoc} * This number may temporarily differ from {@link #getNumExecutors()} if there * are busy tasks when the configured size is decreased. OneOffExecutors are * not included in this count. */ + @Override public final int countExecutors() { return executors.size(); } @@ -996,14 +966,14 @@ public List getAllExecutors() { } /** - * Used to render the list of executors. - * @return a snapshot of the executor display information + * {@inheritDoc} * @since 1.607 */ - @Restricted(NoExternalUse.class) - public List getDisplayExecutors() { + @Override + @NonNull + public List getDisplayExecutors() { // The size may change while we are populating, but let's start with a reasonable guess to minimize resizing - List result = new ArrayList<>(executors.size() + oneOffExecutors.size()); + List result = new ArrayList<>(executors.size() + oneOffExecutors.size()); int index = 0; for (Executor e : executors) { if (e.isDisplayCell()) { @@ -1659,15 +1629,8 @@ public Object getTarget() { return e != null ? e.getOwner() : null; } - /** - * Returns {@code true} if the computer is accepting tasks. Needed to allow agents programmatic suspension of task - * scheduling that does not overlap with being offline. - * - * @return {@code true} if the computer is accepting tasks - * @see hudson.slaves.RetentionStrategy#isAcceptingTasks(Computer) - * @see hudson.model.Node#isAcceptingTasks() - */ @OverrideMustInvoke + @Override public boolean isAcceptingTasks() { final Node node = getNode(); return getRetentionStrategy().isAcceptingTasks(this) && (node == null || node.isAcceptingTasks()); @@ -1727,79 +1690,12 @@ public static void relocateOldLogs() { } } - /** - * A value class to provide a consistent snapshot view of the state of an executor to avoid race conditions - * during rendering of the executors list. - * - * @since 1.607 - */ - @Restricted(NoExternalUse.class) - public static class DisplayExecutor implements ModelObject { - - @NonNull - private final String displayName; - @NonNull - private final String url; - @NonNull - private final Executor executor; - - public DisplayExecutor(@NonNull String displayName, @NonNull String url, @NonNull Executor executor) { - this.displayName = displayName; - this.url = url; - this.executor = executor; - } - - @Override - @NonNull - public String getDisplayName() { - return displayName; - } - - @NonNull - public String getUrl() { - return url; - } - - @NonNull - public Executor getExecutor() { - return executor; - } - - @Override - public String toString() { - String sb = "DisplayExecutor{" + "displayName='" + displayName + '\'' + - ", url='" + url + '\'' + - ", executor=" + executor + - '}'; - return sb; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - DisplayExecutor that = (DisplayExecutor) o; - - return executor.equals(that.executor); - } - - @Extension(ordinal = Double.MAX_VALUE) - @Restricted(DoNotUse.class) - public static class InternalComputerListener extends ComputerListener { - @Override - public void onOnline(Computer c, TaskListener listener) throws IOException, InterruptedException { - c.cachedEnvironment = null; - } - } - + @Extension(ordinal = Double.MAX_VALUE) + @Restricted(DoNotUse.class) + public static class InternalComputerListener extends ComputerListener { @Override - public int hashCode() { - return executor.hashCode(); + public void onOnline(Computer c, TaskListener listener) throws IOException, InterruptedException { + c.cachedEnvironment = null; } } diff --git a/core/src/main/java/hudson/model/ComputerSet.java b/core/src/main/java/hudson/model/ComputerSet.java index f25d25a19a50c..fcc0aa4e41e73 100644 --- a/core/src/main/java/hudson/model/ComputerSet.java +++ b/core/src/main/java/hudson/model/ComputerSet.java @@ -30,6 +30,8 @@ import hudson.BulkChange; import hudson.DescriptorExtensionList; import hudson.Extension; +import hudson.ExtensionList; +import hudson.ExtensionPoint; import hudson.Util; import hudson.XmlFile; import hudson.init.Initializer; @@ -47,18 +49,24 @@ import java.lang.reflect.InvocationTargetException; import java.util.AbstractList; import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.model.IComputer; import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithChildren; import jenkins.model.ModelObjectWithContextMenu.ContextMenu; import jenkins.util.Timer; import jenkins.widgets.HasWidgets; import net.sf.json.JSONObject; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; +import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest2; @@ -106,15 +114,36 @@ public static List get_monitors() { return monitors.toList(); } - @Exported(name = "computer", inline = true) + /** + * @deprecated Use {@link #getComputers()} instead. + * @return All {@link Computer} instances managed by this set. + */ + @Deprecated(since = "2.480") public Computer[] get_all() { - return Jenkins.get().getComputers(); + return getComputers().stream().filter(Computer.class::isInstance).toArray(Computer[]::new); + } + + /** + * @return All {@link IComputer} instances managed by this set, sorted by name. + */ + @Exported(name = "computer", inline = true) + public Collection getComputers() { + return ExtensionList.lookupFirst(ComputerSource.class).get().stream().sorted(Comparator.comparing(IComputer::getName)).toList(); + } + + /** + * Allows plugins to override the displayed list of computers. + * + */ + @Restricted(Beta.class) + public interface ComputerSource extends ExtensionPoint { + Collection get(); } @Override public ContextMenu doChildrenContextMenu(StaplerRequest2 request, StaplerResponse2 response) throws Exception { ContextMenu m = new ContextMenu(); - for (Computer c : get_all()) { + for (IComputer c : getComputers()) { m.add(c); } return m; @@ -170,7 +199,7 @@ public int size() { @Exported public int getTotalExecutors() { int r = 0; - for (Computer c : get_all()) { + for (IComputer c : getComputers()) { if (c.isOnline()) r += c.countExecutors(); } @@ -183,7 +212,7 @@ public int getTotalExecutors() { @Exported public int getBusyExecutors() { int r = 0; - for (Computer c : get_all()) { + for (IComputer c : getComputers()) { if (c.isOnline()) r += c.countBusy(); } @@ -195,7 +224,7 @@ public int getBusyExecutors() { */ public int getIdleExecutors() { int r = 0; - for (Computer c : get_all()) + for (IComputer c : getComputers()) if ((c.isOnline() || c.isConnecting()) && c.isAcceptingTasks()) r += c.countIdle(); return r; @@ -214,7 +243,7 @@ public Computer getDynamic(String token, StaplerRequest2 req, StaplerResponse2 r public void do_launchAll(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); - for (Computer c : get_all()) { + for (IComputer c : getComputers()) { if (c.isLaunchSupported()) c.connect(true); } @@ -502,4 +531,13 @@ private static NodeMonitor createDefaultInstance(Descriptor d, bool } return null; } + + @Extension(ordinal = -1) + @Restricted(DoNotUse.class) + public static class ComputerSourceImpl implements ComputerSource { + @Override + public Collection get() { + return Jenkins.get().getComputersCollection(); + } + } } diff --git a/core/src/main/java/hudson/model/Executor.java b/core/src/main/java/hudson/model/Executor.java index 94aa1c770d218..f86dbd7c6c0a0 100644 --- a/core/src/main/java/hudson/model/Executor.java +++ b/core/src/main/java/hudson/model/Executor.java @@ -61,6 +61,7 @@ import java.util.stream.Collectors; import jenkins.model.CauseOfInterruption; import jenkins.model.CauseOfInterruption.UserInterruption; +import jenkins.model.IExecutor; import jenkins.model.InterruptedBuildAction; import jenkins.model.Jenkins; import jenkins.model.queue.AsynchronousExecution; @@ -88,7 +89,7 @@ * @author Kohsuke Kawaguchi */ @ExportedBean -public class Executor extends Thread implements ModelObject { +public class Executor extends Thread implements ModelObject, IExecutor { protected final @NonNull Computer owner; private final Queue queue; private final ReadWriteLock lock = new ReentrantReadWriteLock(); @@ -526,6 +527,7 @@ public void completedAsynchronous(@CheckForNull Throwable error) { * @return * null if the executor is idle. */ + @Override public @CheckForNull Queue.Executable getCurrentExecutable() { lock.readLock().lock(); try { @@ -555,14 +557,8 @@ public Queue.Executable getCurrentExecutableForApi() { return Collections.unmodifiableCollection(causes); } - /** - * Returns the current {@link WorkUnit} (of {@link #getCurrentExecutable() the current executable}) - * that this executor is running. - * - * @return - * null if the executor is idle. - */ @CheckForNull + @Override public WorkUnit getCurrentWorkUnit() { lock.readLock().lock(); try { @@ -601,22 +597,14 @@ public String getDisplayName() { return "Executor #" + getNumber(); } - /** - * Gets the executor number that uniquely identifies it among - * other {@link Executor}s for the same computer. - * - * @return - * a sequential number starting from 0. - */ @Exported + @Override public int getNumber() { return number; } - /** - * Returns true if this {@link Executor} is ready for action. - */ @Exported + @Override public boolean isIdle() { lock.readLock().lock(); try { @@ -705,13 +693,8 @@ public boolean isParking() { return null; } - /** - * Returns the progress of the current build in the number between 0-100. - * - * @return -1 - * if it's impossible to estimate the progress. - */ @Exported + @Override public int getProgress() { long d = executableEstimatedDuration; if (d <= 0) { @@ -725,14 +708,8 @@ public int getProgress() { return num; } - /** - * Returns true if the current build is likely stuck. - * - *

- * This is a heuristics based approach, but if the build is suspiciously taking for a long time, - * this method returns true. - */ @Exported + @Override public boolean isLikelyStuck() { lock.readLock().lock(); try { @@ -754,6 +731,7 @@ public boolean isLikelyStuck() { } } + @Override public long getElapsedTime() { lock.readLock().lock(); try { @@ -777,20 +755,7 @@ public long getTimeSpentInQueue() { } } - /** - * Gets the string that says how long since this build has started. - * - * @return - * string like "3 minutes" "1 day" etc. - */ - public String getTimestampString() { - return Util.getTimeSpanString(getElapsedTime()); - } - - /** - * Computes a human-readable text that shows the expected remaining time - * until the build completes. - */ + @Override public String getEstimatedRemainingTime() { long d = executableEstimatedDuration; if (d < 0) { @@ -911,9 +876,7 @@ public HttpResponse doYank() { return HttpResponses.redirectViaContextPath("/"); } - /** - * Checks if the current user has a permission to stop this build. - */ + @Override public boolean hasStopPermission() { lock.readLock().lock(); try { diff --git a/core/src/main/java/hudson/model/ItemGroupMixIn.java b/core/src/main/java/hudson/model/ItemGroupMixIn.java index 404d213f73449..89945267edc54 100644 --- a/core/src/main/java/hudson/model/ItemGroupMixIn.java +++ b/core/src/main/java/hudson/model/ItemGroupMixIn.java @@ -31,7 +31,6 @@ import hudson.security.AccessControlled; import hudson.util.CopyOnWriteMap; import hudson.util.Function1; -import hudson.util.Secret; import io.jenkins.servlet.ServletExceptionWrapper; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletResponse; @@ -44,11 +43,11 @@ import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.regex.Matcher; import javax.xml.transform.TransformerException; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import jenkins.model.Jenkins; +import jenkins.security.ExtendedReadRedaction; import jenkins.security.NotReallyRoleSensitiveCallable; import jenkins.util.xml.XMLUtils; import org.kohsuke.stapler.StaplerRequest; @@ -239,18 +238,17 @@ public synchronized T copy(T src, String name) throws I src.checkPermission(Item.EXTENDED_READ); XmlFile srcConfigFile = Items.getConfigFile(src); if (!src.hasPermission(Item.CONFIGURE)) { - Matcher matcher = AbstractItem.SECRET_PATTERN.matcher(srcConfigFile.asString()); - while (matcher.find()) { - if (Secret.decrypt(matcher.group(1)) != null) { - // AccessDeniedException2 does not permit a custom message, and anyway redirecting the user to the login screen is obviously pointless. - throw new AccessDeniedException( - Messages.ItemGroupMixIn_may_not_copy_as_it_contains_secrets_and_( - src.getFullName(), - Jenkins.getAuthentication2().getName(), - Item.PERMISSIONS.title, - Item.EXTENDED_READ.name, - Item.CONFIGURE.name)); - } + final String originalConfigDotXml = srcConfigFile.asString(); + final String redactedConfigDotXml = ExtendedReadRedaction.applyAll(originalConfigDotXml); + if (!originalConfigDotXml.equals(redactedConfigDotXml)) { + // AccessDeniedException2 does not permit a custom message, and anyway redirecting the user to the login screen is obviously pointless. + throw new AccessDeniedException( + Messages.ItemGroupMixIn_may_not_copy_as_it_contains_secrets_and_( + src.getFullName(), + Jenkins.getAuthentication2().getName(), + Item.PERMISSIONS.title, + Item.EXTENDED_READ.name, + Item.CONFIGURE.name)); } } src.getDescriptor().checkApplicableIn(parent); @@ -302,8 +300,17 @@ public synchronized TopLevelItem createProjectFromXML(String name, InputStream x } }); - success = acl.getACL().hasCreatePermission2(Jenkins.getAuthentication2(), parent, result.getDescriptor()) - && result.getDescriptor().isApplicableIn(parent); + boolean hasCreatePermission = acl.getACL().hasCreatePermission2(Jenkins.getAuthentication2(), parent, result.getDescriptor()); + boolean applicableIn = result.getDescriptor().isApplicableIn(parent); + + success = hasCreatePermission && applicableIn; + + if (!hasCreatePermission) { + throw new AccessDeniedException(Jenkins.getAuthentication2().getName() + " does not have required permissions to create " + result.getDescriptor().clazz.getName()); + } + if (!applicableIn) { + throw new AccessDeniedException(result.getDescriptor().clazz.getName() + " is not applicable in " + parent.getFullName()); + } add(result); diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index 08eba90b906ca..797cba56f3942 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -1956,24 +1956,6 @@ default void checkAbortPermission() { } } - /** - * Works just like {@link #checkAbortPermission()} except it indicates the status by a return value, - * instead of exception. - * Also used by default for {@link hudson.model.Queue.Item#hasCancelPermission}. - *

- * NOTE: If you have implemented {@link AccessControlled} this returns by default - * {@code return hasPermission(hudson.model.Item.CANCEL);} - * - * @return false - * if the user doesn't have the permission. - */ - default boolean hasAbortPermission() { - if (this instanceof AccessControlled) { - return ((AccessControlled) this).hasPermission(CANCEL); - } - return true; - } - /** * Returns the URL of this task relative to the context root of the application. * @@ -1984,6 +1966,7 @@ default boolean hasAbortPermission() { * @return * URL that ends with '/'. */ + @Override String getUrl(); /** diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index 37b09fd47d25f..ec7eaabf32c60 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -1551,6 +1551,9 @@ public synchronized void deleteArtifacts() throws IOException { * if we fail to delete. */ public void delete() throws IOException { + if (isLogUpdated()) { + throw new IOException("Unable to delete " + this + " because it is still running"); + } synchronized (this) { // Avoid concurrent delete. See https://issues.jenkins.io/browse/JENKINS-61687 if (isPendingDelete) { @@ -1570,6 +1573,7 @@ public void delete() throws IOException { )); //Still firing the delete listeners; just no need to clean up rootDir RunListener.fireDeleted(this); + SaveableListener.fireOnDeleted(this, getDataFile()); synchronized (this) { // avoid holding a lock while calling plugin impls of onDeleted removeRunFromParent(); } @@ -1578,6 +1582,7 @@ public void delete() throws IOException { //The root dir exists and is a directory that needs to be purged RunListener.fireDeleted(this); + SaveableListener.fireOnDeleted(this, getDataFile()); if (artifactManager != null) { deleteArtifacts(); @@ -1883,12 +1888,6 @@ protected final void execute(@NonNull RunExecution job) { LOGGER.log(Level.SEVERE, "Failed to save build record", e); } } - - try { - getParent().logRotate(); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Failed to rotate log", e); - } } finally { onEndBuilding(); if (logger != null) { diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index 8a38ef2d470b7..0e3cc894148af 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -538,14 +538,17 @@ public String getDownloadUrl() { /** * Is this the legacy default update center site? - * @deprecated - * Will be removed, currently returns always false. - * @since 2.343 + * @since 1.357 */ - @Deprecated @Restricted(NoExternalUse.class) public boolean isLegacyDefault() { - return false; + return isJenkinsCI(); + } + + private boolean isJenkinsCI() { + return url != null + && UpdateCenter.PREDEFINED_UPDATE_SITE_ID.equals(id) + && url.startsWith("http://updates.jenkins-ci.org/"); } /** diff --git a/core/src/main/java/hudson/model/View.java b/core/src/main/java/hudson/model/View.java index 5d53a49ef0b7d..b17077cde44d5 100644 --- a/core/src/main/java/hudson/model/View.java +++ b/core/src/main/java/hudson/model/View.java @@ -949,9 +949,28 @@ public void doRssLatest(StaplerRequest2 req, StaplerResponse2 rsp) throws IOExce /** * Accepts {@code config.xml} submission, as well as serve it. + * + * @since 2.475 */ @WebMethod(name = "config.xml") public HttpResponse doConfigDotXml(StaplerRequest2 req) throws IOException { + if (Util.isOverridden(View.class, getClass(), "doConfigDotXml", StaplerRequest.class)) { + return doConfigDotXml(StaplerRequest.fromStaplerRequest2(req)); + } else { + return doConfigDotXmlImpl(req); + } + } + + /** + * @deprecated use {@link #doConfigDotXml(StaplerRequest2)} + */ + @Deprecated + @StaplerNotDispatchable + public HttpResponse doConfigDotXml(StaplerRequest req) throws IOException { + return doConfigDotXmlImpl(StaplerRequest.toStaplerRequest2(req)); + } + + private HttpResponse doConfigDotXmlImpl(StaplerRequest2 req) throws IOException { if (req.getMethod().equals("GET")) { // read checkPermission(READ); diff --git a/core/src/main/java/hudson/model/listeners/SaveableListener.java b/core/src/main/java/hudson/model/listeners/SaveableListener.java index 46bbc6ab60be5..14877d080bf53 100644 --- a/core/src/main/java/hudson/model/listeners/SaveableListener.java +++ b/core/src/main/java/hudson/model/listeners/SaveableListener.java @@ -30,8 +30,7 @@ import hudson.ExtensionPoint; import hudson.XmlFile; import hudson.model.Saveable; -import java.util.logging.Level; -import java.util.logging.Logger; +import jenkins.util.Listeners; /** * Receives notifications about save actions on {@link Saveable} objects in Hudson. @@ -54,6 +53,17 @@ public abstract class SaveableListener implements ExtensionPoint { */ public void onChange(Saveable o, XmlFile file) {} + /** + * Called when a {@link Saveable} object gets deleted. + * + * @param o + * The saveable object. + * @param file + * The {@link XmlFile} for this saveable object. + * @since 2.480 + */ + public void onDeleted(Saveable o, XmlFile file) {} + /** * Registers this object as an active listener so that it can start getting * callbacks invoked. @@ -77,13 +87,15 @@ public void unregister() { * Fires the {@link #onChange} event. */ public static void fireOnChange(Saveable o, XmlFile file) { - for (SaveableListener l : all()) { - try { - l.onChange(o, file); - } catch (Throwable t) { - Logger.getLogger(SaveableListener.class.getName()).log(Level.WARNING, null, t); - } - } + Listeners.notify(SaveableListener.class, false, l -> l.onChange(o, file)); + } + + /** + * Fires the {@link #onDeleted} event. + * @since 2.480 + */ + public static void fireOnDeleted(Saveable o, XmlFile file) { + Listeners.notify(SaveableListener.class, false, l -> l.onDeleted(o, file)); } /** diff --git a/core/src/main/java/hudson/model/queue/SubTask.java b/core/src/main/java/hudson/model/queue/SubTask.java index 0690d074617c4..9a971d9ca40b7 100644 --- a/core/src/main/java/hudson/model/queue/SubTask.java +++ b/core/src/main/java/hudson/model/queue/SubTask.java @@ -33,20 +33,16 @@ import hudson.model.Queue; import hudson.model.ResourceActivity; import java.io.IOException; +import jenkins.model.queue.ITask; /** * A component of {@link Queue.Task} that represents a computation carried out by a single {@link Executor}. * * A {@link Queue.Task} consists of a number of {@link SubTask}. * - *

- * Plugins are encouraged to extend from {@link AbstractSubTask} - * instead of implementing this interface directly, to maintain - * compatibility with future changes to this interface. - * * @since 1.377 */ -public interface SubTask extends ResourceActivity { +public interface SubTask extends ResourceActivity, ITask { /** * If this task needs to be run on a node with a particular label, * return that {@link Label}. Otherwise null, indicating @@ -115,4 +111,13 @@ default long getEstimatedDuration() { default Object getSameNodeConstraint() { return null; } + + /** + * A subtask may not be reachable by its own URL. In that case, this method should return null. + * @return the URL where to reach specifically this subtask, relative to Jenkins URL. If non-null, must end with '/'. + */ + @Override + default String getUrl() { + return null; + } } diff --git a/core/src/main/java/hudson/security/AuthorizationStrategy.java b/core/src/main/java/hudson/security/AuthorizationStrategy.java index db3001fc40f2c..5ca218f6a8fdb 100644 --- a/core/src/main/java/hudson/security/AuthorizationStrategy.java +++ b/core/src/main/java/hudson/security/AuthorizationStrategy.java @@ -43,6 +43,7 @@ import java.io.Serializable; import java.util.Collection; import java.util.Collections; +import jenkins.model.IComputer; import jenkins.model.Jenkins; import jenkins.security.stapler.StaplerAccessibleType; import net.sf.json.JSONObject; @@ -154,6 +155,22 @@ public abstract class AuthorizationStrategy extends AbstractDescribableImpl + * Default implementation delegates to {@link #getACL(Computer)} if the computer is an instance of {@link Computer}, + * otherwise it will fall back to {@link #getRootACL()}. + * + * @since 2.480 + **/ + public @NonNull ACL getACL(@NonNull IComputer computer) { + if (computer instanceof Computer c) { + return getACL(c); + } + return getRootACL(); + } + /** * Implementation can choose to provide different ACL for different {@link Cloud}s. * This can be used as a basis for more fine-grained access control. diff --git a/core/src/main/java/hudson/tasks/LogRotator.java b/core/src/main/java/hudson/tasks/LogRotator.java index ca95081b0385f..0b47f035d8096 100644 --- a/core/src/main/java/hudson/tasks/LogRotator.java +++ b/core/src/main/java/hudson/tasks/LogRotator.java @@ -250,7 +250,7 @@ private boolean shouldKeepRun(Run r, Run lsb, Run lstb) { LOGGER.log(FINER, "{0} is not to be removed or purged of artifacts because it’s the last stable build", r); return true; } - if (r.isBuilding()) { + if (r.isLogUpdated()) { LOGGER.log(FINER, "{0} is not to be removed or purged of artifacts because it’s still building", r); return true; } diff --git a/core/src/main/java/hudson/util/RobustCollectionConverter.java b/core/src/main/java/hudson/util/RobustCollectionConverter.java index 64dbbc7d9e9ad..f914d909be27a 100644 --- a/core/src/main/java/hudson/util/RobustCollectionConverter.java +++ b/core/src/main/java/hudson/util/RobustCollectionConverter.java @@ -26,6 +26,7 @@ import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.XStreamException; +import com.thoughtworks.xstream.converters.ConversionException; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.converters.collections.CollectionConverter; import com.thoughtworks.xstream.converters.reflection.ReflectionProvider; @@ -34,11 +35,15 @@ import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.mapper.Mapper; import com.thoughtworks.xstream.security.InputManipulationException; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.diagnosis.OldDataMonitor; +import java.lang.reflect.Type; import java.util.Collection; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; import java.util.logging.Logger; import jenkins.util.xstream.CriticalXStreamException; +import org.jvnet.tiger_types.Types; /** * {@link CollectionConverter} that ignores {@link XStreamException}. @@ -52,14 +57,39 @@ @SuppressWarnings({"rawtypes", "unchecked"}) public class RobustCollectionConverter extends CollectionConverter { private final SerializableConverter sc; + /** + * When available, this field holds the declared type of the collection being deserialized. + */ + private final @CheckForNull Class elementType; public RobustCollectionConverter(XStream xs) { - this(xs.getMapper(), xs.getReflectionProvider()); + this(xs.getMapper(), xs.getReflectionProvider(), null); } public RobustCollectionConverter(Mapper mapper, ReflectionProvider reflectionProvider) { + this(mapper, reflectionProvider, null); + } + + /** + * Creates a converter that will validate the types of collection elements during deserialization. + *

Elements with invalid types will be omitted from deserialized collections and may result in an + * {@link OldDataMonitor} warning. + *

This type checking currently uses the erasure of the type argument, so for example, the element type for a + * {@code List>} is just a raw {@code Optional}, so non-integer values inside of the optional + * would still deserialize successfully and the resulting optional would be included in the list. + * + * @see RobustReflectionConverter#unmarshalField + */ + public RobustCollectionConverter(Mapper mapper, ReflectionProvider reflectionProvider, Type collectionType) { super(mapper); sc = new SerializableConverter(mapper, reflectionProvider, new ClassLoaderReference(null)); + if (collectionType != null && Collection.class.isAssignableFrom(Types.erasure(collectionType))) { + var baseType = Types.getBaseClass(collectionType, Collection.class); + var typeArg = Types.getTypeArgument(baseType, 0, Object.class); + this.elementType = Types.erasure(typeArg); + } else { + this.elementType = null; + } } @Override @@ -85,9 +115,19 @@ protected void populateCollection(HierarchicalStreamReader reader, Unmarshalling reader.moveDown(); try { Object item = readBareItem(reader, context, collection); - long nanoNow = System.nanoTime(); - collection.add(item); - XStream2SecurityUtils.checkForCollectionDoSAttack(context, nanoNow); + if (elementType != null && item != null && !elementType.isInstance(item)) { + var exception = new ConversionException("Invalid type for collection element"); + // c.f. TreeUnmarshaller.addInformationTo + exception.add("required-type", elementType.getName()); + exception.add("class", item.getClass().getName()); + exception.add("converter-type", getClass().getName()); + reader.appendErrors(exception); + RobustReflectionConverter.addErrorInContext(context, exception); + } else { + long nanoNow = System.nanoTime(); + collection.add(item); + XStream2SecurityUtils.checkForCollectionDoSAttack(context, nanoNow); + } } catch (CriticalXStreamException e) { throw e; } catch (InputManipulationException e) { diff --git a/core/src/main/java/hudson/util/RobustMapConverter.java b/core/src/main/java/hudson/util/RobustMapConverter.java index f845e38771ccd..c802959d0d096 100644 --- a/core/src/main/java/hudson/util/RobustMapConverter.java +++ b/core/src/main/java/hudson/util/RobustMapConverter.java @@ -31,9 +31,13 @@ import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.mapper.Mapper; import com.thoughtworks.xstream.security.InputManipulationException; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.diagnosis.OldDataMonitor; +import java.lang.reflect.Type; import java.util.Map; import java.util.logging.Logger; import jenkins.util.xstream.CriticalXStreamException; +import org.jvnet.tiger_types.Types; /** * Loads a {@link Map} while tolerating read errors on its keys and values. @@ -42,13 +46,47 @@ final class RobustMapConverter extends MapConverter { private static final Object ERROR = new Object(); + /** + * When available, this field holds the declared type of the keys of the map being deserialized. + */ + private final @CheckForNull Class keyType; + + /** + * When available, this field holds the declared type of the values of the map being deserialized. + */ + private final @CheckForNull Class valueType; + RobustMapConverter(Mapper mapper) { + this(mapper, null); + } + + /** + * Creates a converter that will validate the types of map entry keys and values during deserialization. + *

Map entries whose key or value has an invalid type will be omitted from deserialized maps and may result in + * an {@link OldDataMonitor} warning. + *

This type checking currently uses the erasure of the type argument, so for example, the value type for a + * {@code Map>} is just a raw {@code Optional}, so non-integer values inside of the + * optional would still deserialize successfully and the resulting map entry would be included in the map. + * + * @see RobustReflectionConverter#unmarshalField + */ + RobustMapConverter(Mapper mapper, Type mapType) { super(mapper); + if (mapType != null && Map.class.isAssignableFrom(Types.erasure(mapType))) { + var baseType = Types.getBaseClass(mapType, Map.class); + var keyTypeArg = Types.getTypeArgument(baseType, 0, Object.class); + this.keyType = Types.erasure(keyTypeArg); + var valueTypeArg = Types.getTypeArgument(baseType, 1, Object.class); + this.valueType = Types.erasure(valueTypeArg); + } else { + this.keyType = null; + this.valueType = null; + } } @Override protected void putCurrentEntryIntoMap(HierarchicalStreamReader reader, UnmarshallingContext context, Map map, Map target) { - Object key = read(reader, context, map); - Object value = read(reader, context, map); + Object key = read(reader, context, map, keyType); + Object value = read(reader, context, map, valueType); if (key != ERROR && value != ERROR) { try { long nanoNow = System.nanoTime(); @@ -64,7 +102,7 @@ final class RobustMapConverter extends MapConverter { } } - private Object read(HierarchicalStreamReader reader, UnmarshallingContext context, Map map) { + private Object read(HierarchicalStreamReader reader, UnmarshallingContext context, Map map, @CheckForNull Class expectedType) { if (!reader.hasMoreChildren()) { var exception = new ConversionException("Invalid map entry"); reader.appendErrors(exception); @@ -73,7 +111,18 @@ private Object read(HierarchicalStreamReader reader, UnmarshallingContext contex } reader.moveDown(); try { - return readBareItem(reader, context, map); + var object = readBareItem(reader, context, map); + if (expectedType != null && object != null && !expectedType.isInstance(object)) { + var exception = new ConversionException("Invalid type for map entry key/value"); + // c.f. TreeUnmarshaller.addInformationTo + exception.add("required-type", expectedType.getName()); + exception.add("class", object.getClass().getName()); + exception.add("converter-type", getClass().getName()); + reader.appendErrors(exception); + RobustReflectionConverter.addErrorInContext(context, exception); + return ERROR; + } + return object; } catch (CriticalXStreamException x) { throw x; } catch (XStreamException | LinkageError x) { diff --git a/core/src/main/java/hudson/util/RobustReflectionConverter.java b/core/src/main/java/hudson/util/RobustReflectionConverter.java index d1bc500003e12..686aad13c342a 100644 --- a/core/src/main/java/hudson/util/RobustReflectionConverter.java +++ b/core/src/main/java/hudson/util/RobustReflectionConverter.java @@ -48,6 +48,7 @@ import hudson.model.Saveable; import hudson.security.ACL; import java.lang.reflect.Field; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -65,6 +66,7 @@ import jenkins.util.xstream.CriticalXStreamException; import net.jcip.annotations.GuardedBy; import org.acegisecurity.Authentication; +import org.jvnet.tiger_types.Types; /** * Custom {@link ReflectionConverter} that handle errors more gracefully. @@ -80,7 +82,7 @@ @SuppressWarnings({"rawtypes", "unchecked"}) public class RobustReflectionConverter implements Converter { - private static /* non-final for Groovy */ boolean RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS = SystemProperties.getBoolean(RobustReflectionConverter.class.getName() + ".recordFailuresForAllAuthentications", false); + static /* non-final for Groovy */ boolean RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS = SystemProperties.getBoolean(RobustReflectionConverter.class.getName() + ".recordFailuresForAllAuthentications", false); private static /* non-final for Groovy */ boolean RECORD_FAILURES_FOR_ADMINS = SystemProperties.getBoolean(RobustReflectionConverter.class.getName() + ".recordFailuresForAdmins", false); protected final ReflectionProvider reflectionProvider; @@ -324,7 +326,8 @@ public Object doUnmarshal(final Object result, final HierarchicalStreamReader re } } - Map implicitCollectionsForCurrentObject = null; + Map> implicitCollectionsForCurrentObject = new HashMap<>(); + Map> implicitCollectionElementTypesForCurrentObject = new HashMap<>(); while (reader.hasMoreChildren()) { reader.moveDown(); @@ -365,7 +368,7 @@ public Object doUnmarshal(final Object result, final HierarchicalStreamReader re reflectionProvider.writeField(result, fieldName, value, classDefiningField); seenFields.add(classDefiningField, fieldName); } else { - implicitCollectionsForCurrentObject = writeValueToImplicitCollection(context, value, implicitCollectionsForCurrentObject, result, fieldName); + writeValueToImplicitCollection(reader, context, value, implicitCollectionsForCurrentObject, implicitCollectionElementTypesForCurrentObject, result, fieldName); } } } catch (CriticalXStreamException e) { @@ -451,18 +454,23 @@ private boolean fieldDefinedInClass(Object result, String attrName) { protected Object unmarshalField(final UnmarshallingContext context, final Object result, Class type, Field field) { Converter converter = mapper.getLocalConverter(field.getDeclaringClass(), field.getName()); + if (converter == null) { + if (new RobustCollectionConverter(mapper, reflectionProvider).canConvert(type)) { + converter = new RobustCollectionConverter(mapper, reflectionProvider, field.getGenericType()); + } else if (new RobustMapConverter(mapper).canConvert(type)) { + converter = new RobustMapConverter(mapper, field.getGenericType()); + } + } return context.convertAnother(result, type, converter); } - private Map writeValueToImplicitCollection(UnmarshallingContext context, Object value, Map implicitCollections, Object result, String itemFieldName) { + private void writeValueToImplicitCollection(HierarchicalStreamReader reader, UnmarshallingContext context, Object value, Map> implicitCollections, Map> implicitCollectionElementTypes, Object result, String itemFieldName) { String fieldName = mapper.getFieldNameForItemTypeAndName(context.getRequiredType(), value.getClass(), itemFieldName); if (fieldName != null) { - if (implicitCollections == null) { - implicitCollections = new HashMap(); // lazy instantiation - } - Collection collection = (Collection) implicitCollections.get(fieldName); + Collection collection = implicitCollections.get(fieldName); if (collection == null) { - Class fieldType = mapper.defaultImplementationOf(reflectionProvider.getFieldType(result, fieldName, null)); + Field field = reflectionProvider.getField(result.getClass(), fieldName); + Class fieldType = mapper.defaultImplementationOf(field.getType()); if (!Collection.class.isAssignableFrom(fieldType)) { throw new ObjectAccessException("Field " + fieldName + " of " + result.getClass().getName() + " is configured for an implicit Collection, but field is of type " + fieldType.getName()); @@ -473,10 +481,25 @@ private Map writeValueToImplicitCollection(UnmarshallingContext context, Object collection = (Collection) pureJavaReflectionProvider.newInstance(fieldType); reflectionProvider.writeField(result, fieldName, collection, null); implicitCollections.put(fieldName, collection); + Type fieldGenericType = field.getGenericType(); + Type elementGenericType = Types.getTypeArgument(Types.getBaseClass(fieldGenericType, Collection.class), 0, Object.class); + Class elementType = Types.erasure(elementGenericType); + implicitCollectionElementTypes.put(fieldName, elementType); + } + Class elementType = implicitCollectionElementTypes.getOrDefault(fieldName, Object.class); + if (!elementType.isInstance(value)) { + var exception = new ConversionException("Invalid element type for implicit collection for field: " + fieldName); + // c.f. TreeUnmarshaller.addInformationTo + exception.add("required-type", elementType.getName()); + exception.add("class", value.getClass().getName()); + exception.add("converter-type", getClass().getName()); + reader.appendErrors(exception); + throw exception; } collection.add(value); + } else { + // TODO: Should we warn in this case? The value will be ignored. } - return implicitCollections; } private Class determineWhichClassDefinesField(HierarchicalStreamReader reader) { diff --git a/core/src/main/java/jenkins/cli/SafeRestartCommand.java b/core/src/main/java/jenkins/cli/SafeRestartCommand.java index 54c624bbba274..4c1a8009e44dc 100644 --- a/core/src/main/java/jenkins/cli/SafeRestartCommand.java +++ b/core/src/main/java/jenkins/cli/SafeRestartCommand.java @@ -32,6 +32,7 @@ import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.args4j.Option; +import org.kohsuke.stapler.StaplerRequest2; /** * Safe Restart Jenkins - do not accept any new jobs and try to pause existing. @@ -53,7 +54,7 @@ public String getShortDescription() { @Override protected int run() throws Exception { - Jenkins.get().doSafeRestart(null, message); + Jenkins.get().doSafeRestart((StaplerRequest2) null, message); return 0; } } diff --git a/core/src/main/java/jenkins/model/BackgroundGlobalBuildDiscarder.java b/core/src/main/java/jenkins/model/BackgroundGlobalBuildDiscarder.java index 1a42c0577a479..ad33643879f57 100644 --- a/core/src/main/java/jenkins/model/BackgroundGlobalBuildDiscarder.java +++ b/core/src/main/java/jenkins/model/BackgroundGlobalBuildDiscarder.java @@ -31,6 +31,7 @@ import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Stream; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -56,8 +57,18 @@ protected void execute(TaskListener listener) throws IOException, InterruptedExc } } + /** + * Runs all globally configured build discarders against a job. + */ public static void processJob(TaskListener listener, Job job) { - GlobalBuildDiscarderConfiguration.get().getConfiguredBuildDiscarders().forEach(strategy -> { + processJob(listener, job, GlobalBuildDiscarderConfiguration.get().getConfiguredBuildDiscarders().stream()); + } + + /** + * Runs the specified build discarders against a job. + */ + public static void processJob(TaskListener listener, Job job, Stream strategies) { + strategies.forEach(strategy -> { String displayName = strategy.getDescriptor().getDisplayName(); if (strategy.isApplicable(job)) { try { diff --git a/core/src/main/java/jenkins/model/DisplayExecutor.java b/core/src/main/java/jenkins/model/DisplayExecutor.java new file mode 100644 index 0000000000000..f7559ff85799c --- /dev/null +++ b/core/src/main/java/jenkins/model/DisplayExecutor.java @@ -0,0 +1,97 @@ +/* + * The MIT License + * + * Copyright 2024 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.model; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.Executor; +import hudson.model.ModelObject; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * A value class providing a consistent snapshot view of the state of an executor to avoid race conditions + * during rendering of the executors list. + */ +@Restricted(NoExternalUse.class) +public class DisplayExecutor implements ModelObject, IDisplayExecutor { + + @NonNull + private final String displayName; + @NonNull + private final String url; + @NonNull + private final Executor executor; + + public DisplayExecutor(@NonNull String displayName, @NonNull String url, @NonNull Executor executor) { + this.displayName = displayName; + this.url = url; + this.executor = executor; + } + + @Override + @NonNull + public String getDisplayName() { + return displayName; + } + + @Override + @NonNull + public String getUrl() { + return url; + } + + @Override + @NonNull + public Executor getExecutor() { + return executor; + } + + @Override + public String toString() { + return "DisplayExecutor{" + "displayName='" + displayName + '\'' + + ", url='" + url + '\'' + + ", executor=" + executor + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DisplayExecutor that = (DisplayExecutor) o; + + return executor.equals(that.executor); + } + + @Override + public int hashCode() { + return executor.hashCode(); + } +} diff --git a/core/src/main/java/jenkins/model/GlobalBuildDiscarderListener.java b/core/src/main/java/jenkins/model/GlobalBuildDiscarderListener.java index 7ddea84c4241c..6d2c58b447746 100644 --- a/core/src/main/java/jenkins/model/GlobalBuildDiscarderListener.java +++ b/core/src/main/java/jenkins/model/GlobalBuildDiscarderListener.java @@ -35,7 +35,7 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; /** - * Run background build discarders on an individual job once a build is finalized + * Run build discarders on an individual job once a build is finalized */ @Extension @Restricted(NoExternalUse.class) @@ -46,6 +46,15 @@ public class GlobalBuildDiscarderListener extends RunListener { @Override public void onFinalized(Run run) { Job job = run.getParent(); - BackgroundGlobalBuildDiscarder.processJob(new LogTaskListener(LOGGER, Level.FINE), job); + try { + // Job-level build discarder execution is unconditional. + job.logRotate(); + } catch (Exception e) { + LOGGER.log(Level.WARNING, e, () -> "Failed to rotate log for " + run); + } + // Avoid calling Job.logRotate twice in case JobGlobalBuildDiscarderStrategy is configured globally. + BackgroundGlobalBuildDiscarder.processJob(new LogTaskListener(LOGGER, Level.FINE), job, + GlobalBuildDiscarderConfiguration.get().getConfiguredBuildDiscarders().stream() + .filter(s -> !(s instanceof JobGlobalBuildDiscarderStrategy))); } } diff --git a/core/src/main/java/jenkins/model/IComputer.java b/core/src/main/java/jenkins/model/IComputer.java new file mode 100644 index 0000000000000..0708748f43709 --- /dev/null +++ b/core/src/main/java/jenkins/model/IComputer.java @@ -0,0 +1,182 @@ +/* + * The MIT License + * + * Copyright 2024 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.model; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Util; +import hudson.model.Computer; +import hudson.model.Node; +import hudson.security.ACL; +import hudson.security.AccessControlled; +import java.util.List; +import java.util.concurrent.Future; +import org.jenkins.ui.icon.Icon; +import org.jenkins.ui.icon.IconSet; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +/** + * Interface for computer-like objects meant to be passed to {@code t:executors} tag. + * + * @since 2.480 + */ +@Restricted(Beta.class) +public interface IComputer extends AccessControlled { + /** + * Returns {@link Node#getNodeName() the name of the node}. + */ + @NonNull + String getName(); + + /** + * Used to render the list of executors. + * @return a snapshot of the executor display information + */ + @NonNull + List getDisplayExecutors(); + + /** + * @return {@code true} if the node is offline. {@code false} if it is online. + */ + boolean isOffline(); + + /** + * @return the node name for UI purposes. + */ + @NonNull + String getDisplayName(); + + /** + * Returns {@code true} if the computer is accepting tasks. Needed to allow agents programmatic suspension of task + * scheduling that does not overlap with being offline. + * + * @return {@code true} if the computer is accepting tasks + * @see hudson.slaves.RetentionStrategy#isAcceptingTasks(Computer) + * @see hudson.model.Node#isAcceptingTasks() + */ + boolean isAcceptingTasks(); + + /** + * @return the URL where to reach specifically this computer, relative to Jenkins URL. + */ + @NonNull + String getUrl(); + + /** + * @return {@code true} if this computer has a defined offline cause, @{code false} otherwise. + */ + default boolean hasOfflineCause() { + return Util.fixEmpty(getOfflineCauseReason()) != null; + } + + /** + * If the computer was offline (either temporarily or not), + * this method will return the cause as a string (without user info). + *

+ * {@code hasOfflineCause() == true} implies this must be nonempty. + * + * @return + * empty string if the system was put offline without given a cause. + */ + @NonNull + String getOfflineCauseReason(); + + /** + * @return true if the node is currently connecting to the Jenkins controller. + */ + boolean isConnecting(); + + /** + * Returns the icon for this computer. + *

+ * It is both the recommended and default implementation to serve different icons based on {@link #isOffline} + * + * @see #getIconClassName() + */ + String getIcon(); + + /** + * Returns the alternative text for the computer icon. + */ + String getIconAltText(); + + /** + * Returns the class name that will be used to look up the icon. + *

+ * This class name will be added as a class tag to the html img tags where the icon should + * show up followed by a size specifier given by {@link Icon#toNormalizedIconSizeClass(String)} + * The conversion of class tag to src tag is registered through {@link IconSet#addIcon(Icon)} + * + * @see #getIcon() + */ + default String getIconClassName() { + return getIcon(); + } + + /** + * Returns the number of {@link IExecutor}s that are doing some work right now. + */ + int countBusy(); + /** + * Returns the current size of the executor pool for this computer. + */ + int countExecutors(); + + /** + * @return true if the computer is online. + */ + boolean isOnline(); + /** + * @return the number of {@link IExecutor}s that are idle right now. + */ + int countIdle(); + + /** + * @return true if this computer can be launched by Jenkins proactively and automatically. + * + *

+ * For example, inbound agents return {@code false} from this, because the launch process + * needs to be initiated from the agent side. + */ + boolean isLaunchSupported(); + + /** + * Attempts to connect this computer. + * + * @param forceReconnect If true and a connect activity is already in progress, it will be cancelled and + * the new one will be started. If false, and a connect activity is already in progress, this method + * will do nothing and just return the pending connection operation. + * @return A {@link Future} representing pending completion of the task. The 'completion' includes + * both a successful completion and a non-successful completion (such distinction typically doesn't + * make much sense because as soon as {@link IComputer} is connected it can be disconnected by some other threads.) + */ + Future connect(boolean forceReconnect); + + @NonNull + @Override + default ACL getACL() { + return Jenkins.get().getAuthorizationStrategy().getACL(this); + } +} diff --git a/core/src/main/java/jenkins/model/IDisplayExecutor.java b/core/src/main/java/jenkins/model/IDisplayExecutor.java new file mode 100644 index 0000000000000..5f959a158faa0 --- /dev/null +++ b/core/src/main/java/jenkins/model/IDisplayExecutor.java @@ -0,0 +1,55 @@ +/* + * The MIT License + * + * Copyright 2024 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.model; + +import edu.umd.cs.findbugs.annotations.NonNull; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +/** + * A snapshot of the executor information for display purpose. + * + * @since 2.480 + */ +@Restricted(Beta.class) +public interface IDisplayExecutor { + /** + * @return The UI label for this executor. + */ + @NonNull + String getDisplayName(); + + /** + * @return the URL where to reach specifically this executor, relative to Jenkins URL. + */ + @NonNull + String getUrl(); + + /** + * @return the executor this display information is for. + */ + @NonNull + IExecutor getExecutor(); +} diff --git a/core/src/main/java/jenkins/model/IExecutor.java b/core/src/main/java/jenkins/model/IExecutor.java new file mode 100644 index 0000000000000..1aec72db5be9a --- /dev/null +++ b/core/src/main/java/jenkins/model/IExecutor.java @@ -0,0 +1,144 @@ +/* + * The MIT License + * + * Copyright 2024 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.model; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.Util; +import hudson.model.Queue; +import hudson.model.queue.WorkUnit; +import jenkins.model.queue.ITask; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +/** + * Interface for an executor that can be displayed in the executors widget. + * + * @since 2.480 + */ +@Restricted(Beta.class) +public interface IExecutor { + /** + * Returns true if this {@link IExecutor} is ready for action. + */ + boolean isIdle(); + + /** + * @return the {@link IComputer} that this executor belongs to. + */ + IComputer getOwner(); + + /** + * @return the current executable, if any. + */ + @CheckForNull Queue.Executable getCurrentExecutable(); + + /** + * Returns the current {@link WorkUnit} (of {@link #getCurrentExecutable() the current executable}) + * that this executor is running. + * + * @return + * null if the executor is idle. + */ + @CheckForNull WorkUnit getCurrentWorkUnit(); + + /** + * @return the current display name of the executor. Usually the name of the executable. + */ + String getDisplayName(); + + /** + * @return a reference to the parent task of the current executable, if any. + */ + @CheckForNull + default ITask getParentTask() { + var currentExecutable = getCurrentExecutable(); + if (currentExecutable == null) { + var workUnit = getCurrentWorkUnit(); + if (workUnit != null) { + return workUnit.work; + } else { + // Idle + return null; + } + } else { + return currentExecutable.getParent(); + } + } + + /** + * Checks if the current user has a permission to stop this build. + */ + boolean hasStopPermission(); + + /** + * Gets the executor number that uniquely identifies it among + * other {@link IExecutor}s for the same computer. + * + * @return + * a sequential number starting from 0. + */ + int getNumber(); + + /** + * Gets the elapsed time since the build has started. + * + * @return + * the number of milliseconds since the build has started. + */ + long getElapsedTime(); + + /** + * Gets the string that says how long since this build has started. + * + * @return + * string like "3 minutes" "1 day" etc. + */ + default String getTimestampString() { + return Util.getTimeSpanString(getElapsedTime()); + } + + /** + * Computes a human-readable text that shows the expected remaining time + * until the build completes. + */ + String getEstimatedRemainingTime(); + + /** + * Returns true if the current build is likely stuck. + * + *

+ * This is a heuristics based approach, but if the build is suspiciously taking for a long time, + * this method returns true. + */ + boolean isLikelyStuck(); + + /** + * Returns the progress of the current build in the number between 0-100. + * + * @return -1 + * if it's impossible to estimate the progress. + */ + int getProgress(); +} diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 705044cd8fa00..90ffceefb4564 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -2071,7 +2071,7 @@ public boolean isUpgradedFromBefore(VersionNumber v) { * Gets the read-only list of all {@link Computer}s. */ public Computer[] getComputers() { - return computers.values().stream().sorted(Comparator.comparing(Computer::getName)).toArray(Computer[]::new); + return getComputersCollection().stream().sorted(Comparator.comparing(Computer::getName)).toArray(Computer[]::new); } @CLIResolver @@ -2080,7 +2080,7 @@ public Computer[] getComputers() { || name.equals("(master)")) // backwards compatibility for URLs name = ""; - for (Computer c : computers.values()) { + for (Computer c : getComputersCollection()) { if (c.getName().equals(name)) return c; } @@ -2247,6 +2247,14 @@ protected ConcurrentMap getComputerMap() { return computers; } + /** + * @return the collection of all {@link Computer}s in this instance. + */ + @Restricted(NoExternalUse.class) + public Collection getComputersCollection() { + return computers.values(); + } + /** * Returns all {@link Node}s in the system, excluding {@link Jenkins} instance itself which * represents the built-in node (in other words, this only returns agents). @@ -2479,7 +2487,7 @@ protected Iterable allAsIterable() { protected Computer get(String key) { return getComputer(key); } @Override - protected Collection all() { return computers.values(); } + protected Collection all() { return getComputersCollection(); } }) .add(new CollectionSearchIndex() { // for users @Override @@ -3814,7 +3822,7 @@ private Set> _cleanUpDisconnectComputers(final List errors) final Set> pending = new HashSet<>(); // JENKINS-28840 we know we will be interrupting all the Computers so get the Queue lock once for all Queue.withLock(() -> { - for (Computer c : computers.values()) { + for (Computer c : getComputersCollection()) { try { c.interrupt(); killComputer(c); @@ -4665,7 +4673,7 @@ public HttpResponse doSafeRestart(StaplerRequest req) throws IOException, Servle /** * Queues up a safe restart of Jenkins. Jobs have to finish or pause before it can proceed. No new jobs are accepted. * - * @since 2.414 + * @since 2.475 */ public HttpResponse doSafeRestart(StaplerRequest2 req, @QueryParameter("message") String message) throws IOException, ServletException, RestartNotSupportedException { checkPermission(MANAGE); @@ -4684,6 +4692,20 @@ public HttpResponse doSafeRestart(StaplerRequest2 req, @QueryParameter("message" return HttpResponses.redirectToDot(); } + /** + * @deprecated use {@link #doSafeRestart(StaplerRequest2, String)} + * @since 2.414 + */ + @Deprecated + @StaplerNotDispatchable + public HttpResponse doSafeRestart(StaplerRequest req, @QueryParameter("message") String message) throws IOException, javax.servlet.ServletException, RestartNotSupportedException { + try { + return doSafeRestart(StaplerRequest.toStaplerRequest2(req), message); + } catch (ServletException e) { + throw ServletExceptionWrapper.fromJakartaServletException(e); + } + } + private static Lifecycle restartableLifecycle() throws RestartNotSupportedException { if (Main.isUnitTest) { throw new RestartNotSupportedException("Restarting the controller JVM is not supported in JenkinsRule-based tests"); @@ -5476,6 +5498,7 @@ protected MasterComputer() { * Returns "" to match with {@link Jenkins#getNodeName()}. */ @Override + @NonNull public String getName() { return ""; } @@ -5497,6 +5520,7 @@ public String getCaption() { } @Override + @NonNull public String getUrl() { return "computer/(built-in)/"; } diff --git a/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java b/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java index f46280fef0c01..11eea481106a3 100644 --- a/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java +++ b/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java @@ -254,8 +254,20 @@ public ContextMenu add(Node n) { * Adds a computer * * @since 1.513 + * @deprecated use {@link #add(IComputer)} instead. */ + @Deprecated(since = "2.480") public ContextMenu add(Computer c) { + return add((IComputer) c); + } + + /** + * Adds a {@link IComputer} instance. + * @param c the computer to add to the menu + * @return this + * @since 2.480 + */ + public ContextMenu add(IComputer c) { return add(new MenuItem() .withDisplayName(c.getDisplayName()) .withIconClass(c.getIconClassName()) diff --git a/core/src/main/java/jenkins/model/Nodes.java b/core/src/main/java/jenkins/model/Nodes.java index 8f4c0e5eafc62..e15d391c4975e 100644 --- a/core/src/main/java/jenkins/model/Nodes.java +++ b/core/src/main/java/jenkins/model/Nodes.java @@ -295,6 +295,7 @@ public void run() { jenkins.trimLabels(node); } NodeListener.fireOnDeleted(node); + SaveableListener.fireOnDeleted(node, getConfigFile(node)); } } diff --git a/core/src/main/java/jenkins/model/queue/ITask.java b/core/src/main/java/jenkins/model/queue/ITask.java new file mode 100644 index 0000000000000..37b0e62150de0 --- /dev/null +++ b/core/src/main/java/jenkins/model/queue/ITask.java @@ -0,0 +1,76 @@ +/* + * The MIT License + * + * Copyright 2024 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.model.queue; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.model.Item; +import hudson.model.ModelObject; +import hudson.security.AccessControlled; + +/** + * A task that can be displayed in the executors widget. + * + * @since 2.480 + */ +public interface ITask extends ModelObject { + /** + * @return {@code true} if the current user can cancel the current task. + * + * NOTE: If you have implemented {@link AccessControlled} this returns by default + * {@code hasPermission(Item.CANCEL)} + */ + default boolean hasAbortPermission() { + if (this instanceof AccessControlled ac) { + return ac.hasPermission(Item.CANCEL); + } + return true; + } + + /** + * @return {@code true} if the current user has read access on the task. + */ + @SuppressWarnings("unused") // jelly + default boolean hasReadPermission() { + if (this instanceof AccessControlled ac) { + return ac.hasPermission(Item.READ); + } + return true; + } + + /** + * @return the full display name of the task. + *

+ * Defaults to the same as {@link #getDisplayName()}. + */ + default String getFullDisplayName() { + return getDisplayName(); + } + + /** + * @return the URL where to reach specifically this task, relative to Jenkins URL. If non-null, must end with '/'. + */ + @CheckForNull + String getUrl(); +} diff --git a/core/src/main/java/jenkins/model/queue/ItemDeletion.java b/core/src/main/java/jenkins/model/queue/ItemDeletion.java index a2d954fbc4593..b278f4d24c930 100644 --- a/core/src/main/java/jenkins/model/queue/ItemDeletion.java +++ b/core/src/main/java/jenkins/model/queue/ItemDeletion.java @@ -266,12 +266,10 @@ public static void cancelBuildsInProgress(@NonNull Item initiatingItem) throws F // comparison with executor.getCurrentExecutable() == executable currently should always be // true as we no longer recycle Executors, but safer to future-proof in case we ever // revisit recycling. - if (!entry.getKey().isAlive() + if (!entry.getKey().isActive() || entry.getValue() != entry.getKey().getCurrentExecutable()) { iterator.remove(); } - // I don't know why, but we have to keep interrupting - entry.getKey().interrupt(Result.ABORTED); } Thread.sleep(50L); } diff --git a/core/src/main/java/jenkins/security/ExtendedReadRedaction.java b/core/src/main/java/jenkins/security/ExtendedReadRedaction.java new file mode 100644 index 0000000000000..7baec1962f484 --- /dev/null +++ b/core/src/main/java/jenkins/security/ExtendedReadRedaction.java @@ -0,0 +1,36 @@ +package jenkins.security; + +import hudson.ExtensionList; +import hudson.ExtensionPoint; + +/** + * Redact {@code config.xml} contents for users with ExtendedRead permission + * while lacking the required Configure permission to see the full unredacted + * configuration. + * + * @see SECURITY-266 + * @see Jenkins Security Advisory 2016-05-11 + * @since 2.479 + */ +public interface ExtendedReadRedaction extends ExtensionPoint { + /** + * Redacts sensitive information from the provided {@code config.xml} file content. + * Input may already have redactions applied; output may be passed through further redactions. + * These methods are expected to retain the basic structure of the XML document contained in input/output strings. + * + * @param configDotXml String representation of (potentially already redacted) config.xml file + * @return Redacted config.xml file content + */ + String apply(String configDotXml); + + static ExtensionList all() { + return ExtensionList.lookup(ExtendedReadRedaction.class); + } + + static String applyAll(String configDotXml) { + for (ExtendedReadRedaction redaction : all()) { + configDotXml = redaction.apply(configDotXml); + } + return configDotXml; + } +} diff --git a/core/src/main/java/jenkins/security/ExtendedReadSecretRedaction.java b/core/src/main/java/jenkins/security/ExtendedReadSecretRedaction.java new file mode 100644 index 0000000000000..91a79f354d716 --- /dev/null +++ b/core/src/main/java/jenkins/security/ExtendedReadSecretRedaction.java @@ -0,0 +1,28 @@ +package jenkins.security; + +import hudson.Extension; +import hudson.util.Secret; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Restricted(NoExternalUse.class) +@Extension +public class ExtendedReadSecretRedaction implements ExtendedReadRedaction { + + private static final Pattern SECRET_PATTERN = Pattern.compile(">(" + Secret.ENCRYPTED_VALUE_PATTERN + ")<"); + + @Override + public String apply(String configDotXml) { + Matcher matcher = SECRET_PATTERN.matcher(configDotXml); + StringBuilder cleanXml = new StringBuilder(); + while (matcher.find()) { + if (Secret.decrypt(matcher.group(1)) != null) { + matcher.appendReplacement(cleanXml, ">********<"); + } + } + matcher.appendTail(cleanXml); + return cleanXml.toString(); + } +} diff --git a/core/src/main/resources/hudson/model/ComputerSet/index.jelly b/core/src/main/resources/hudson/model/ComputerSet/index.jelly index f261e1f3f8cd2..c96346ac49490 100644 --- a/core/src/main/resources/hudson/model/ComputerSet/index.jelly +++ b/core/src/main/resources/hudson/model/ComputerSet/index.jelly @@ -72,8 +72,8 @@ THE SOFTWARE. - - + +

@@ -93,7 +93,7 @@ THE SOFTWARE. - +
diff --git a/core/src/main/resources/hudson/model/Run/delete.jelly b/core/src/main/resources/hudson/model/Run/delete.jelly index 83e3cbd77e712..7443203f2650a 100644 --- a/core/src/main/resources/hudson/model/Run/delete.jelly +++ b/core/src/main/resources/hudson/model/Run/delete.jelly @@ -27,7 +27,7 @@ THE SOFTWARE. --> - + diff --git a/core/src/main/resources/hudson/tasks/Shell/config.groovy b/core/src/main/resources/hudson/tasks/Shell/config.groovy index f895ffe88f3ef..bfd1631969177 100644 --- a/core/src/main/resources/hudson/tasks/Shell/config.groovy +++ b/core/src/main/resources/hudson/tasks/Shell/config.groovy @@ -25,7 +25,7 @@ package hudson.tasks.Shell f=namespace(lib.FormTagLib) f.entry(title:_("Command"),description:_("description",rootURL)) { - f.textarea(name: "command", value: instance?.command, class: "fixed-width", 'codemirror-mode': 'shell', 'codemirror-config': "mode: 'text/x-sh'") + f.textarea(name: "command", value: instance?.command, class: "fixed-width", 'codemirror-mode': 'shell', 'codemirror-config': '"mode": "text/x-sh"') } f.advanced() { diff --git a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config.groovy b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config.groovy index f470918608d3a..78b82d5a50212 100644 --- a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config.groovy +++ b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config.groovy @@ -29,22 +29,21 @@ import hudson.model.AdministrativeMonitor f = namespace(lib.FormTagLib) st = namespace("jelly:stapler") -f.section(title: _("Administrative monitors configuration")) { +f.section(title: _("Administrative monitors"), description: _("blurb")) { f.advanced(title: _("Administrative monitors")) { - f.entry(title: _("Enabled administrative monitors")) { - p(class: "jenkins-form-description", _("blurb")) + f.entry() { for (AdministrativeMonitor am : new ArrayList<>(AdministrativeMonitor.all()) .sort({ o1, o2 -> o1.getDisplayName() <=> o2.getDisplayName() })) { - div(class: "jenkins-checkbox-help-wrapper") { - f.checkbox(name: "administrativeMonitor", - title: am.displayName, - checked: am.enabled, - json: am.id) - if (am.isSecurity()) { - span(style: 'margin-left: 0.5rem', class: 'am-badge', _("Security")) + div(style: "margin-bottom: 0.625rem") { + div(class: "jenkins-checkbox-help-wrapper") { + f.checkbox(name: "administrativeMonitor", + title: am.displayName, + checked: am.enabled, + json: am.id) + if (am.isSecurity()) { + span(style: 'margin-left: 0.5rem', class: 'jenkins-badge', _("Security")) + } } - } - div(class: "tr") { div(class: "jenkins-checkbox__description") { st.include(it: am, page: "description", optional: true) } diff --git a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_it.properties b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_it.properties index 99774b93a33c7..e6a4cda88b8d8 100644 --- a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_it.properties +++ b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_it.properties @@ -22,10 +22,8 @@ # THE SOFTWARE. Administrative\ monitors=Monitor amministrativi -Administrative\ monitors\ configuration=Configurazione monitor amministrativi blurb=I monitor amministrativi sono avvisi visualizzati agli amministratori \ di Jenkins riguardanti lo stato dell''istanza di Jenkins. In generale è \ caldamente consigliato mantenere tutti i monitor amministrativi abilitati, \ ma se non si è interessati a ricevere specifici avvisi, li si deselezioni \ qui per nasconderli permanentemente. -Enabled\ administrative\ monitors=Monitor amministrativi abilitati diff --git a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_ru.properties b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_ru.properties index fabeb6e453efa..fc794e82e30f5 100644 --- a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_ru.properties +++ b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_ru.properties @@ -21,8 +21,6 @@ # THE SOFTWARE. Administrative\ monitors=Мониторы администрирования -Administrative\ monitors\ configuration=Настройка мониторов администрирования blurb=Мониторы администрирования - это предупреждения о состоянии экземпляра Jenkins, которые показываются \ администраторам Jenkins. Обычно настоятельно рекомендуется оставить все мониторы администрирования включёнными, но \ если некоторые предупреждения вас не интересуют, отключите их здесь, чтобы навсегда скрыть их. -Enabled\ administrative\ monitors=Включённые мониторы администрирования diff --git a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_sv_SE.properties b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_sv_SE.properties index 2d46f007949b2..4d20f2a9ff9bc 100644 --- a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_sv_SE.properties +++ b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_sv_SE.properties @@ -4,5 +4,3 @@ blurb=Administrativa övervakningar är varningar som visas för Jenkins-adminis inte är intresserad av specifika varningar kan du avmarkera dem här för att \ dölja dem permanent. Administrative\ monitors=Administrativa övervakningar -Administrative\ monitors\ configuration=Konfiguration av administrativa övervakningar -Enabled\ administrative\ monitors=Aktiverade administrativa övervakningar \ No newline at end of file diff --git a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_tr.properties b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_tr.properties index 188640215ba65..63b7ff3612b01 100644 --- a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_tr.properties +++ b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_tr.properties @@ -1,4 +1,2 @@ -Administrative\ monitors\ configuration=İdari monitör ayarları Administrative\ monitors=İdari monitörler -Enabled\ administrative\ monitors=Aktif idari monitörler blurb=İdari monitörler, Jenkins yöneticilerine Jenkins örneğinin durumu hakkında gösterilen uyarılardır. Genel olarak tüm yönetim monitörlerini etkin tutmanız şiddetle tavsiye edilir, ancak belirli uyarılarla ilgilenmiyorsanız, kalıcı olarak gizlemek için buradaki işaretlerini kaldırın. diff --git a/core/src/main/resources/jenkins/model/Jenkins/_404.jelly b/core/src/main/resources/jenkins/model/Jenkins/_404.jelly index 7230e4f31acc5..2578c10b99bd0 100644 --- a/core/src/main/resources/jenkins/model/Jenkins/_404.jelly +++ b/core/src/main/resources/jenkins/model/Jenkins/_404.jelly @@ -44,6 +44,9 @@ THE SOFTWARE. ${%noAccess} + + ${%tryLoggingIn} + ${%notFound} diff --git a/core/src/main/resources/jenkins/model/Jenkins/_404.properties b/core/src/main/resources/jenkins/model/Jenkins/_404.properties index c671d3ed833c0..42a8c07eb02ff 100644 --- a/core/src/main/resources/jenkins/model/Jenkins/_404.properties +++ b/core/src/main/resources/jenkins/model/Jenkins/_404.properties @@ -1,3 +1,4 @@ title = {0} noAccess = This page may not exist, or you may not have permission to see it. +tryLoggingIn = If you have an account, try logging in. notFound = This page does not exist. diff --git a/core/src/main/resources/lib/form/secretTextarea.jelly b/core/src/main/resources/lib/form/secretTextarea.jelly index 323a8435d0f3d..379023919528f 100644 --- a/core/src/main/resources/lib/form/secretTextarea.jelly +++ b/core/src/main/resources/lib/form/secretTextarea.jelly @@ -101,7 +101,7 @@ Example usage:
diff --git a/core/src/main/resources/lib/form/textarea/textarea.js b/core/src/main/resources/lib/form/textarea/textarea.js index 1677e11fdd168..53ea6293dc62f 100644 --- a/core/src/main/resources/lib/form/textarea/textarea.js +++ b/core/src/main/resources/lib/form/textarea/textarea.js @@ -3,7 +3,28 @@ Behaviour.specify("TEXTAREA.codemirror", "textarea", 0, function (e) { if (!config) { config = ""; } - config = eval("({" + config + "})"); + try { + config = JSON.parse("{" + config + "}"); + } catch (ex) { + /* + * Attempt to parse fairly common legacy format whose exact content is: + * mode:'' + */ + let match = config.match("^mode: ?'([^']+)'$"); + if (match) { + console.log( + "Parsing simple legacy codemirror-config value using fallback: " + + config, + ); + config = { mode: match[1] }; + } else { + console.log( + "Failed to parse codemirror-config '{" + config + "}' as JSON", + ex, + ); + config = {}; + } + } if (!config.onBlur) { config.onBlur = function (editor) { editor.save(); diff --git a/core/src/main/resources/lib/hudson/executors.jelly b/core/src/main/resources/lib/hudson/executors.jelly index 6b1775ce34a3b..af2224818c40f 100644 --- a/core/src/main/resources/lib/hudson/executors.jelly +++ b/core/src/main/resources/lib/hudson/executors.jelly @@ -27,7 +27,7 @@ THE SOFTWARE. Displays the status of executors. - If specified, this is the list of computers whose executors are rendered. If omitted, all the computers + If specified, this is the list of executor holders whose executors are rendered. If omitted, all the computers in the system will be rendered. @@ -39,9 +39,9 @@ THE SOFTWARE. - +
- ( ${%offline}) + ( ${%offline})
@@ -73,16 +73,16 @@ THE SOFTWARE. - + + + +
- - - - + - + @@ -91,30 +91,26 @@ THE SOFTWARE. - + ${%Idle} - - - - - - - - - - ${%Unknown Task} - - + + + + + + + ${%Unknown Task} + +
- + @@ -123,7 +119,7 @@ THE SOFTWARE.
- +
diff --git a/core/src/site/site.xml b/core/src/site/site.xml index 56902cecd2a16..8b17895170df8 100644 --- a/core/src/site/site.xml +++ b/core/src/site/site.xml @@ -6,7 +6,7 @@ + diff --git a/core/src/test/java/hudson/util/RobustCollectionConverterTest.java b/core/src/test/java/hudson/util/RobustCollectionConverterTest.java index 7786fb0833f76..57f2ad42af39c 100644 --- a/core/src/test/java/hudson/util/RobustCollectionConverterTest.java +++ b/core/src/test/java/hudson/util/RobustCollectionConverterTest.java @@ -32,6 +32,8 @@ import static org.junit.Assert.assertTrue; import com.thoughtworks.xstream.security.InputManipulationException; +import hudson.model.Saveable; +import java.io.IOException; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; @@ -42,10 +44,24 @@ import java.util.Map; import java.util.Set; import jenkins.util.xstream.CriticalXStreamException; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.jvnet.hudson.test.Issue; public class RobustCollectionConverterTest { + private final boolean originalRecordFailures = RobustReflectionConverter.RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS; + + @Before + public void before() { + RobustReflectionConverter.RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS = true; + } + + @After + public void after() { + RobustReflectionConverter.RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS = originalRecordFailures; + } + @Test public void workingByDefaultWithSimplePayload() { XStream2 xstream2 = new XStream2(); @@ -173,4 +189,58 @@ private Set preparePayload() { } return set; } + + @Issue("JENKINS-63343") + @Test + public void checkElementTypes() { + var xmlContent = + """ + + + 1 + 2 + oops! + + 3 + + + """; + var actual = (Data) new XStream2().fromXML(xmlContent); + assertEquals(Arrays.asList(1, 2, null, 3), actual.numbers); + } + + @Test + public void rawtypes() { + var xmlContent = + """ + + + 1 + 2 + oops! + 3 + + + """; + var actual = (DataRaw) new XStream2().fromXML(xmlContent); + assertEquals(List.of(1, 2, "oops!", 3), actual.values); + } + + public static class Data implements Saveable { + private List numbers; + + @Override + public void save() throws IOException { + // We only implement Saveable so that RobustReflectionConverter logs deserialization problems. + } + } + + public static class DataRaw implements Saveable { + private List values; + + @Override + public void save() throws IOException { + // We only implement Saveable so that RobustReflectionConverter logs deserialization problems. + } + } } diff --git a/core/src/test/java/hudson/util/RobustMapConverterTest.java b/core/src/test/java/hudson/util/RobustMapConverterTest.java index b74ac9303f71b..c46a7ade2f576 100644 --- a/core/src/test/java/hudson/util/RobustMapConverterTest.java +++ b/core/src/test/java/hudson/util/RobustMapConverterTest.java @@ -32,13 +32,29 @@ import static org.junit.Assert.assertTrue; import com.thoughtworks.xstream.security.InputManipulationException; +import hudson.model.Saveable; +import java.io.IOException; import java.util.HashMap; import java.util.Map; import jenkins.util.xstream.CriticalXStreamException; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.jvnet.hudson.test.Issue; public class RobustMapConverterTest { + private final boolean originalRecordFailures = RobustReflectionConverter.RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS; + + @Before + public void before() { + RobustReflectionConverter.RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS = true; + } + + @After + public void after() { + RobustReflectionConverter.RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS = originalRecordFailures; + } + /** * As RobustMapConverter is the replacer of the default MapConverter * We had to patch it in order to not be impacted by CVE-2021-43859 @@ -146,6 +162,7 @@ private Map preparePayload() { @Test public void robustAgainstInvalidEntry() { + RobustReflectionConverter.RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS = true; XStream2 xstream2 = new XStream2(); String xml = """ @@ -184,7 +201,103 @@ public void robustAgainstInvalidEntryWithNoValue() { assertThat(data.map, equalTo(Map.of("key2", "value2"))); } - private static final class Data { + @Issue("JENKINS-63343") + @Test + public void robustAgainstInvalidKeyType() { + XStream2 xstream2 = new XStream2(); + String xml = + """ + + + + 1 + value1 + + + key2 + value2 + + + + value3 + + + + """; + Data data = (Data) xstream2.fromXML(xml); + var map = new HashMap<>(); + map.put("key2", "value2"); + map.put(null, "value3"); + assertThat(data.map, equalTo(map)); + } + + @Issue("JENKINS-63343") + @Test + public void robustAgainstInvalidValueType() { + XStream2 xstream2 = new XStream2(); + String xml = + """ + + + + key1 + value1 + + + key2 + 2 + + + key3 + + + + + """; + Data data = (Data) xstream2.fromXML(xml); + var map = new HashMap<>(); + map.put("key1", "value1"); + map.put("key3", null); + assertThat(data.map, equalTo(map)); + } + + @Test + public void rawtypes() { + XStream2 xstream2 = new XStream2(); + String xml = + """ + + + + key1 + value1 + + + key2 + 2 + + + + """; + var data = (DataRaw) xstream2.fromXML(xml); + assertThat(data.map, equalTo(Map.of("key1", "value1", "key2", 2))); + } + + private static class Data implements Saveable { Map map; + + @Override + public void save() throws IOException { + // We only implement Saveable so that RobustReflectionConverter logs deserialization problems. + } + } + + private static class DataRaw implements Saveable { + Map map; + + @Override + public void save() throws IOException { + // We only implement Saveable so that RobustReflectionConverter logs deserialization problems. + } } } diff --git a/core/src/test/java/hudson/util/RobustReflectionConverterTest.java b/core/src/test/java/hudson/util/RobustReflectionConverterTest.java index bb392132afdd8..9f3da4b754b3d 100644 --- a/core/src/test/java/hudson/util/RobustReflectionConverterTest.java +++ b/core/src/test/java/hudson/util/RobustReflectionConverterTest.java @@ -26,6 +26,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -41,6 +42,8 @@ import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.mapper.Mapper; import com.thoughtworks.xstream.security.InputManipulationException; +import hudson.model.Saveable; +import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -52,6 +55,9 @@ import java.util.logging.Logger; import jenkins.util.xstream.CriticalXStreamException; import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.jvnet.hudson.test.Issue; @@ -59,11 +65,22 @@ * @author Kohsuke Kawaguchi */ public class RobustReflectionConverterTest { + private final boolean originalRecordFailures = RobustReflectionConverter.RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS; static { Logger.getLogger(RobustReflectionConverter.class.getName()).setLevel(Level.OFF); } + @Before + public void before() { + RobustReflectionConverter.RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS = true; + } + + @After + public void after() { + RobustReflectionConverter.RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS = originalRecordFailures; + } + @Test public void robustUnmarshalling() { Point p = read(new XStream2()); @@ -132,8 +149,72 @@ public void implicitCollection() { "", xs.toXML(h)); } - public static class Hold { + @Ignore("Throws an NPE in writeValueToImplicitCollection. Issue has existed since RobustReflectionConverter was created.") + @Test + public void implicitCollectionsAllowNullElements() { + XStream2 xs = new XStream2(); + xs.alias("hold", Hold.class); + xs.addImplicitCollection(Hold.class, "items", "item", String.class); + Hold h = (Hold) xs.fromXML("b"); + assertThat(h.items, Matchers.containsInAnyOrder(null, "b")); + assertEquals("\n" + + " \n" + + " b\n" + + "", xs.toXML(h)); + } + + @Issue("JENKINS-63343") + @Test + public void robustAgainstImplicitCollectionElementsWithBadTypes() { + XStream2 xs = new XStream2(); + xs.alias("hold", Hold.class); + // Note that the fix only matters for `addImplicitCollection` overloads like the following where the element type is not provided. + xs.addImplicitCollection(Hold.class, "items"); + Hold h = (Hold) xs.fromXML( + """ + + 123 + abc + 456 + def + + """); + assertThat(h.items, equalTo(List.of("abc", "def"))); + } + + public static class Hold implements Saveable { List items; + + @Override + public void save() throws IOException { + // We only implement Saveable so that RobustReflectionConverter logs deserialization problems. + } + } + + @Test + public void implicitCollectionRawtypes() { + XStream2 xs = new XStream2(); + xs.alias("hold", HoldRaw.class); + xs.addImplicitCollection(HoldRaw.class, "items"); + var h = (HoldRaw) xs.fromXML( + """ + + 123 + abc + 456 + def + + """); + assertThat(h.items, equalTo(List.of(123, "abc", 456, "def"))); + } + + public static class HoldRaw implements Saveable { + List items; + + @Override + public void save() throws IOException { + // We only implement Saveable so that RobustReflectionConverter logs deserialization problems. + } } @Retention(RetentionPolicy.RUNTIME) @interface Owner { diff --git a/eslint.config.cjs b/eslint.config.cjs index f6c1c6181e461..0362bf627a7ae 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -18,7 +18,7 @@ module.exports = [ // External scripts ".pnp.cjs", ".pnp.loader.mjs", - "war/src/main/js/plugin-setup-wizard/bootstrap-detached.js", + "src/main/js/plugin-setup-wizard/bootstrap-detached.js", "war/src/main/webapp/scripts/yui/*", ], }, @@ -91,8 +91,8 @@ module.exports = [ { files: [ "eslint.config.cjs", - "war/postcss.config.js", - "war/webpack.config.js", + "postcss.config.js", + "webpack.config.js", ".stylelintrc.js", ], languageOptions: { diff --git a/package.json b/package.json index 2e182ee59b0df..1e9ae7d9161eb 100644 --- a/package.json +++ b/package.json @@ -10,45 +10,45 @@ }, "private": true, "scripts": { - "dev": "webpack --config war/webpack.config.js", - "prod": "webpack --config war/webpack.config.js --mode=production", + "dev": "webpack --config webpack.config.js", + "prod": "webpack --config webpack.config.js --mode=production", "build": "yarn prod", "start": "yarn dev --watch", "lint:js": "eslint . && prettier --check .", "lint:js-ci": "eslint . -f checkstyle -o target/eslint-warnings.xml && prettier --check .", - "lint:css": "stylelint war/src/main/scss", - "lint:css-ci": "stylelint war/src/main/scss --custom-formatter stylelint-checkstyle-reporter -o target/stylelint-warnings.xml", + "lint:css": "stylelint src/main/scss", + "lint:css-ci": "stylelint src/main/scss --custom-formatter stylelint-checkstyle-reporter -o target/stylelint-warnings.xml", "lint:ci": "yarn lint:js-ci && yarn lint:css-ci", - "lint:fix": "eslint --fix . && prettier --write . && stylelint war/src/main/scss --fix", + "lint:fix": "eslint --fix . && prettier --write . && stylelint src/main/scss --fix", "lint": "yarn lint:js && yarn lint:css" }, "devDependencies": { - "@babel/cli": "7.25.6", - "@babel/core": "7.25.2", - "@babel/preset-env": "7.25.4", - "@eslint/js": "9.10.0", + "@babel/cli": "7.25.7", + "@babel/core": "7.25.7", + "@babel/preset-env": "7.25.7", + "@eslint/js": "9.12.0", "babel-loader": "9.2.1", "clean-webpack-plugin": "4.0.0", "css-loader": "7.1.2", "css-minimizer-webpack-plugin": "7.0.0", - "eslint": "9.10.0", + "eslint": "9.12.0", "eslint-config-prettier": "9.1.0", "eslint-formatter-checkstyle": "8.40.0", - "globals": "15.9.0", + "globals": "15.11.0", "handlebars-loader": "1.7.3", "mini-css-extract-plugin": "2.9.1", "postcss": "8.4.47", "postcss-loader": "8.1.1", - "postcss-preset-env": "10.0.3", + "postcss-preset-env": "10.0.6", "postcss-scss": "4.0.9", "prettier": "3.3.3", - "sass": "1.79.2", - "sass-loader": "16.0.1", + "sass": "1.79.4", + "sass-loader": "16.0.2", "style-loader": "4.0.0", "stylelint": "16.9.0", "stylelint-checkstyle-reporter": "1.0.0", "stylelint-config-standard": "36.0.1", - "webpack": "5.94.0", + "webpack": "5.95.0", "webpack-cli": "5.1.4", "webpack-remove-empty-scripts": "1.0.4" }, @@ -65,5 +65,8 @@ "defaults", "not IE 11" ], + "engines": { + "node": ">=20.0.0" + }, "packageManager": "yarn@4.5.0" } diff --git a/pom.xml b/pom.xml index 56c71c9fee33c..1a472b3fd02a7 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci jenkins - 1.122 + 1.123 @@ -73,9 +73,9 @@ THE SOFTWARE. - 2.478 + 2.481 -SNAPSHOT - 2024-09-17T13:51:35Z + 2024-10-08T14:08:31Z github @@ -92,18 +92,13 @@ THE SOFTWARE. Max Medium - 1.33 + 1.34 4.13.2 - 1.29 + 1.30 false - 8.1 - 20.17.0 - - 1.22.19 - - 4.5.0 - cc00dce5de4f68d11450519a0f69eadf2a1cbe5cc0d8e740bfac817a31d76874 + 8.2 + 20.18.0 yarn install - yarn + corepack initialize + + yarn install + yarn build - yarn + corepack generate-sources - build + yarn build + + org.apache.maven.plugins + maven-clean-plugin + + + + + .yarn + false + + + node + false + + + node_modules + false + + + war/src/main/webapp/jsbundles + false + + + + @@ -517,11 +511,11 @@ THE SOFTWARE. yarn lint:ci - yarn + corepack test - lint:ci + yarn lint:ci ${yarn.lint.skip} @@ -550,11 +544,11 @@ THE SOFTWARE. yarn lint - yarn + corepack test - lint + yarn lint ${yarn.lint.skip} @@ -563,38 +557,5 @@ THE SOFTWARE. - - - clean-node - - - cleanNode - - - package.json - - - - - - org.apache.maven.plugins - maven-clean-plugin - - - - - node - false - - - node_modules - false - - - - - - - diff --git a/war/postcss.config.js b/postcss.config.js similarity index 100% rename from war/postcss.config.js rename to postcss.config.js diff --git a/war/src/main/js/add-item.js b/src/main/js/add-item.js similarity index 100% rename from war/src/main/js/add-item.js rename to src/main/js/add-item.js diff --git a/war/src/main/js/add-item.scss b/src/main/js/add-item.scss similarity index 100% rename from war/src/main/js/add-item.scss rename to src/main/js/add-item.scss diff --git a/war/src/main/js/api/pluginManager.js b/src/main/js/api/pluginManager.js similarity index 100% rename from war/src/main/js/api/pluginManager.js rename to src/main/js/api/pluginManager.js diff --git a/war/src/main/js/api/securityConfig.js b/src/main/js/api/securityConfig.js similarity index 100% rename from war/src/main/js/api/securityConfig.js rename to src/main/js/api/securityConfig.js diff --git a/war/src/main/js/app.js b/src/main/js/app.js similarity index 100% rename from war/src/main/js/app.js rename to src/main/js/app.js diff --git a/war/src/main/js/components/autocomplete/index.js b/src/main/js/components/autocomplete/index.js similarity index 100% rename from war/src/main/js/components/autocomplete/index.js rename to src/main/js/components/autocomplete/index.js diff --git a/war/src/main/js/components/confirmation-link/index.js b/src/main/js/components/confirmation-link/index.js similarity index 100% rename from war/src/main/js/components/confirmation-link/index.js rename to src/main/js/components/confirmation-link/index.js diff --git a/war/src/main/js/components/dialogs/index.js b/src/main/js/components/dialogs/index.js similarity index 100% rename from war/src/main/js/components/dialogs/index.js rename to src/main/js/components/dialogs/index.js diff --git a/war/src/main/js/components/dropdowns/hetero-list.js b/src/main/js/components/dropdowns/hetero-list.js similarity index 100% rename from war/src/main/js/components/dropdowns/hetero-list.js rename to src/main/js/components/dropdowns/hetero-list.js diff --git a/war/src/main/js/components/dropdowns/index.js b/src/main/js/components/dropdowns/index.js similarity index 100% rename from war/src/main/js/components/dropdowns/index.js rename to src/main/js/components/dropdowns/index.js diff --git a/war/src/main/js/components/dropdowns/inpage-jumplist.js b/src/main/js/components/dropdowns/inpage-jumplist.js similarity index 100% rename from war/src/main/js/components/dropdowns/inpage-jumplist.js rename to src/main/js/components/dropdowns/inpage-jumplist.js diff --git a/war/src/main/js/components/dropdowns/jumplists.js b/src/main/js/components/dropdowns/jumplists.js similarity index 100% rename from war/src/main/js/components/dropdowns/jumplists.js rename to src/main/js/components/dropdowns/jumplists.js diff --git a/war/src/main/js/components/dropdowns/overflow-button.js b/src/main/js/components/dropdowns/overflow-button.js similarity index 100% rename from war/src/main/js/components/dropdowns/overflow-button.js rename to src/main/js/components/dropdowns/overflow-button.js diff --git a/war/src/main/js/components/dropdowns/templates.js b/src/main/js/components/dropdowns/templates.js similarity index 100% rename from war/src/main/js/components/dropdowns/templates.js rename to src/main/js/components/dropdowns/templates.js diff --git a/war/src/main/js/components/dropdowns/utils.js b/src/main/js/components/dropdowns/utils.js similarity index 98% rename from war/src/main/js/components/dropdowns/utils.js rename to src/main/js/components/dropdowns/utils.js index a15c4d2c2ba5d..27931dc7de322 100644 --- a/war/src/main/js/components/dropdowns/utils.js +++ b/src/main/js/components/dropdowns/utils.js @@ -12,6 +12,10 @@ const SELECTED_ITEM_CLASS = "jenkins-dropdown__item--selected"; * @param callback - called to retrieve the list of dropdown items */ function generateDropdown(element, callback, immediate) { + if (element._tippy && element._tippy.props.theme === "dropdown") { + element._tippy.destroy(); + } + tippy( element, Object.assign({}, Templates.dropdown(), { diff --git a/war/src/main/js/components/notifications/index.js b/src/main/js/components/notifications/index.js similarity index 100% rename from war/src/main/js/components/notifications/index.js rename to src/main/js/components/notifications/index.js diff --git a/war/src/main/js/components/row-selection-controller/index.js b/src/main/js/components/row-selection-controller/index.js similarity index 100% rename from war/src/main/js/components/row-selection-controller/index.js rename to src/main/js/components/row-selection-controller/index.js diff --git a/war/src/main/js/components/search-bar/index.js b/src/main/js/components/search-bar/index.js similarity index 100% rename from war/src/main/js/components/search-bar/index.js rename to src/main/js/components/search-bar/index.js diff --git a/war/src/main/js/components/stop-button-link/index.js b/src/main/js/components/stop-button-link/index.js similarity index 100% rename from war/src/main/js/components/stop-button-link/index.js rename to src/main/js/components/stop-button-link/index.js diff --git a/war/src/main/js/components/tooltips/index.js b/src/main/js/components/tooltips/index.js similarity index 100% rename from war/src/main/js/components/tooltips/index.js rename to src/main/js/components/tooltips/index.js diff --git a/war/src/main/js/handlebars-helpers/id.js b/src/main/js/handlebars-helpers/id.js similarity index 100% rename from war/src/main/js/handlebars-helpers/id.js rename to src/main/js/handlebars-helpers/id.js diff --git a/war/src/main/js/handlebars-helpers/ifeq.js b/src/main/js/handlebars-helpers/ifeq.js similarity index 100% rename from war/src/main/js/handlebars-helpers/ifeq.js rename to src/main/js/handlebars-helpers/ifeq.js diff --git a/war/src/main/js/handlebars-helpers/ifneq.js b/src/main/js/handlebars-helpers/ifneq.js similarity index 100% rename from war/src/main/js/handlebars-helpers/ifneq.js rename to src/main/js/handlebars-helpers/ifneq.js diff --git a/war/src/main/js/handlebars-helpers/in-array.js b/src/main/js/handlebars-helpers/in-array.js similarity index 100% rename from war/src/main/js/handlebars-helpers/in-array.js rename to src/main/js/handlebars-helpers/in-array.js diff --git a/war/src/main/js/handlebars-helpers/replace.js b/src/main/js/handlebars-helpers/replace.js similarity index 100% rename from war/src/main/js/handlebars-helpers/replace.js rename to src/main/js/handlebars-helpers/replace.js diff --git a/war/src/main/js/keyboard-shortcuts.js b/src/main/js/keyboard-shortcuts.js similarity index 100% rename from war/src/main/js/keyboard-shortcuts.js rename to src/main/js/keyboard-shortcuts.js diff --git a/war/src/main/js/pages/cloud-set/index.js b/src/main/js/pages/cloud-set/index.js similarity index 100% rename from war/src/main/js/pages/cloud-set/index.js rename to src/main/js/pages/cloud-set/index.js diff --git a/war/src/main/js/pages/cloud-set/index.scss b/src/main/js/pages/cloud-set/index.scss similarity index 100% rename from war/src/main/js/pages/cloud-set/index.scss rename to src/main/js/pages/cloud-set/index.scss diff --git a/war/src/main/js/pages/computer-set/index.js b/src/main/js/pages/computer-set/index.js similarity index 100% rename from war/src/main/js/pages/computer-set/index.js rename to src/main/js/pages/computer-set/index.js diff --git a/war/src/main/js/pages/dashboard/index.js b/src/main/js/pages/dashboard/index.js similarity index 100% rename from war/src/main/js/pages/dashboard/index.js rename to src/main/js/pages/dashboard/index.js diff --git a/war/src/main/js/pages/manage-jenkins/index.js b/src/main/js/pages/manage-jenkins/index.js similarity index 100% rename from war/src/main/js/pages/manage-jenkins/index.js rename to src/main/js/pages/manage-jenkins/index.js diff --git a/war/src/main/js/pages/manage-jenkins/system-information/index.js b/src/main/js/pages/manage-jenkins/system-information/index.js similarity index 100% rename from war/src/main/js/pages/manage-jenkins/system-information/index.js rename to src/main/js/pages/manage-jenkins/system-information/index.js diff --git a/war/src/main/js/pages/project/builds-card.js b/src/main/js/pages/project/builds-card.js similarity index 100% rename from war/src/main/js/pages/project/builds-card.js rename to src/main/js/pages/project/builds-card.js diff --git a/war/src/main/js/pages/project/builds-card.types.js b/src/main/js/pages/project/builds-card.types.js similarity index 100% rename from war/src/main/js/pages/project/builds-card.types.js rename to src/main/js/pages/project/builds-card.types.js diff --git a/war/src/main/js/pages/register/index.js b/src/main/js/pages/register/index.js similarity index 100% rename from war/src/main/js/pages/register/index.js rename to src/main/js/pages/register/index.js diff --git a/war/src/main/js/plugin-manager-ui.js b/src/main/js/plugin-manager-ui.js similarity index 100% rename from war/src/main/js/plugin-manager-ui.js rename to src/main/js/plugin-manager-ui.js diff --git a/war/src/main/js/plugin-setup-wizard/bootstrap-detached.js b/src/main/js/plugin-setup-wizard/bootstrap-detached.js similarity index 100% rename from war/src/main/js/plugin-setup-wizard/bootstrap-detached.js rename to src/main/js/plugin-setup-wizard/bootstrap-detached.js diff --git a/war/src/main/js/pluginSetupWizard.js b/src/main/js/pluginSetupWizard.js similarity index 100% rename from war/src/main/js/pluginSetupWizard.js rename to src/main/js/pluginSetupWizard.js diff --git a/war/src/main/js/pluginSetupWizardGui.js b/src/main/js/pluginSetupWizardGui.js similarity index 100% rename from war/src/main/js/pluginSetupWizardGui.js rename to src/main/js/pluginSetupWizardGui.js diff --git a/war/src/main/js/section-to-sidebar-items.js b/src/main/js/section-to-sidebar-items.js similarity index 100% rename from war/src/main/js/section-to-sidebar-items.js rename to src/main/js/section-to-sidebar-items.js diff --git a/war/src/main/js/section-to-tabs.js b/src/main/js/section-to-tabs.js similarity index 100% rename from war/src/main/js/section-to-tabs.js rename to src/main/js/section-to-tabs.js diff --git a/war/src/main/js/sortable-drag-drop.js b/src/main/js/sortable-drag-drop.js similarity index 100% rename from war/src/main/js/sortable-drag-drop.js rename to src/main/js/sortable-drag-drop.js diff --git a/war/src/main/js/templates/configureInstance.hbs b/src/main/js/templates/configureInstance.hbs similarity index 100% rename from war/src/main/js/templates/configureInstance.hbs rename to src/main/js/templates/configureInstance.hbs diff --git a/war/src/main/js/templates/errorPanel.hbs b/src/main/js/templates/errorPanel.hbs similarity index 100% rename from war/src/main/js/templates/errorPanel.hbs rename to src/main/js/templates/errorPanel.hbs diff --git a/war/src/main/js/templates/firstUserPanel.hbs b/src/main/js/templates/firstUserPanel.hbs similarity index 100% rename from war/src/main/js/templates/firstUserPanel.hbs rename to src/main/js/templates/firstUserPanel.hbs diff --git a/war/src/main/js/templates/incompleteInstallationPanel.hbs b/src/main/js/templates/incompleteInstallationPanel.hbs similarity index 100% rename from war/src/main/js/templates/incompleteInstallationPanel.hbs rename to src/main/js/templates/incompleteInstallationPanel.hbs diff --git a/war/src/main/js/templates/loadingPanel.hbs b/src/main/js/templates/loadingPanel.hbs similarity index 100% rename from war/src/main/js/templates/loadingPanel.hbs rename to src/main/js/templates/loadingPanel.hbs diff --git a/war/src/main/js/templates/offlinePanel.hbs b/src/main/js/templates/offlinePanel.hbs similarity index 100% rename from war/src/main/js/templates/offlinePanel.hbs rename to src/main/js/templates/offlinePanel.hbs diff --git a/war/src/main/js/templates/plugin-manager/available.hbs b/src/main/js/templates/plugin-manager/available.hbs similarity index 100% rename from war/src/main/js/templates/plugin-manager/available.hbs rename to src/main/js/templates/plugin-manager/available.hbs diff --git a/war/src/main/js/templates/pluginSelectList.hbs b/src/main/js/templates/pluginSelectList.hbs similarity index 100% rename from war/src/main/js/templates/pluginSelectList.hbs rename to src/main/js/templates/pluginSelectList.hbs diff --git a/war/src/main/js/templates/pluginSelectionPanel.hbs b/src/main/js/templates/pluginSelectionPanel.hbs similarity index 100% rename from war/src/main/js/templates/pluginSelectionPanel.hbs rename to src/main/js/templates/pluginSelectionPanel.hbs diff --git a/war/src/main/js/templates/pluginSetupWizard.hbs b/src/main/js/templates/pluginSetupWizard.hbs similarity index 100% rename from war/src/main/js/templates/pluginSetupWizard.hbs rename to src/main/js/templates/pluginSetupWizard.hbs diff --git a/war/src/main/js/templates/progressPanel.hbs b/src/main/js/templates/progressPanel.hbs similarity index 100% rename from war/src/main/js/templates/progressPanel.hbs rename to src/main/js/templates/progressPanel.hbs diff --git a/war/src/main/js/templates/proxyConfigPanel.hbs b/src/main/js/templates/proxyConfigPanel.hbs similarity index 100% rename from war/src/main/js/templates/proxyConfigPanel.hbs rename to src/main/js/templates/proxyConfigPanel.hbs diff --git a/war/src/main/js/templates/setupCompletePanel.hbs b/src/main/js/templates/setupCompletePanel.hbs similarity index 100% rename from war/src/main/js/templates/setupCompletePanel.hbs rename to src/main/js/templates/setupCompletePanel.hbs diff --git a/war/src/main/js/templates/successPanel.hbs b/src/main/js/templates/successPanel.hbs similarity index 100% rename from war/src/main/js/templates/successPanel.hbs rename to src/main/js/templates/successPanel.hbs diff --git a/war/src/main/js/templates/welcomePanel.hbs b/src/main/js/templates/welcomePanel.hbs similarity index 100% rename from war/src/main/js/templates/welcomePanel.hbs rename to src/main/js/templates/welcomePanel.hbs diff --git a/war/src/main/js/util/behavior-shim.js b/src/main/js/util/behavior-shim.js similarity index 100% rename from war/src/main/js/util/behavior-shim.js rename to src/main/js/util/behavior-shim.js diff --git a/war/src/main/js/util/dom.js b/src/main/js/util/dom.js similarity index 100% rename from war/src/main/js/util/dom.js rename to src/main/js/util/dom.js diff --git a/war/src/main/js/util/i18n.js b/src/main/js/util/i18n.js similarity index 100% rename from war/src/main/js/util/i18n.js rename to src/main/js/util/i18n.js diff --git a/war/src/main/js/util/jenkins.js b/src/main/js/util/jenkins.js similarity index 100% rename from war/src/main/js/util/jenkins.js rename to src/main/js/util/jenkins.js diff --git a/war/src/main/js/util/jenkinsLocalStorage.js b/src/main/js/util/jenkinsLocalStorage.js similarity index 100% rename from war/src/main/js/util/jenkinsLocalStorage.js rename to src/main/js/util/jenkinsLocalStorage.js diff --git a/war/src/main/js/util/jquery-ext.js b/src/main/js/util/jquery-ext.js similarity index 100% rename from war/src/main/js/util/jquery-ext.js rename to src/main/js/util/jquery-ext.js diff --git a/war/src/main/js/util/keyboard.js b/src/main/js/util/keyboard.js similarity index 100% rename from war/src/main/js/util/keyboard.js rename to src/main/js/util/keyboard.js diff --git a/war/src/main/js/util/localStorage.js b/src/main/js/util/localStorage.js similarity index 100% rename from war/src/main/js/util/localStorage.js rename to src/main/js/util/localStorage.js diff --git a/war/src/main/js/util/page.js b/src/main/js/util/page.js similarity index 100% rename from war/src/main/js/util/page.js rename to src/main/js/util/page.js diff --git a/war/src/main/js/util/path.js b/src/main/js/util/path.js similarity index 100% rename from war/src/main/js/util/path.js rename to src/main/js/util/path.js diff --git a/war/src/main/js/util/security.js b/src/main/js/util/security.js similarity index 100% rename from war/src/main/js/util/security.js rename to src/main/js/util/security.js diff --git a/war/src/main/js/util/symbols.js b/src/main/js/util/symbols.js similarity index 100% rename from war/src/main/js/util/symbols.js rename to src/main/js/util/symbols.js diff --git a/war/src/main/js/widgets/add/addform.scss b/src/main/js/widgets/add/addform.scss similarity index 100% rename from war/src/main/js/widgets/add/addform.scss rename to src/main/js/widgets/add/addform.scss diff --git a/war/src/main/scss/_bootstrap.scss b/src/main/scss/_bootstrap.scss similarity index 100% rename from war/src/main/scss/_bootstrap.scss rename to src/main/scss/_bootstrap.scss diff --git a/war/src/main/scss/abstracts/_colors.scss b/src/main/scss/abstracts/_colors.scss similarity index 100% rename from war/src/main/scss/abstracts/_colors.scss rename to src/main/scss/abstracts/_colors.scss diff --git a/war/src/main/scss/abstracts/_index.scss b/src/main/scss/abstracts/_index.scss similarity index 100% rename from war/src/main/scss/abstracts/_index.scss rename to src/main/scss/abstracts/_index.scss diff --git a/war/src/main/scss/abstracts/_mixins.scss b/src/main/scss/abstracts/_mixins.scss similarity index 100% rename from war/src/main/scss/abstracts/_mixins.scss rename to src/main/scss/abstracts/_mixins.scss diff --git a/war/src/main/scss/abstracts/_theme.scss b/src/main/scss/abstracts/_theme.scss similarity index 96% rename from war/src/main/scss/abstracts/_theme.scss rename to src/main/scss/abstracts/_theme.scss index c108634f7c851..0558369c9ac36 100644 --- a/war/src/main/scss/abstracts/_theme.scss +++ b/src/main/scss/abstracts/_theme.scss @@ -331,12 +331,6 @@ $semantics: ( // Plugin manager --plugin-manager-bg-color-already-upgraded: var(--light-grey); - --plugin-manager-category-link-bg-color: var(--very-light-grey); - --plugin-manager-category-link-bg-color--hover: #f2f2f2; - --plugin-manager-category-link-border-color: var(--medium-grey); - --plugin-manager-category-link-border-color--hover: var(--black); - --plugin-manager-category-text-color: var(--text-color); - --plugin-manager-category-link-color--hover: var(--text-color); // Auto complete --auto-complete-bg-color--prehighlight: #b3d4ff; diff --git a/war/src/main/scss/base/_breakpoints.scss b/src/main/scss/base/_breakpoints.scss similarity index 100% rename from war/src/main/scss/base/_breakpoints.scss rename to src/main/scss/base/_breakpoints.scss diff --git a/war/src/main/scss/base/_core.scss b/src/main/scss/base/_core.scss similarity index 100% rename from war/src/main/scss/base/_core.scss rename to src/main/scss/base/_core.scss diff --git a/war/src/main/scss/base/_display.scss b/src/main/scss/base/_display.scss similarity index 100% rename from war/src/main/scss/base/_display.scss rename to src/main/scss/base/_display.scss diff --git a/war/src/main/scss/base/_index.scss b/src/main/scss/base/_index.scss similarity index 100% rename from war/src/main/scss/base/_index.scss rename to src/main/scss/base/_index.scss diff --git a/war/src/main/scss/base/_layout-commons.scss b/src/main/scss/base/_layout-commons.scss similarity index 100% rename from war/src/main/scss/base/_layout-commons.scss rename to src/main/scss/base/_layout-commons.scss diff --git a/war/src/main/scss/base/_spacing.scss b/src/main/scss/base/_spacing.scss similarity index 100% rename from war/src/main/scss/base/_spacing.scss rename to src/main/scss/base/_spacing.scss diff --git a/war/src/main/scss/base/_style.scss b/src/main/scss/base/_style.scss similarity index 100% rename from war/src/main/scss/base/_style.scss rename to src/main/scss/base/_style.scss diff --git a/war/src/main/scss/base/_typography.scss b/src/main/scss/base/_typography.scss similarity index 100% rename from war/src/main/scss/base/_typography.scss rename to src/main/scss/base/_typography.scss diff --git a/war/src/main/scss/base/_visibility-utils.scss b/src/main/scss/base/_visibility-utils.scss similarity index 100% rename from war/src/main/scss/base/_visibility-utils.scss rename to src/main/scss/base/_visibility-utils.scss diff --git a/war/src/main/scss/base/_yui-compatibility.scss b/src/main/scss/base/_yui-compatibility.scss similarity index 100% rename from war/src/main/scss/base/_yui-compatibility.scss rename to src/main/scss/base/_yui-compatibility.scss diff --git a/war/src/main/scss/components/_alert.scss b/src/main/scss/components/_alert.scss similarity index 100% rename from war/src/main/scss/components/_alert.scss rename to src/main/scss/components/_alert.scss diff --git a/war/src/main/scss/components/_app-bar.scss b/src/main/scss/components/_app-bar.scss similarity index 100% rename from war/src/main/scss/components/_app-bar.scss rename to src/main/scss/components/_app-bar.scss diff --git a/src/main/scss/components/_badges.scss b/src/main/scss/components/_badges.scss new file mode 100644 index 0000000000000..45fc8b9dc8039 --- /dev/null +++ b/src/main/scss/components/_badges.scss @@ -0,0 +1,24 @@ +.jenkins-badge { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 100px; + font-size: 0.6875rem; + font-weight: 400; + min-height: 20px; + min-width: 20px; + padding: 0 0.4rem; + background: color-mix( + in sRGB, + var(--text-color-secondary) 12.5%, + transparent + ); + + &[class*="color"] { + background: color-mix(in sRGB, var(--color) 85%, transparent); + color: var(--white) !important; + box-shadow: inset 0 -1px 2px var(--color, var(--text-color-secondary)); + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(2.5px); + } +} diff --git a/war/src/main/scss/components/_breadcrumbs.scss b/src/main/scss/components/_breadcrumbs.scss similarity index 100% rename from war/src/main/scss/components/_breadcrumbs.scss rename to src/main/scss/components/_breadcrumbs.scss diff --git a/war/src/main/scss/components/_buttons-deprecated.scss b/src/main/scss/components/_buttons-deprecated.scss similarity index 100% rename from war/src/main/scss/components/_buttons-deprecated.scss rename to src/main/scss/components/_buttons-deprecated.scss diff --git a/war/src/main/scss/components/_buttons.scss b/src/main/scss/components/_buttons.scss similarity index 100% rename from war/src/main/scss/components/_buttons.scss rename to src/main/scss/components/_buttons.scss diff --git a/war/src/main/scss/components/_cards.scss b/src/main/scss/components/_cards.scss similarity index 100% rename from war/src/main/scss/components/_cards.scss rename to src/main/scss/components/_cards.scss diff --git a/war/src/main/scss/components/_content-blocks.scss b/src/main/scss/components/_content-blocks.scss similarity index 100% rename from war/src/main/scss/components/_content-blocks.scss rename to src/main/scss/components/_content-blocks.scss diff --git a/war/src/main/scss/components/_dialogs.scss b/src/main/scss/components/_dialogs.scss similarity index 100% rename from war/src/main/scss/components/_dialogs.scss rename to src/main/scss/components/_dialogs.scss diff --git a/war/src/main/scss/components/_dropdowns.scss b/src/main/scss/components/_dropdowns.scss similarity index 100% rename from war/src/main/scss/components/_dropdowns.scss rename to src/main/scss/components/_dropdowns.scss diff --git a/war/src/main/scss/components/_icons.scss b/src/main/scss/components/_icons.scss similarity index 100% rename from war/src/main/scss/components/_icons.scss rename to src/main/scss/components/_icons.scss diff --git a/war/src/main/scss/components/_index.scss b/src/main/scss/components/_index.scss similarity index 100% rename from war/src/main/scss/components/_index.scss rename to src/main/scss/components/_index.scss diff --git a/war/src/main/scss/components/_notice.scss b/src/main/scss/components/_notice.scss similarity index 100% rename from war/src/main/scss/components/_notice.scss rename to src/main/scss/components/_notice.scss diff --git a/war/src/main/scss/components/_notifications.scss b/src/main/scss/components/_notifications.scss similarity index 100% rename from war/src/main/scss/components/_notifications.scss rename to src/main/scss/components/_notifications.scss diff --git a/war/src/main/scss/components/_page-footer.scss b/src/main/scss/components/_page-footer.scss similarity index 100% rename from war/src/main/scss/components/_page-footer.scss rename to src/main/scss/components/_page-footer.scss diff --git a/war/src/main/scss/components/_page-header.scss b/src/main/scss/components/_page-header.scss similarity index 100% rename from war/src/main/scss/components/_page-header.scss rename to src/main/scss/components/_page-header.scss diff --git a/war/src/main/scss/components/_panes-and-bigtable.scss b/src/main/scss/components/_panes-and-bigtable.scss similarity index 100% rename from war/src/main/scss/components/_panes-and-bigtable.scss rename to src/main/scss/components/_panes-and-bigtable.scss diff --git a/war/src/main/scss/components/_progress-animation.scss b/src/main/scss/components/_progress-animation.scss similarity index 100% rename from war/src/main/scss/components/_progress-animation.scss rename to src/main/scss/components/_progress-animation.scss diff --git a/war/src/main/scss/components/_progress-bar.scss b/src/main/scss/components/_progress-bar.scss similarity index 100% rename from war/src/main/scss/components/_progress-bar.scss rename to src/main/scss/components/_progress-bar.scss diff --git a/war/src/main/scss/components/_row-selection-controller.scss b/src/main/scss/components/_row-selection-controller.scss similarity index 100% rename from war/src/main/scss/components/_row-selection-controller.scss rename to src/main/scss/components/_row-selection-controller.scss diff --git a/war/src/main/scss/components/_section.scss b/src/main/scss/components/_section.scss similarity index 100% rename from war/src/main/scss/components/_section.scss rename to src/main/scss/components/_section.scss diff --git a/war/src/main/scss/components/_side-panel-tasks.scss b/src/main/scss/components/_side-panel-tasks.scss similarity index 100% rename from war/src/main/scss/components/_side-panel-tasks.scss rename to src/main/scss/components/_side-panel-tasks.scss diff --git a/war/src/main/scss/components/_side-panel-widgets.scss b/src/main/scss/components/_side-panel-widgets.scss similarity index 100% rename from war/src/main/scss/components/_side-panel-widgets.scss rename to src/main/scss/components/_side-panel-widgets.scss diff --git a/war/src/main/scss/components/_skip-link.scss b/src/main/scss/components/_skip-link.scss similarity index 100% rename from war/src/main/scss/components/_skip-link.scss rename to src/main/scss/components/_skip-link.scss diff --git a/war/src/main/scss/components/_spinner.scss b/src/main/scss/components/_spinner.scss similarity index 100% rename from war/src/main/scss/components/_spinner.scss rename to src/main/scss/components/_spinner.scss diff --git a/war/src/main/scss/components/_table.scss b/src/main/scss/components/_table.scss similarity index 100% rename from war/src/main/scss/components/_table.scss rename to src/main/scss/components/_table.scss diff --git a/war/src/main/scss/components/_tabs.scss b/src/main/scss/components/_tabs.scss similarity index 100% rename from war/src/main/scss/components/_tabs.scss rename to src/main/scss/components/_tabs.scss diff --git a/war/src/main/scss/components/_tooltips.scss b/src/main/scss/components/_tooltips.scss similarity index 100% rename from war/src/main/scss/components/_tooltips.scss rename to src/main/scss/components/_tooltips.scss diff --git a/war/src/main/scss/form/_checkbox.scss b/src/main/scss/form/_checkbox.scss similarity index 99% rename from war/src/main/scss/form/_checkbox.scss rename to src/main/scss/form/_checkbox.scss index d9cd5f18c9518..6e5633dd991f4 100644 --- a/war/src/main/scss/form/_checkbox.scss +++ b/src/main/scss/form/_checkbox.scss @@ -187,9 +187,7 @@ } .jenkins-checkbox__description { - margin-top: 0.3rem; margin-left: 34px; - margin-bottom: 1rem; color: var(--text-color-secondary); line-height: 1.66; } diff --git a/war/src/main/scss/form/_codemirror.scss b/src/main/scss/form/_codemirror.scss similarity index 100% rename from war/src/main/scss/form/_codemirror.scss rename to src/main/scss/form/_codemirror.scss diff --git a/war/src/main/scss/form/_file-upload.scss b/src/main/scss/form/_file-upload.scss similarity index 100% rename from war/src/main/scss/form/_file-upload.scss rename to src/main/scss/form/_file-upload.scss diff --git a/war/src/main/scss/form/_index.scss b/src/main/scss/form/_index.scss similarity index 100% rename from war/src/main/scss/form/_index.scss rename to src/main/scss/form/_index.scss diff --git a/war/src/main/scss/form/_input.scss b/src/main/scss/form/_input.scss similarity index 100% rename from war/src/main/scss/form/_input.scss rename to src/main/scss/form/_input.scss diff --git a/war/src/main/scss/form/_layout.scss b/src/main/scss/form/_layout.scss similarity index 100% rename from war/src/main/scss/form/_layout.scss rename to src/main/scss/form/_layout.scss diff --git a/war/src/main/scss/form/_radio.scss b/src/main/scss/form/_radio.scss similarity index 100% rename from war/src/main/scss/form/_radio.scss rename to src/main/scss/form/_radio.scss diff --git a/war/src/main/scss/form/_reorderable-list.scss b/src/main/scss/form/_reorderable-list.scss similarity index 100% rename from war/src/main/scss/form/_reorderable-list.scss rename to src/main/scss/form/_reorderable-list.scss diff --git a/war/src/main/scss/form/_search-bar.scss b/src/main/scss/form/_search-bar.scss similarity index 100% rename from war/src/main/scss/form/_search-bar.scss rename to src/main/scss/form/_search-bar.scss diff --git a/war/src/main/scss/form/_select.scss b/src/main/scss/form/_select.scss similarity index 100% rename from war/src/main/scss/form/_select.scss rename to src/main/scss/form/_select.scss diff --git a/war/src/main/scss/form/_textarea.scss b/src/main/scss/form/_textarea.scss similarity index 100% rename from war/src/main/scss/form/_textarea.scss rename to src/main/scss/form/_textarea.scss diff --git a/war/src/main/scss/form/_toggle-switch.scss b/src/main/scss/form/_toggle-switch.scss similarity index 100% rename from war/src/main/scss/form/_toggle-switch.scss rename to src/main/scss/form/_toggle-switch.scss diff --git a/war/src/main/scss/form/_validation.scss b/src/main/scss/form/_validation.scss similarity index 100% rename from war/src/main/scss/form/_validation.scss rename to src/main/scss/form/_validation.scss diff --git a/war/src/main/scss/pages/_about.scss b/src/main/scss/pages/_about.scss similarity index 100% rename from war/src/main/scss/pages/_about.scss rename to src/main/scss/pages/_about.scss diff --git a/war/src/main/scss/pages/_build.scss b/src/main/scss/pages/_build.scss similarity index 100% rename from war/src/main/scss/pages/_build.scss rename to src/main/scss/pages/_build.scss diff --git a/war/src/main/scss/pages/_dashboard.scss b/src/main/scss/pages/_dashboard.scss similarity index 100% rename from war/src/main/scss/pages/_dashboard.scss rename to src/main/scss/pages/_dashboard.scss diff --git a/war/src/main/scss/pages/_icon-legend.scss b/src/main/scss/pages/_icon-legend.scss similarity index 100% rename from war/src/main/scss/pages/_icon-legend.scss rename to src/main/scss/pages/_icon-legend.scss diff --git a/war/src/main/scss/pages/_index.scss b/src/main/scss/pages/_index.scss similarity index 100% rename from war/src/main/scss/pages/_index.scss rename to src/main/scss/pages/_index.scss diff --git a/war/src/main/scss/pages/_job.scss b/src/main/scss/pages/_job.scss similarity index 95% rename from war/src/main/scss/pages/_job.scss rename to src/main/scss/pages/_job.scss index 3b982fcffcf40..8692d19dc3f99 100644 --- a/war/src/main/scss/pages/_job.scss +++ b/src/main/scss/pages/_job.scss @@ -129,9 +129,12 @@ font-weight: 450; flex-grow: 1; padding: 0.45rem 0 0; + word-break: normal; + overflow-wrap: anywhere; .app-builds-container__item__time { color: var(--text-color-secondary); + white-space: nowrap; } } @@ -168,6 +171,8 @@ padding-left: 2.25rem; margin-top: -2px; grid-column: 1 / span 2; + word-break: normal; + overflow-wrap: anywhere; &::before { content: ""; diff --git a/war/src/main/scss/pages/_manage-jenkins.scss b/src/main/scss/pages/_manage-jenkins.scss similarity index 100% rename from war/src/main/scss/pages/_manage-jenkins.scss rename to src/main/scss/pages/_manage-jenkins.scss diff --git a/war/src/main/scss/pages/_plugin-manager.scss b/src/main/scss/pages/_plugin-manager.scss similarity index 100% rename from war/src/main/scss/pages/_plugin-manager.scss rename to src/main/scss/pages/_plugin-manager.scss diff --git a/war/src/main/scss/pages/_setupWizardConfigureInstance.scss b/src/main/scss/pages/_setupWizardConfigureInstance.scss similarity index 100% rename from war/src/main/scss/pages/_setupWizardConfigureInstance.scss rename to src/main/scss/pages/_setupWizardConfigureInstance.scss diff --git a/war/src/main/scss/pages/_setupWizardFirstUser.scss b/src/main/scss/pages/_setupWizardFirstUser.scss similarity index 100% rename from war/src/main/scss/pages/_setupWizardFirstUser.scss rename to src/main/scss/pages/_setupWizardFirstUser.scss diff --git a/war/src/main/scss/pages/_sign-in-register.scss b/src/main/scss/pages/_sign-in-register.scss similarity index 100% rename from war/src/main/scss/pages/_sign-in-register.scss rename to src/main/scss/pages/_sign-in-register.scss diff --git a/war/src/main/scss/pluginSetupWizard.scss b/src/main/scss/pluginSetupWizard.scss similarity index 100% rename from war/src/main/scss/pluginSetupWizard.scss rename to src/main/scss/pluginSetupWizard.scss diff --git a/war/src/main/scss/simple-page.scss b/src/main/scss/simple-page.scss similarity index 100% rename from war/src/main/scss/simple-page.scss rename to src/main/scss/simple-page.scss diff --git a/war/src/main/scss/styles.scss b/src/main/scss/styles.scss similarity index 100% rename from war/src/main/scss/styles.scss rename to src/main/scss/styles.scss diff --git a/test/pom.xml b/test/pom.xml index 0f3aa999e08e8..ba67c98041371 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -112,7 +112,7 @@ THE SOFTWARE. org.jenkins-ci.plugins scm-api - 696.v778d637b_a_762 + 698.v8e3b_c788f0a_6 @@ -178,7 +178,7 @@ THE SOFTWARE. org.jenkins-ci.main jenkins-test-harness - 2289.vfd344a_6d1660 + 2341.v35346d95d2b_7 test @@ -206,7 +206,7 @@ THE SOFTWARE. org.jenkins-ci.modules instance-identity - 185.v303dc7c645f9 + 201.vd2a_b_5a_468a_a_6 test @@ -218,25 +218,25 @@ THE SOFTWARE. org.jenkins-ci.plugins cloudbees-folder - 6.951.v5f91d88d76b_b_ + 6.955.v81e2a_35c08d3 test org.jenkins-ci.plugins credentials - 1378.v81ef4269d764 + 1389.vd7a_b_f5fa_50a_2 test org.jenkins-ci.plugins junit - 1300.v03d9d8a_cf1fb_ + 1304.vc85a_b_ca_96613 test org.jenkins-ci.plugins mailer - 472.vf7c289a_4b_420 + 488.v0c9639c1a_eb_3 test @@ -248,7 +248,7 @@ THE SOFTWARE. org.jenkins-ci.plugins matrix-project - 832.va_66e270d2946 + 839.vff91cd7e3a_b_2 test diff --git a/test/src/test/java/hudson/cli/DeleteBuildsCommandTest.java b/test/src/test/java/hudson/cli/DeleteBuildsCommandTest.java index dfac64fc1b5aa..5e2e917e98082 100644 --- a/test/src/test/java/hudson/cli/DeleteBuildsCommandTest.java +++ b/test/src/test/java/hudson/cli/DeleteBuildsCommandTest.java @@ -32,21 +32,18 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertNotNull; -import static org.junit.Assume.assumeFalse; -import hudson.Functions; import hudson.model.ExecutorTest; import hudson.model.FreeStyleProject; import hudson.model.Item; import hudson.model.Run; import hudson.model.labels.LabelAtom; import hudson.tasks.Shell; -import java.io.IOException; import jenkins.model.Jenkins; -import org.junit.AssumptionViolatedException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; /** @@ -139,8 +136,8 @@ public class DeleteBuildsCommandTest { assertThat(result.stdout(), containsString("Deleted 0 builds")); } - @Test public void deleteBuildsShouldSuccessEvenTheBuildIsRunning() throws Exception { - assumeFalse("You can't delete files that are in use on Windows", Functions.isWindows()); + @Issue("JENKINS-73835") + @Test public void deleteBuildsShouldFailIfTheBuildIsRunning() throws Exception { FreeStyleProject project = j.createFreeStyleProject("aProject"); ExecutorTest.startBlockingBuild(project); assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1)); @@ -148,15 +145,9 @@ public class DeleteBuildsCommandTest { final CLICommandInvoker.Result result = command .authorizedTo(Jenkins.READ, Item.READ, Run.DELETE) .invokeWithArgs("aProject", "1"); - assertThat(result, succeeded()); - assertThat(result.stdout(), containsString("Deleted 1 builds")); - assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0)); - assertThat(project.isBuilding(), equalTo(false)); - try { - project.delete(); - } catch (IOException | InterruptedException x) { - throw new AssumptionViolatedException("Could not delete test project (race condition?)", x); - } + assertThat(result, failedWith(1)); + assertThat(result, hasNoStandardOutput()); + assertThat(result.stderr(), containsString("Unable to delete aProject #1 because it is still running")); } @Test public void deleteBuildsShouldSuccessEvenTheBuildIsStuckInTheQueue() throws Exception { diff --git a/test/src/test/java/hudson/logging/LogRecorderManagerTest.java b/test/src/test/java/hudson/logging/LogRecorderManagerTest.java index 608e7eb0b464f..101729038a332 100644 --- a/test/src/test/java/hudson/logging/LogRecorderManagerTest.java +++ b/test/src/test/java/hudson/logging/LogRecorderManagerTest.java @@ -229,7 +229,7 @@ public static class DeletingLogRecorderListener extends SaveableListener { private static boolean recordDeletion; @Override - public void onChange(Saveable o, XmlFile file) { + public void onDeleted(Saveable o, XmlFile file) { if (o instanceof LogRecorder && "dummy".equals(((LogRecorder) o).getName())) { if (!file.exists()) { recordDeletion = true; diff --git a/test/src/test/java/hudson/model/AbstractItemTest.java b/test/src/test/java/hudson/model/AbstractItemTest.java index 165c39a96e6e9..8e2a5036b14a2 100644 --- a/test/src/test/java/hudson/model/AbstractItemTest.java +++ b/test/src/test/java/hudson/model/AbstractItemTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import hudson.ExtensionList; import hudson.XmlFile; @@ -33,6 +34,7 @@ import org.jvnet.hudson.test.MockAuthorizationStrategy; import org.jvnet.hudson.test.SleepBuilder; import org.jvnet.hudson.test.TestExtension; +import org.springframework.security.core.Authentication; public class AbstractItemTest { @@ -45,12 +47,21 @@ public class AbstractItemTest { @Test public void reload() throws Exception { Jenkins jenkins = j.jenkins; - FreeStyleProject p = jenkins.createProject(FreeStyleProject.class, "foo"); - p.setDescription("Hello World"); + jenkins.setSecurityRealm(j.createDummySecurityRealm()); + MockAuthorizationStrategy mas = new MockAuthorizationStrategy(); + mas.grant(Item.CONFIGURE).everywhere().to("alice", "bob"); + mas.grant(Item.READ).everywhere().to("alice"); - FreeStyleBuild b = j.buildAndAssertSuccess(p); - b.setDescription("This is my build"); + FreeStyleProject p; + FreeStyleBuild b; + var alice = User.getById("alice", true); + try (ACLContext ignored = ACL.as(alice)) { + p = jenkins.createProject(FreeStyleProject.class, "foo"); + p.setDescription("Hello World"); + b = j.buildAndAssertSuccess(p); + b.setDescription("This is my build"); + } // update on disk representation Path path = p.getConfigFile().getFile().toPath(); Files.writeString(path, Files.readString(path, StandardCharsets.UTF_8).replaceAll("Hello World", "Good Evening"), StandardCharsets.UTF_8); @@ -61,15 +72,25 @@ public void reload() throws Exception { // reload away p.doReload(); - assertFalse(SaveableListener.class.getSimpleName() + " should not have been called", testSaveableListener.wasCalled()); - - + assertFalse(SaveableListener.class.getSimpleName() + " should not have been called", testSaveableListener.isChangeCalled()); 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 + + try (var ignored = ACL.as(alice)) { + p.setDescription("This is Alice's project"); + } + assertTrue(SaveableListener.class.getSimpleName() + " should have been called", testSaveableListener.isChangeCalled()); + assertThat(testSaveableListener.getChangeUser(), equalTo(alice.impersonate2())); + + try (var ignored = ACL.as(alice)) { + p.delete(); + } + assertTrue(SaveableListener.class.getSimpleName() + " should have been called", testSaveableListener.isDeleteCalled()); + assertThat(testSaveableListener.getDeleteUser(), equalTo(alice.impersonate2())); } @Test @@ -158,20 +179,45 @@ private String getPath(URL u) { public static class TestSaveableListener extends SaveableListener { private Saveable saveable; - private boolean called; + private boolean changeCalled; + private Authentication changeUser; + + private boolean deleteCalled; + private Authentication deleteUser; private void setSaveable(Saveable saveable) { this.saveable = saveable; } - public boolean wasCalled() { - return called; + public boolean isChangeCalled() { + return changeCalled; + } + + public Authentication getChangeUser() { + return changeUser; + } + + public boolean isDeleteCalled() { + return deleteCalled; + } + + public Authentication getDeleteUser() { + return deleteUser; } @Override public void onChange(Saveable o, XmlFile file) { if (o == saveable) { - this.called = true; + changeCalled = true; + changeUser = Jenkins.getAuthentication2(); + } + } + + @Override + public void onDeleted(Saveable o, XmlFile file) { + if (o == saveable) { + deleteCalled = true; + deleteUser = Jenkins.getAuthentication2(); } } } diff --git a/test/src/test/java/hudson/model/ComputerSetTest.java b/test/src/test/java/hudson/model/ComputerSetTest.java index f40cf3e00c96a..e137cbfec823e 100644 --- a/test/src/test/java/hudson/model/ComputerSetTest.java +++ b/test/src/test/java/hudson/model/ComputerSetTest.java @@ -26,7 +26,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; @@ -91,10 +90,10 @@ public void nodeOfflineCli() throws Exception { @Test public void getComputerNames() throws Exception { assertThat(ComputerSet.getComputerNames(), is(empty())); - j.createSlave("aNode", "", null); - assertThat(ComputerSet.getComputerNames(), contains("aNode")); j.createSlave("anAnotherNode", "", null); - assertThat(ComputerSet.getComputerNames(), containsInAnyOrder("aNode", "anAnotherNode")); + assertThat(ComputerSet.getComputerNames(), contains("anAnotherNode")); + j.createSlave("aNode", "", null); + assertThat(ComputerSet.getComputerNames(), contains("aNode", "anAnotherNode")); } @Test diff --git a/test/src/test/java/hudson/model/RunTest.java b/test/src/test/java/hudson/model/RunTest.java index b6a145857020b..4df895309abb9 100644 --- a/test/src/test/java/hudson/model/RunTest.java +++ b/test/src/test/java/hudson/model/RunTest.java @@ -30,10 +30,14 @@ import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import hudson.ExtensionList; import hudson.FilePath; import hudson.Launcher; +import hudson.XmlFile; +import hudson.model.listeners.SaveableListener; import hudson.tasks.ArtifactArchiver; import hudson.tasks.BuildTrigger; import hudson.tasks.Builder; @@ -59,6 +63,7 @@ import org.junit.experimental.categories.Category; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.SleepBuilder; import org.jvnet.hudson.test.SmokeTest; import org.jvnet.hudson.test.TestExtension; import org.kohsuke.stapler.DataBoundConstructor; @@ -112,6 +117,29 @@ public class RunTest { FreeStyleBuild b = j.buildAndAssertSuccess(p); b.delete(); assertTrue(Mgr.deleted.get()); + assertTrue(ExtensionList.lookupSingleton(SaveableListenerImpl.class).deleted); + } + + @TestExtension("deleteArtifactsCustom") + public static class SaveableListenerImpl extends SaveableListener { + boolean deleted; + + @Override + public void onDeleted(Saveable o, XmlFile file) { + deleted = true; + } + } + + @Issue("JENKINS-73835") + @Test public void buildsMayNotBeDeletedWhileRunning() throws Exception { + var p = j.createFreeStyleProject(); + p.getBuildersList().add(new SleepBuilder(999999)); + var b = p.scheduleBuild2(0).waitForStart(); + var ex = assertThrows(IOException.class, () -> b.delete()); + assertThat(ex.getMessage(), containsString("Unable to delete " + b + " because it is still running")); + b.getExecutor().interrupt(); + j.waitForCompletion(b); + b.delete(); // Works fine. } @Issue("SECURITY-1902") diff --git a/test/src/test/java/hudson/model/UpdateCenterMigrationTest.java b/test/src/test/java/hudson/model/UpdateCenterMigrationTest.java new file mode 100644 index 0000000000000..6d002dbcad911 --- /dev/null +++ b/test/src/test/java/hudson/model/UpdateCenterMigrationTest.java @@ -0,0 +1,35 @@ +package hudson.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +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.recipes.LocalData; + +public class UpdateCenterMigrationTest { + + @Rule + public JenkinsRule j = new JenkinsRule() { + @Override + protected void configureUpdateCenter() { + // Avoid reverse proxy + DownloadService.neverUpdate = true; + UpdateSite.neverUpdate = true; + } + }; + + @Issue("JENKINS-73760") + @LocalData + @Test + public void updateCenterMigration() { + UpdateSite site = j.jenkins.getUpdateCenter().getSites().stream() + .filter(s -> UpdateCenter.PREDEFINED_UPDATE_SITE_ID.equals(s.getId())) + .findFirst() + .orElseThrow(); + assertFalse(site.isLegacyDefault()); + assertEquals(j.jenkins.getUpdateCenter().getDefaultBaseUrl() + "update-center.json", site.getUrl()); + } +} diff --git a/test/src/test/java/hudson/model/UpdateSiteTest.java b/test/src/test/java/hudson/model/UpdateSiteTest.java index 629c7c7f46e8a..293cca2de7684 100644 --- a/test/src/test/java/hudson/model/UpdateSiteTest.java +++ b/test/src/test/java/hudson/model/UpdateSiteTest.java @@ -72,6 +72,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; public class UpdateSiteTest { @@ -205,6 +206,19 @@ public void shutdownWebserver() throws Exception { assertNotEquals("plugin data is present", Collections.emptyMap(), site.getData().plugins); } + @Issue("JENKINS-73760") + @Test + public void isLegacyDefault() { + assertFalse("isLegacyDefault should be false with null id", new UpdateSite(null, "url").isLegacyDefault()); + assertFalse( + "isLegacyDefault should be false when id is not default and url is http://updates.jenkins-ci.org/", + new UpdateSite("dummy", "http://updates.jenkins-ci.org/").isLegacyDefault()); + assertTrue( + "isLegacyDefault should be true when id is default and url is http://updates.jenkins-ci.org/", + new UpdateSite(UpdateCenter.PREDEFINED_UPDATE_SITE_ID, "http://updates.jenkins-ci.org/").isLegacyDefault()); + assertFalse("isLegacyDefault should be false with null url", new UpdateSite(null, null).isLegacyDefault()); + } + @Test public void getAvailables() throws Exception { UpdateSite site = getUpdateSite("/plugins/available-update-center.json"); List available = site.getAvailables(); diff --git a/test/src/test/java/hudson/tasks/LogRotatorTest.java b/test/src/test/java/hudson/tasks/LogRotatorTest.java index b72e837dbd86a..118a860147a1e 100644 --- a/test/src/test/java/hudson/tasks/LogRotatorTest.java +++ b/test/src/test/java/hudson/tasks/LogRotatorTest.java @@ -50,8 +50,10 @@ import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; +import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.jvnet.hudson.test.BuildWatcher; import org.jvnet.hudson.test.FailureBuilder; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; @@ -62,6 +64,9 @@ */ public class LogRotatorTest { + @ClassRule + public static BuildWatcher watcher = new BuildWatcher(); + @Rule public JenkinsRule j = new JenkinsRule(); @@ -96,6 +101,17 @@ public void successVsFailureWithRemoveLastBuild() throws Exception { assertEquals(2, numberOf(project.getLastFailedBuild())); } + @Test + public void ableToDeleteCurrentBuild() throws Exception { + var p = j.createFreeStyleProject(); + // Keep 0 builds, i.e. immediately delete builds as they complete. + LogRotator logRotator = new LogRotator(-1, 0, -1, -1); + logRotator.setRemoveLastBuild(true); + p.setBuildDiscarder(logRotator); + j.buildAndAssertStatus(Result.SUCCESS, p); + assertNull(p.getBuildByNumber(1)); + } + @Test @Issue("JENKINS-2417") public void stableVsUnstable() throws Exception { diff --git a/test/src/test/java/jenkins/model/NodesTest.java b/test/src/test/java/jenkins/model/NodesTest.java index 275dd9a3e5fbc..c7207b9977c56 100644 --- a/test/src/test/java/jenkins/model/NodesTest.java +++ b/test/src/test/java/jenkins/model/NodesTest.java @@ -37,10 +37,13 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.ExtensionList; +import hudson.XmlFile; import hudson.model.Descriptor; import hudson.model.Failure; import hudson.model.Node; +import hudson.model.Saveable; import hudson.model.Slave; +import hudson.model.listeners.SaveableListener; import hudson.slaves.ComputerLauncher; import hudson.slaves.DumbSlave; import java.io.IOException; @@ -99,6 +102,10 @@ public void addNodeShouldReplaceExistingNode() throws Exception { assertEquals(0, l.deleted); assertEquals(1, l.updated); assertEquals(1, l.created); + var saveableListener = ExtensionList.lookupSingleton(SaveableListenerImpl.class); + assertEquals(0, saveableListener.deleted); + r.jenkins.removeNode(newNode); + assertEquals(1, saveableListener.deleted); } @TestExtension("addNodeShouldReplaceExistingNode") @@ -122,6 +129,16 @@ protected void onCreated(Node node) { } } + @TestExtension("addNodeShouldReplaceExistingNode") + public static final class SaveableListenerImpl extends SaveableListener { + int deleted; + + @Override + public void onDeleted(Saveable o, XmlFile file) { + deleted++; + } + } + @Test @Issue("JENKINS-56403") public void replaceNodeShouldRemoveOldNode() throws Exception { diff --git a/test/src/test/java/jenkins/security/RedactSecretJsonInErrorMessageSanitizerHtmlTest.java b/test/src/test/java/jenkins/security/RedactSecretJsonInErrorMessageSanitizerHtmlTest.java index 0fe9b8865d167..b1435951f0e2d 100644 --- a/test/src/test/java/jenkins/security/RedactSecretJsonInErrorMessageSanitizerHtmlTest.java +++ b/test/src/test/java/jenkins/security/RedactSecretJsonInErrorMessageSanitizerHtmlTest.java @@ -31,19 +31,24 @@ import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.not; +import hudson.ExtensionList; import hudson.model.Describable; import hudson.model.Descriptor; import hudson.model.RootAction; import hudson.util.Secret; +import jakarta.servlet.ServletException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.logging.Level; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.htmlunit.Page; +import org.htmlunit.html.HtmlElement; +import org.htmlunit.html.HtmlElementUtil; import org.htmlunit.html.HtmlForm; import org.htmlunit.html.HtmlInput; import org.htmlunit.html.HtmlPage; +import org.htmlunit.html.HtmlTextArea; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.Issue; @@ -223,6 +228,81 @@ public void checkSanitizationIsApplied_inStapler() throws Exception { )); } + @Test + @Issue("SECURITY-3451") + public void secretTextAreaSubmissionRedaction() throws Exception { + final TestSecretTextarea action = ExtensionList.lookupSingleton(TestSecretTextarea.class); + + logging.record("", Level.WARNING).capture(100); + + final String secretValue = "s3cr3t"; + + try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) { + + HtmlPage page = wc.goTo("test"); + + final HtmlForm form = page.getFormByName("config"); + final HtmlElement button = form.getElementsByTagName("button").stream().filter(b -> HtmlElementUtil.hasClassName(b, "secret-update-btn")).findFirst().orElseThrow(); + HtmlElementUtil.click(button); + + ((HtmlTextArea) form.getElementsByTagName("textarea").stream().filter(field -> HtmlElementUtil.hasClassName(field, "secretTextarea-redact")).findFirst().orElseThrow()).setText(secretValue); + + Page formSubmitPage = j.submit(form); + assertThat(formSubmitPage.getWebResponse().getContentAsString(), allOf( + containsString(RedactSecretJsonInErrorMessageSanitizer.REDACT_VALUE), + not(containsString(secretValue)) + )); + } + + // check the system log also + Throwable thrown = logging.getRecords().stream().filter(r -> r.getMessage().contains("Error while serving")).findAny().get().getThrown(); + // the exception from RequestImpl + assertThat(thrown.getCause().getMessage(), allOf( + containsString(RedactSecretJsonInErrorMessageSanitizer.REDACT_VALUE), + not(containsString(secretValue)) + )); + + StringWriter buffer = new StringWriter(); + thrown.printStackTrace(new PrintWriter(buffer)); + String fullStack = buffer.getBuffer().toString(); + assertThat(fullStack, allOf( + containsString(RedactSecretJsonInErrorMessageSanitizer.REDACT_VALUE), + not(containsString(secretValue)) + )); + } + + public static class SecretTextareaDescribable { + @DataBoundConstructor + public SecretTextareaDescribable(Secret secret) { + throw new IllegalArgumentException("there is something wrong with the secret"); + } + } + + @TestExtension("secretTextAreaSubmissionRedaction") + public static class TestSecretTextarea implements RootAction { + + public JSONObject lastJsonReceived; + + public void doSubmitTest(StaplerRequest2 req) throws ServletException { + req.bindJSON(SecretTextareaDescribable.class, req.getSubmittedForm()); + } + + @Override + public String getIconFileName() { + return null; + } + + @Override + public String getDisplayName() { + return null; + } + + @Override + public String getUrlName() { + return "test"; + } + } + public static class TestDescribable implements Describable { @DataBoundConstructor diff --git a/test/src/test/java/jenkins/security/Security3448Test.java b/test/src/test/java/jenkins/security/Security3448Test.java new file mode 100644 index 0000000000000..d2a4695317cf4 --- /dev/null +++ b/test/src/test/java/jenkins/security/Security3448Test.java @@ -0,0 +1,171 @@ +package jenkins.security; + +import static hudson.cli.CLICommandInvoker.Matcher.failedWith; +import static hudson.cli.CLICommandInvoker.Matcher.succeededSilently; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertTrue; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.cli.CLICommand; +import hudson.cli.CLICommandInvoker; +import hudson.cli.CreateJobCommand; +import hudson.model.Build; +import hudson.model.Descriptor; +import hudson.model.FreeStyleProject; +import hudson.model.ItemGroup; +import hudson.model.Project; +import hudson.model.TopLevelItem; +import hudson.model.TopLevelItemDescriptor; +import hudson.security.ACL; +import hudson.security.AuthorizationStrategy; +import hudson.security.Permission; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import jenkins.model.Jenkins; +import org.hamcrest.Matchers; +import org.htmlunit.HttpMethod; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; +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.springframework.security.core.Authentication; + +public class Security3448Test { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Issue("SECURITY-3448") + @Test + public void jobCreationFromCLI() { + CLICommand cmd = new CreateJobCommand(); + CLICommandInvoker invoker = new CLICommandInvoker(j, cmd); + + j.jenkins.setAuthorizationStrategy(AuthorizationStrategy.UNSECURED); + + assertThat(j.jenkins.getItems(), Matchers.hasSize(0)); + assertThat(invoker.withStdin(new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8))).invokeWithArgs("job1"), succeededSilently()); + assertThat(j.jenkins.getItems(), Matchers.hasSize(1)); + + assertThat(invoker.withStdin(new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8))).invokeWithArgs("job2"), failedWith(6)); + assertThat(j.jenkins.getItems(), Matchers.hasSize(1)); + + j.jenkins.setAuthorizationStrategy(new UnsecuredNoFreestyleAuthorizationStrategy()); + + assertThat(invoker.withStdin(new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8))).invokeWithArgs("job2"), failedWith(6)); + assertThat(j.jenkins.getItems(), Matchers.hasSize(1)); + } + + @Test + @Issue("SECURITY-3448") + public void jobCreationFromREST() throws Exception { + j.jenkins.setCrumbIssuer(null); + + try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) { + String doCreateItem = j.getURL().toString() + "createItem?name="; + URL job1 = new URL(doCreateItem + "job1"); + WebRequest req = new WebRequest(job1, HttpMethod.POST); + req.setAdditionalHeader("Content-Type", "application/xml"); + req.setRequestBody(""); + j.jenkins.setAuthorizationStrategy(AuthorizationStrategy.UNSECURED); + + assertThat(j.jenkins.getItems(), Matchers.hasSize(0)); + wc.getPage(req); + assertThat(j.jenkins.getItems(), Matchers.hasSize(1)); + + URL job2 = new URL(doCreateItem + "job2"); + req.setUrl(job2); + req.setRequestBody(""); + + WebResponse rspJob2 = wc.getPage(req).getWebResponse(); + assertTrue(rspJob2.getContentAsString().contains("Security3448Test$NotApplicableProject is not applicable in")); + assertThat(j.jenkins.getItems(), Matchers.hasSize(1)); + + URL job3 = new URL(doCreateItem + "job3"); + req.setUrl(job3); + req.setRequestBody(""); + j.jenkins.setAuthorizationStrategy(new UnsecuredNoFreestyleAuthorizationStrategy()); + + WebResponse rspJob3 = wc.getPage(req).getWebResponse(); + assertTrue(rspJob3.getContentAsString().contains("does not have required permissions to create hudson.model.FreeStyleProject")); + assertThat(j.jenkins.getItems(), Matchers.hasSize(1)); + } + } + + public static class UnsecuredNoFreestyleAuthorizationStrategy extends AuthorizationStrategy { + + @DataBoundConstructor + public UnsecuredNoFreestyleAuthorizationStrategy() {} + + @Override + public ACL getRootACL() { + return new ACL() { + + @Override + public boolean hasPermission2(Authentication a, Permission permission) { + return true; + } + + @Override + public boolean hasCreatePermission2( + @NonNull Authentication a, @NonNull ItemGroup c, @NonNull TopLevelItemDescriptor d) { + return d.clazz != FreeStyleProject.class; + } + }; + } + + @Override + public Collection getGroups() { + return Collections.emptyList(); + } + + @TestExtension + public static class DescriptorImpl extends Descriptor {} + } + + public static class NotApplicableProject extends Project implements TopLevelItem { + + NotApplicableProject(ItemGroup parent, String name) { + super(parent, name); + } + + @Override + protected Class getBuildClass() { + return NotApplicableBuild.class; + } + + @Override + public TopLevelItemDescriptor getDescriptor() { + return (NotApplicableProject.DescriptorImpl) Jenkins.get().getDescriptorOrDie(getClass()); + } + + @TestExtension + public static class DescriptorImpl extends AbstractProjectDescriptor { + + @Override + public boolean isApplicableIn(ItemGroup parent) { + return false; + } + + @Override + public TopLevelItem newInstance(ItemGroup parent, String name) { + return new NotApplicableProject(parent, name); + } + } + } + + public static class NotApplicableBuild extends Build { + + protected NotApplicableBuild(NotApplicableProject project) throws IOException { + super(project); + } + } +} diff --git a/test/src/test/java/lib/form/PasswordTest.java b/test/src/test/java/lib/form/PasswordTest.java index facb1d7418df5..526ef1586ae9c 100644 --- a/test/src/test/java/lib/form/PasswordTest.java +++ b/test/src/test/java/lib/form/PasswordTest.java @@ -67,6 +67,7 @@ import jenkins.model.GlobalConfiguration; import jenkins.model.Jenkins; import jenkins.model.TransientActionFactory; +import jenkins.security.ExtendedReadSecretRedaction; import jenkins.tasks.SimpleBuildStep; import org.htmlunit.Page; import org.htmlunit.html.DomElement; @@ -76,6 +77,7 @@ import org.htmlunit.html.HtmlTextInput; 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.MockAuthorizationStrategy; @@ -124,6 +126,7 @@ public String getUrlName() { @Issue({"SECURITY-266", "SECURITY-304"}) @Test + @For(ExtendedReadSecretRedaction.class) public void testExposedCiphertext() throws Exception { boolean saveEnabled = Item.EXTENDED_READ.getEnabled(); Item.EXTENDED_READ.setEnabled(true); diff --git a/test/src/test/resources/hudson/model/UpdateCenterMigrationTest/updateCenterMigration/hudson.model.UpdateCenter.xml b/test/src/test/resources/hudson/model/UpdateCenterMigrationTest/updateCenterMigration/hudson.model.UpdateCenter.xml new file mode 100644 index 0000000000000..4f317e7836476 --- /dev/null +++ b/test/src/test/resources/hudson/model/UpdateCenterMigrationTest/updateCenterMigration/hudson.model.UpdateCenter.xml @@ -0,0 +1,7 @@ + + + + default + http://updates.jenkins-ci.org/update-center.json + + \ No newline at end of file diff --git a/test/src/test/resources/jenkins/security/RedactSecretJsonInErrorMessageSanitizerHtmlTest/TestSecretTextarea/index.jelly b/test/src/test/resources/jenkins/security/RedactSecretJsonInErrorMessageSanitizerHtmlTest/TestSecretTextarea/index.jelly new file mode 100644 index 0000000000000..05b146af865b9 --- /dev/null +++ b/test/src/test/resources/jenkins/security/RedactSecretJsonInErrorMessageSanitizerHtmlTest/TestSecretTextarea/index.jelly @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/war/.gitignore b/war/.gitignore deleted file mode 100644 index 045972f591e6b..0000000000000 --- a/war/.gitignore +++ /dev/null @@ -1,20 +0,0 @@ -work -/rebel.xml -junit.xml - -# Yarn -# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored -.pnp.* -.yarn/* -.yarnrc.yml -!.yarn/patches -!.yarn/plugins -!.yarn/sdks -!.yarn/versions - -# Node -node/ -node_modules/ - -# Generated JavaScript Bundles -jsbundles diff --git a/war/pom.xml b/war/pom.xml index f256a95017cae..770d20d1047c2 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -46,7 +46,7 @@ THE SOFTWARE. localhost 8080 - 2.13.2-125.v200281b_61d59 + 2.14.0-133.vcc091215a_358 3107.v665000b_51092 @@ -148,13 +148,21 @@ THE SOFTWARE. 11 + com.infradna.tool:bridge-method-annotation + org.jenkins-ci:annotation-indexer org.jenkins-ci:commons-jelly + org.jenkins-ci:crypto-util org.jenkins-ci.main:cli org.jenkins-ci.main:jenkins-core org.jenkins-ci.main:websocket-jetty12-ee9 org.jenkins-ci.main:websocket-spi + org.jenkins-ci:memory-monitor + org.jenkins-ci:symbol-annotation + org.jenkins-ci:task-reactor + org.jenkins-ci:version-number org.jvnet.hudson:commons-jelly-tags-define org.jvnet.winp:winp + org.kohsuke:access-modifier-annotation org.kohsuke.stapler:stapler org.kohsuke.stapler:stapler-groovy org.kohsuke.stapler:stapler-jelly @@ -271,7 +279,7 @@ THE SOFTWARE. org.jenkins-ci.plugins mailer - 472.vf7c289a_4b_420 + 488.v0c9639c1a_eb_3 hpi @@ -292,7 +300,7 @@ THE SOFTWARE. org.jenkins-ci.plugins matrix-project - 832.va_66e270d2946 + 839.vff91cd7e3a_b_2 hpi @@ -306,7 +314,7 @@ THE SOFTWARE. org.jenkins-ci.plugins junit - 1300.v03d9d8a_cf1fb_ + 1304.vc85a_b_ca_96613 hpi @@ -410,7 +418,7 @@ THE SOFTWARE. org.jenkins-ci.plugins scm-api - 696.v778d637b_a_762 + 698.v8e3b_c788f0a_6 hpi @@ -480,7 +488,7 @@ THE SOFTWARE. org.jenkins-ci.modules instance-identity - 185.v303dc7c645f9 + 201.vd2a_b_5a_468a_a_6 hpi @@ -494,7 +502,7 @@ THE SOFTWARE. io.jenkins.plugins asm-api - 9.7-33.v4d23ef79fcc8 + 9.7.1-95.v9f552033802a_ hpi @@ -580,7 +588,7 @@ THE SOFTWARE. io.jenkins.lib support-log-formatter - 1.2 + 1.3 ${project.build.directory} @@ -637,7 +645,7 @@ THE SOFTWARE. org.eclipse.jetty.ee9 jetty-ee9-maven-plugin - 12.0.13 + 12.0.14