diff --git a/bom/pom.xml b/bom/pom.xml index 537d08f11a36..7f0162f63cf5 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -64,7 +64,7 @@ THE SOFTWARE. org.springframework.security spring-security-bom - 5.8.5 + 5.8.6 pom import @@ -189,7 +189,7 @@ THE SOFTWARE. org.apache.ant ant - 1.10.13 + 1.10.14 org.apache.commons diff --git a/core/src/main/java/hudson/console/PlainTextConsoleOutputStream.java b/core/src/main/java/hudson/console/PlainTextConsoleOutputStream.java index f3b5bd8adf7e..70bc85b7b110 100644 --- a/core/src/main/java/hudson/console/PlainTextConsoleOutputStream.java +++ b/core/src/main/java/hudson/console/PlainTextConsoleOutputStream.java @@ -28,6 +28,10 @@ import java.io.DataInputStream; import java.io.IOException; import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.commons.lang.StringEscapeUtils; /** * Filters out console notes. @@ -36,6 +40,8 @@ */ public class PlainTextConsoleOutputStream extends LineTransformationOutputStream.Delegating { + private static final Logger LOGGER = Logger.getLogger(PlainTextConsoleOutputStream.class.getName()); + /** * */ @@ -64,7 +70,11 @@ protected void eol(byte[] in, int sz) throws IOException { int rest = sz - next; ByteArrayInputStream b = new ByteArrayInputStream(in, next, rest); - ConsoleNote.skip(new DataInputStream(b)); + try { + ConsoleNote.skip(new DataInputStream(b)); + } catch (IOException x) { + LOGGER.log(Level.FINE, "Failed to skip annotation from \"" + StringEscapeUtils.escapeJava(new String(in, next, rest, Charset.defaultCharset())) + "\"", x); + } int bytesUsed = rest - b.available(); // bytes consumed by annotations written += bytesUsed; diff --git a/core/src/main/java/hudson/model/Label.java b/core/src/main/java/hudson/model/Label.java index 43604d408aac..b0c32f212a09 100644 --- a/core/src/main/java/hudson/model/Label.java +++ b/core/src/main/java/hudson/model/Label.java @@ -592,10 +592,16 @@ public static Set parse(@CheckForNull String labels) { final Set r = new TreeSet<>(); labels = fixNull(labels); if (labels.length() > 0) { - final QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(labels); - while (tokenizer.hasMoreTokens()) - r.add(Jenkins.get().getLabelAtom(tokenizer.nextToken())); + Jenkins j = Jenkins.get(); + LabelAtom labelAtom = j.tryGetLabelAtom(labels); + if (labelAtom == null) { + final QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(labels); + while (tokenizer.hasMoreTokens()) + r.add(j.getLabelAtom(tokenizer.nextToken())); + } else { + r.add(labelAtom); } + } return r; } diff --git a/core/src/main/java/hudson/model/Node.java b/core/src/main/java/hudson/model/Node.java index 519d89c10970..2d34e6e90ade 100644 --- a/core/src/main/java/hudson/model/Node.java +++ b/core/src/main/java/hudson/model/Node.java @@ -68,6 +68,7 @@ import net.sf.json.JSONObject; import org.jvnet.localizer.Localizable; import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.accmod.restrictions.ProtectedExternally; import org.kohsuke.stapler.BindInterceptor; import org.kohsuke.stapler.Stapler; @@ -298,6 +299,17 @@ public OfflineCause getTemporaryOfflineCause() { public TagCloud getLabelCloud() { return new TagCloud<>(getAssignedLabels(), Label::getTiedJobCount); } + + /** + * @return An immutable set of LabelAtom associated with the current node label. + */ + @NonNull + @Restricted(NoExternalUse.class) + protected Set getLabelAtomSet() { + // Default implementation doesn't cache, since we can't hook on label updates. + return Collections.unmodifiableSet(Label.parse(getLabelString())); + } + /** * Returns the possibly empty set of labels that are assigned to this node, * including the automatic {@link #getSelfLabel() self label}, manually @@ -305,13 +317,13 @@ public TagCloud getLabelCloud() { * {@link LabelFinder} extension point. * * This method has a side effect of updating the hudson-wide set of labels - * and should be called after events that will change that - e.g. a agent + * and should be called after events that will change that - e.g. an agent * connecting. */ @Exported public Set getAssignedLabels() { - Set r = Label.parse(getLabelString()); + Set r = new HashSet<>(getLabelAtomSet()); r.add(getSelfLabel()); r.addAll(getDynamicLabels()); return Collections.unmodifiableSet(r); diff --git a/core/src/main/java/hudson/model/Slave.java b/core/src/main/java/hudson/model/Slave.java index 22eb63cd2fc6..da5195a963cf 100644 --- a/core/src/main/java/hudson/model/Slave.java +++ b/core/src/main/java/hudson/model/Slave.java @@ -36,6 +36,7 @@ import hudson.Util; import hudson.cli.CLI; import hudson.model.Descriptor.FormException; +import hudson.model.labels.LabelAtom; import hudson.remoting.Callable; import hudson.remoting.Channel; import hudson.remoting.Which; @@ -60,6 +61,7 @@ import java.net.URLConnection; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.jar.JarFile; @@ -179,6 +181,7 @@ protected Slave(@NonNull String name, String remoteFS, ComputerLauncher launcher this.name = name; this.remoteFS = remoteFS; this.launcher = launcher; + this.labelAtomSet = Collections.unmodifiableSet(Label.parse(label)); } /** @@ -193,7 +196,7 @@ protected Slave(@NonNull String name, String nodeDescription, String remoteFS, i this.numExecutors = numExecutors; this.mode = mode; this.remoteFS = Util.fixNull(remoteFS).trim(); - this.label = Util.fixNull(labelString).trim(); + this.labelAtomSet = Collections.unmodifiableSet(Label.parse(labelString)); this.launcher = launcher; this.retentionStrategy = retentionStrategy; getAssignedLabels(); // compute labels now @@ -328,11 +331,24 @@ public String getLabelString() { @Override @DataBoundSetter public void setLabelString(String labelString) throws IOException { - this.label = Util.fixNull(labelString).trim(); + _setLabelString(labelString); // Compute labels now. getAssignedLabels(); } + private void _setLabelString(String labelString) { + this.label = Util.fixNull(labelString).trim(); + this.labelAtomSet = Collections.unmodifiableSet(Label.parse(label)); + } + + @NonNull + private transient Set labelAtomSet; + + @Override + protected Set getLabelAtomSet() { + return labelAtomSet; + } + @Override public Callable getClockDifferenceCallable() { return new GetClockDifference1(); @@ -574,6 +590,7 @@ public int hashCode() { protected Object readResolve() { if (nodeProperties == null) nodeProperties = new DescribableList<>(this); + _setLabelString(label); return this; } diff --git a/core/src/main/java/hudson/security/SecurityRealm.java b/core/src/main/java/hudson/security/SecurityRealm.java index ca62a4d3fe0e..a134755fe95e 100644 --- a/core/src/main/java/hudson/security/SecurityRealm.java +++ b/core/src/main/java/hudson/security/SecurityRealm.java @@ -647,15 +647,27 @@ public static String getFrom() { from = request.getParameter("from"); } + // On the 404 error page, use the session attribute it sets + if (request != null && request.getRequestURI().equals(request.getContextPath() + "/404")) { + final HttpSession session = request.getSession(false); + if (session != null) { + final Object attribute = session.getAttribute("from"); + if (attribute != null) { + from = attribute.toString(); + } + } + } + // If entry point was not found, try to deduce it from the request URI - // except pages related to login process + // except pages related to login process and the 404 error page if (from == null && request != null && request.getRequestURI() != null - && !request.getRequestURI().equals("/loginError") - && !request.getRequestURI().equals("/login")) { - - from = request.getRequestURI(); + // The custom login page makes the next two lines obsolete, but safer to have them. + && !request.getRequestURI().equals(request.getContextPath() + "/loginError") + && !request.getRequestURI().equals(request.getContextPath() + "/login") + && !request.getRequestURI().equals(request.getContextPath() + "/404")) { + from = request.getRequestURI(); } // If deduced entry point isn't deduced yet or the content is a blank value diff --git a/core/src/main/java/jenkins/ErrorAttributeFilter.java b/core/src/main/java/jenkins/ErrorAttributeFilter.java new file mode 100644 index 000000000000..9163e409f61d --- /dev/null +++ b/core/src/main/java/jenkins/ErrorAttributeFilter.java @@ -0,0 +1,30 @@ +package jenkins; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import jenkins.model.Jenkins; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.springframework.security.core.Authentication; + +/** + * Record the current user authentication for later impersonation if the response is 404 Not Found. + * + * @see Jenkins#generateNotFoundResponse(org.kohsuke.stapler.StaplerRequest, org.kohsuke.stapler.StaplerResponse) + */ +@Restricted(NoExternalUse.class) +public class ErrorAttributeFilter implements Filter { + + public static final String USER_ATTRIBUTE = "jenkins.ErrorAttributeFilter.user"; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + final Authentication authentication = Jenkins.getAuthentication2(); + servletRequest.setAttribute(USER_ATTRIBUTE, authentication); + filterChain.doFilter(servletRequest, servletResponse); + } +} diff --git a/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java b/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java index eb9685f54ba0..cc1d5214e0ae 100644 --- a/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java +++ b/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java @@ -28,6 +28,7 @@ import hudson.Extension; import hudson.diagnosis.ReverseProxySetupMonitor; import hudson.model.AdministrativeMonitor; +import hudson.model.ManageJenkinsAction; import hudson.model.PageDecorator; import hudson.util.HudsonIsLoading; import hudson.util.HudsonIsRestarting; @@ -53,9 +54,6 @@ public class AdministrativeMonitorsDecorator extends PageDecorator { private final Collection ignoredJenkinsRestOfUrls = new ArrayList<>(); public AdministrativeMonitorsDecorator() { - // redundant - ignoredJenkinsRestOfUrls.add("manage"); - // otherwise this would be added to every internal context menu building request ignoredJenkinsRestOfUrls.add("contextMenu"); @@ -165,6 +163,11 @@ public Collection getMonitorsToDisplay() { return null; } + // Don't show on Manage Jenkins + if (o instanceof ManageJenkinsAction) { + return null; + } + // don't show for some URLs served directly by Jenkins if (o instanceof Jenkins) { String url = a.getRestOfUrl(); diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 9bf94cf9389a..29477e36d911 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -262,6 +262,7 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import jenkins.AgentProtocol; +import jenkins.ErrorAttributeFilter; import jenkins.ExtensionComponentSet; import jenkins.ExtensionRefreshException; import jenkins.InitReactorRunner; @@ -275,6 +276,7 @@ import jenkins.security.ConfidentialStore; import jenkins.security.MasterToSlaveCallable; import jenkins.security.RedactSecretJsonInErrorMessageSanitizer; +import jenkins.security.ResourceDomainConfiguration; import jenkins.security.SecurityListener; import jenkins.security.stapler.DoActionFilter; import jenkins.security.stapler.StaplerDispatchValidator; @@ -930,7 +932,7 @@ protected Jenkins(File root, ServletContext context, PluginManager pluginManager Trigger.timer = new java.util.Timer("Jenkins cron thread"); queue = new Queue(LoadBalancer.CONSISTENT_HASH); - + labelAtomSet = Collections.unmodifiableSet(Label.parse(label)); try { dependencyGraph = DependencyGraph.EMPTY; } catch (InternalError e) { @@ -1088,6 +1090,7 @@ protected Object readResolve() { /* deserializing without a value set means we need to migrate */ nodeRenameMigrationNeeded = true; } + _setLabelString(label); return this; } @@ -1468,6 +1471,14 @@ public boolean hasPeople() { } public Api getApi() { + /* Do not show "REST API" link in footer when on 404 error page */ + final StaplerRequest req = Stapler.getCurrentRequest(); + if (req != null) { + final Object attribute = req.getAttribute("javax.servlet.error.message"); + if (attribute != null) { + return null; + } + } return new Api(this); } @@ -2114,6 +2125,20 @@ public Label getLabel(String expr) { } } + /** + * Returns the label atom of the given name, only if it already exists. + * @return non-null if the label atom already exists. + */ + @Restricted(NoExternalUse.class) + public @Nullable LabelAtom tryGetLabelAtom(@NonNull String name) { + Label label = labels.get(name); + if (label instanceof LabelAtom) { + return (LabelAtom) label; + } + return null; + } + + /** * Gets all the active labels in the current system. */ @@ -2126,6 +2151,14 @@ public Set