getWidgets() {
return widgets;
}
diff --git a/core/src/main/java/jenkins/security/stapler/DoActionFilter.java b/core/src/main/java/jenkins/security/stapler/DoActionFilter.java
new file mode 100644
index 0000000000..288398bcba
--- /dev/null
+++ b/core/src/main/java/jenkins/security/stapler/DoActionFilter.java
@@ -0,0 +1,133 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2018, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package jenkins.security.stapler;
+
+import hudson.ExtensionList;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.stapler.Function;
+import org.kohsuke.stapler.FunctionList;
+import org.kohsuke.stapler.HttpResponse;
+import org.kohsuke.stapler.interceptor.InterceptorAnnotation;
+
+import javax.annotation.Nonnull;
+import java.lang.annotation.Annotation;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+@Restricted(NoExternalUse.class)
+public class DoActionFilter implements FunctionList.Filter {
+ private static final Logger LOGGER = Logger.getLogger(DoActionFilter.class.getName());
+
+ /**
+ * if a method has "do" as name (not possible in pure Java but doable in Groovy or other JVM languages)
+ * the new system does not consider it as a web method.
+ *
+ * Use @WebMethod(name="") or doIndex in such case.
+ */
+ private static final Pattern DO_METHOD_REGEX = Pattern.compile("^do[^a-z].*");
+
+ public boolean keep(@Nonnull Function m) {
+
+ if (m.getAnnotation(StaplerNotDispatchable.class) != null) {
+ return false;
+ }
+
+ if (m.getAnnotation(StaplerDispatchable.class) != null) {
+ return true;
+ }
+
+ String methodName = m.getName();
+ String signature = m.getSignature();
+
+ // check whitelist
+ ExtensionList whitelistProviders = ExtensionList.lookup(RoutingDecisionProvider.class);
+ if (whitelistProviders.size() > 0) {
+ for (RoutingDecisionProvider provider : whitelistProviders) {
+ RoutingDecisionProvider.Decision methodDecision = provider.decide(signature);
+ if (methodDecision == RoutingDecisionProvider.Decision.ACCEPTED) {
+ LOGGER.log(Level.CONFIG, "Action " + signature + " is acceptable because it is whitelisted by " + provider);
+ return true;
+ }
+ if (methodDecision == RoutingDecisionProvider.Decision.REJECTED) {
+ LOGGER.log(Level.CONFIG, "Action " + signature + " is not acceptable because it is blacklisted by " + provider);
+ return false;
+ }
+ }
+ }
+
+ if (methodName.equals("doDynamic")) {
+ // reject doDynamic because it's treated separately by Stapler.
+ return false;
+ }
+
+ for (Annotation a : m.getAnnotations()) {
+ if (WebMethodConstants.WEB_METHOD_ANNOTATION_NAMES.contains(a.annotationType().getName())) {
+ return true;
+ }
+ if (a.annotationType().getAnnotation(InterceptorAnnotation.class) != null) {
+ // This is a Stapler interceptor annotation like RequirePOST or JsonResponse
+ return true;
+ }
+ }
+
+ // there is rarely more than two annotations in a method signature
+ for (Annotation[] perParameterAnnotation : m.getParameterAnnotations()) {
+ for (Annotation annotation : perParameterAnnotation) {
+ if (WebMethodConstants.WEB_METHOD_PARAMETER_ANNOTATION_NAMES.contains(annotation.annotationType().getName())) {
+ return true;
+ }
+ }
+ }
+
+ if (!DO_METHOD_REGEX.matcher(methodName).matches()) {
+ return false;
+ }
+
+ // after the method name check to avoid allowing methods that are meant to be used by routable ones
+ // normally they should be private in such case
+ for (Class> parameterType : m.getParameterTypes()) {
+ if (WebMethodConstants.WEB_METHOD_PARAMETERS_NAMES.contains(parameterType.getName())) {
+ return true;
+ }
+ }
+
+ Class> returnType = m.getReturnType();
+ if (HttpResponse.class.isAssignableFrom(returnType)) {
+ return true;
+ }
+
+ // as HttpResponseException inherits from RuntimeException,
+ // there is no requirement for the developer to explicitly checks it.
+ Class>[] checkedExceptionTypes = m.getCheckedExceptionTypes();
+ for (Class> checkedExceptionType : checkedExceptionTypes) {
+ if (HttpResponse.class.isAssignableFrom(checkedExceptionType)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/core/src/main/java/jenkins/security/stapler/RoutingDecisionProvider.java b/core/src/main/java/jenkins/security/stapler/RoutingDecisionProvider.java
new file mode 100644
index 0000000000..c7cab91839
--- /dev/null
+++ b/core/src/main/java/jenkins/security/stapler/RoutingDecisionProvider.java
@@ -0,0 +1,38 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2018, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package jenkins.security.stapler;
+
+import hudson.ExtensionPoint;
+
+import javax.annotation.Nonnull;
+
+public abstract class RoutingDecisionProvider implements ExtensionPoint {
+ enum Decision {
+ ACCEPTED,
+ REJECTED,
+ UNKNOWN
+ }
+
+ @Nonnull public abstract Decision decide(@Nonnull String signature);
+}
diff --git a/core/src/main/java/jenkins/security/stapler/StaplerFilteredActionListener.java b/core/src/main/java/jenkins/security/stapler/StaplerFilteredActionListener.java
new file mode 100644
index 0000000000..bdc329775f
--- /dev/null
+++ b/core/src/main/java/jenkins/security/stapler/StaplerFilteredActionListener.java
@@ -0,0 +1,76 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2018, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package jenkins.security.stapler;
+
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.stapler.Function;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+import org.kohsuke.stapler.event.FilteredDoActionTriggerListener;
+import org.kohsuke.stapler.event.FilteredFieldTriggerListener;
+import org.kohsuke.stapler.event.FilteredGetterTriggerListener;
+import org.kohsuke.stapler.lang.FieldRef;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Log a warning message when a "getter" or "doAction" function that was filtered out by SECURITY-400 new rules
+ */
+@Restricted(NoExternalUse.class)
+public class StaplerFilteredActionListener implements FilteredDoActionTriggerListener, FilteredGetterTriggerListener, FilteredFieldTriggerListener {
+ private static final Logger LOGGER = Logger.getLogger(StaplerFilteredActionListener.class.getName());
+
+ private static final String LOG_MESSAGE = "New Stapler routing rules result in the URL \"{0}\" no longer being allowed. " +
+ "If you consider it safe to use, add the following to the whitelist: \"{1}\". " +
+ "Learn more: https://jenkins.io/redirect/stapler-routing";
+
+ @Override
+ public boolean onDoActionTrigger(Function f, StaplerRequest req, StaplerResponse rsp, Object node) {
+ LOGGER.log(Level.WARNING, LOG_MESSAGE, new Object[]{
+ req.getPathInfo(),
+ f.getSignature()
+ });
+ return false;
+ }
+
+ @Override
+ public boolean onGetterTrigger(Function f, StaplerRequest req, StaplerResponse rsp, Object node, String expression) {
+ LOGGER.log(Level.WARNING, LOG_MESSAGE, new Object[]{
+ req.getPathInfo(),
+ f.getSignature()
+ });
+ return false;
+ }
+
+ @Override
+ public boolean onFieldTrigger(FieldRef f, StaplerRequest req, StaplerResponse staplerResponse, Object node, String expression) {
+ LOGGER.log(Level.WARNING, LOG_MESSAGE, new Object[]{
+ req.getPathInfo(),
+ f.getSignature()
+ });
+ return false;
+ }
+}
diff --git a/core/src/main/java/jenkins/security/stapler/StaticRoutingDecisionProvider.java b/core/src/main/java/jenkins/security/stapler/StaticRoutingDecisionProvider.java
new file mode 100644
index 0000000000..cbb5876015
--- /dev/null
+++ b/core/src/main/java/jenkins/security/stapler/StaticRoutingDecisionProvider.java
@@ -0,0 +1,266 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2018, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package jenkins.security.stapler;
+
+import com.google.common.annotations.VisibleForTesting;
+import hudson.BulkChange;
+import hudson.Extension;
+import hudson.ExtensionList;
+import hudson.model.Saveable;
+import jenkins.model.Jenkins;
+import jenkins.util.SystemProperties;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.stapler.Function;
+import org.kohsuke.stapler.WebApp;
+import org.kohsuke.stapler.lang.FieldRef;
+
+import javax.annotation.Nonnull;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Fill the list of getter methods that are whitelisted for Stapler
+ * Each item in the set are formatted to correspond exactly to what {@link Function#getDisplayName()} returns
+ */
+@Restricted(NoExternalUse.class)
+@Extension
+public class StaticRoutingDecisionProvider extends RoutingDecisionProvider implements Saveable {
+ private static final Logger LOGGER = Logger.getLogger(StaticRoutingDecisionProvider.class.getName());
+
+ private Set whitelistSignaturesFromFixedList;
+ private Set whitelistSignaturesFromUserControlledList;
+
+ private Set blacklistSignaturesFromFixedList;
+ private Set blacklistSignaturesFromUserControlledList;
+
+ public StaticRoutingDecisionProvider() {
+ reload();
+ }
+
+ /**
+ * Return the singleton instance of this class, typically for script console use
+ */
+ public static StaticRoutingDecisionProvider get() {
+ return ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class);
+ }
+
+ /**
+ * @see Function#getSignature()
+ * @see FieldRef#getSignature()
+ */
+ @Nonnull
+ public synchronized Decision decide(@Nonnull String signature) {
+ if (whitelistSignaturesFromFixedList == null || whitelistSignaturesFromUserControlledList == null ||
+ blacklistSignaturesFromFixedList == null || blacklistSignaturesFromUserControlledList == null) {
+ reload();
+ }
+
+ LOGGER.log(Level.CONFIG, "Checking whitelist for " + signature);
+
+ // priority to blacklist
+ if (blacklistSignaturesFromFixedList.contains(signature) || blacklistSignaturesFromUserControlledList.contains(signature)) {
+ return Decision.REJECTED;
+ }
+
+ if (whitelistSignaturesFromFixedList.contains(signature) || whitelistSignaturesFromUserControlledList.contains(signature)) {
+ return Decision.ACCEPTED;
+ }
+
+ return Decision.UNKNOWN;
+ }
+
+ public synchronized void reload() {
+ reloadFromDefault();
+ reloadFromUserControlledList();
+
+ resetMetaClassCache();
+ }
+
+ @VisibleForTesting
+ synchronized void resetAndSave(){
+ this.whitelistSignaturesFromFixedList = new HashSet<>();
+ this.whitelistSignaturesFromUserControlledList = new HashSet<>();
+ this.blacklistSignaturesFromFixedList = new HashSet<>();
+ this.blacklistSignaturesFromUserControlledList = new HashSet<>();
+
+ this.save();
+ }
+
+ private void resetMetaClassCache() {
+ // to allow the change to be effective, i.e. rebuild the MetaClass using the new whitelist
+ WebApp.get(Jenkins.get().servletContext).clearMetaClassCache();
+ }
+
+ private synchronized void reloadFromDefault() {
+ try (InputStream is = StaticRoutingDecisionProvider.class.getResourceAsStream("default-whitelist.txt")) {
+ whitelistSignaturesFromFixedList = new HashSet<>();
+ blacklistSignaturesFromFixedList = new HashSet<>();
+
+ parseFileIntoList(
+ IOUtils.readLines(is, StandardCharsets.UTF_8),
+ whitelistSignaturesFromFixedList,
+ blacklistSignaturesFromFixedList
+ );
+ } catch (IOException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+
+ LOGGER.log(Level.FINE, "Found {0} getter in the standard whitelist", whitelistSignaturesFromFixedList.size());
+ }
+
+ public synchronized StaticRoutingDecisionProvider add(@Nonnull String signature) {
+ if (this.whitelistSignaturesFromUserControlledList.add(signature)) {
+ LOGGER.log(Level.INFO, "Signature [{0}] added to the whitelist", signature);
+ save();
+ resetMetaClassCache();
+ } else {
+ LOGGER.log(Level.INFO, "Signature [{0}] was already present in the whitelist", signature);
+ }
+ return this;
+ }
+
+ public synchronized StaticRoutingDecisionProvider addBlacklistSignature(@Nonnull String signature) {
+ if (this.blacklistSignaturesFromUserControlledList.add(signature)) {
+ LOGGER.log(Level.INFO, "Signature [{0}] added to the blacklist", signature);
+ save();
+ resetMetaClassCache();
+ } else {
+ LOGGER.log(Level.INFO, "Signature [{0}] was already present in the blacklist", signature);
+ }
+ return this;
+ }
+
+ public synchronized StaticRoutingDecisionProvider remove(@Nonnull String signature) {
+ if (this.whitelistSignaturesFromUserControlledList.remove(signature)) {
+ LOGGER.log(Level.INFO, "Signature [{0}] removed from the whitelist", signature);
+ save();
+ resetMetaClassCache();
+ } else {
+ LOGGER.log(Level.INFO, "Signature [{0}] was not present in the whitelist", signature);
+ }
+ return this;
+ }
+
+ public synchronized StaticRoutingDecisionProvider removeBlacklistSignature(@Nonnull String signature) {
+ if (this.blacklistSignaturesFromUserControlledList.remove(signature)) {
+ LOGGER.log(Level.INFO, "Signature [{0}] removed from the blacklist", signature);
+ save();
+ resetMetaClassCache();
+ } else {
+ LOGGER.log(Level.INFO, "Signature [{0}] was not present in the blacklist", signature);
+ }
+ return this;
+ }
+
+ /**
+ * Saves the configuration info to the disk.
+ */
+ public synchronized void save() {
+ if (BulkChange.contains(this)) {
+ return;
+ }
+
+ File file = getConfigFile();
+ try {
+ List allSignatures = new ArrayList<>(whitelistSignaturesFromUserControlledList);
+ blacklistSignaturesFromUserControlledList.stream()
+ .map(signature -> "!" + signature)
+ .forEach(allSignatures::add);
+
+ FileUtils.writeLines(file, allSignatures);
+ } catch (IOException e) {
+ LOGGER.log(Level.WARNING, "Failed to save " + file.getAbsolutePath(), e);
+ }
+ }
+
+ /**
+ * Loads the data from the disk into this object.
+ *
+ *
+ * The constructor of the derived class must call this method.
+ * (If we do that in the base class, the derived class won't
+ * get a chance to set default values.)
+ */
+ private synchronized void reloadFromUserControlledList() {
+ File file = getConfigFile();
+ if (!file.exists()) {
+ if ((whitelistSignaturesFromUserControlledList != null && whitelistSignaturesFromUserControlledList.isEmpty()) ||
+ (blacklistSignaturesFromUserControlledList != null && blacklistSignaturesFromUserControlledList.isEmpty())) {
+ LOGGER.log(Level.INFO, "No whitelist source file found at " + file + " so resetting user-controlled whitelist");
+ }
+ whitelistSignaturesFromUserControlledList = new HashSet<>();
+ blacklistSignaturesFromUserControlledList = new HashSet<>();
+ return;
+ }
+
+ LOGGER.log(Level.INFO, "Whitelist source file found at " + file);
+
+ try {
+ whitelistSignaturesFromUserControlledList = new HashSet<>();
+ blacklistSignaturesFromUserControlledList = new HashSet<>();
+
+ parseFileIntoList(
+ FileUtils.readLines(file, StandardCharsets.UTF_8),
+ whitelistSignaturesFromUserControlledList,
+ blacklistSignaturesFromUserControlledList
+ );
+ } catch (IOException e) {
+ LOGGER.log(Level.WARNING, "Failed to load " + file.getAbsolutePath(), e);
+ }
+ }
+
+ private File getConfigFile() {
+ return new File(WHITELIST_PATH == null ? new File(Jenkins.get().getRootDir(), "stapler-whitelist.txt").toString() : WHITELIST_PATH);
+ }
+
+ private void parseFileIntoList(List lines, Set whitelist, Set blacklist){
+ lines.stream()
+ .filter(line -> !line.matches("#.*|\\s*"))
+ .forEach(line -> {
+ if (line.startsWith("!")) {
+ String withoutExclamation = line.substring(1);
+ if (!withoutExclamation.isEmpty()) {
+ blacklist.add(withoutExclamation);
+ }
+ } else {
+ whitelist.add(line);
+ }
+ });
+ }
+
+ /** Allow script console access */
+ public static String WHITELIST_PATH = SystemProperties.getString(StaticRoutingDecisionProvider.class.getName() + ".whitelist");
+
+}
diff --git a/core/src/main/java/jenkins/security/stapler/TypedFilter.java b/core/src/main/java/jenkins/security/stapler/TypedFilter.java
new file mode 100644
index 0000000000..6f9bdb4019
--- /dev/null
+++ b/core/src/main/java/jenkins/security/stapler/TypedFilter.java
@@ -0,0 +1,276 @@
+package jenkins.security.stapler;
+
+import hudson.ExtensionList;
+import jenkins.util.SystemProperties;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.stapler.Function;
+import org.kohsuke.stapler.FunctionList;
+import org.kohsuke.stapler.StaplerFallback;
+import org.kohsuke.stapler.StaplerOverridable;
+import org.kohsuke.stapler.StaplerProxy;
+import org.kohsuke.stapler.WebApp;
+import org.kohsuke.stapler.interceptor.InterceptorAnnotation;
+import org.kohsuke.stapler.lang.FieldRef;
+
+import javax.annotation.Nonnull;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+@Restricted(NoExternalUse.class)
+public class TypedFilter implements FieldRef.Filter, FunctionList.Filter {
+ private static final Logger LOGGER = Logger.getLogger(TypedFilter.class.getName());
+
+ private static final Map, Boolean> staplerCache = new HashMap<>();
+
+ private boolean isClassAcceptable(Class> clazz) {
+ if (clazz.isArray()) {
+ // special case to allow klass.isArray() dispatcher
+ Class> elementClazz = clazz.getComponentType();
+ // does not seem possible to fall in an infinite loop since array cannot be recursively defined
+ if (isClassAcceptable(elementClazz)) {
+ LOGGER.log(Level.FINE,
+ "Class {0} is acceptable because it is an Array of acceptable elements {1}",
+ new Object[]{clazz.getName(), elementClazz.getName()}
+ );
+ return true;
+ } else {
+ LOGGER.log(Level.FINE,
+ "Class {0} is not acceptable because it is an Array of non-acceptable elements {1}",
+ new Object[]{clazz.getName(), elementClazz.getName()}
+ );
+ return false;
+ }
+ }
+ return SKIP_TYPE_CHECK || isStaplerRelevantCached(clazz);
+ }
+
+ private static boolean isStaplerRelevantCached(@Nonnull Class> clazz) {
+ if (staplerCache.containsKey(clazz)) {
+ return staplerCache.get(clazz);
+ }
+ boolean ret = isStaplerRelevant(clazz);
+
+ staplerCache.put(clazz, ret);
+ return ret;
+ }
+
+ @Restricted(NoExternalUse.class)
+ public static boolean isStaplerRelevant(@Nonnull Class> clazz) {
+ return isSpecificClassStaplerRelevant(clazz) || isSuperTypesStaplerRelevant(clazz);
+ }
+
+ private static boolean isSuperTypesStaplerRelevant(@Nonnull Class> clazz) {
+ Class> superclass = clazz.getSuperclass();
+ if (superclass != null && isStaplerRelevantCached(superclass)) {
+ return true;
+ }
+ for (Class> interfaceClass : clazz.getInterfaces()) {
+ if (isStaplerRelevantCached(interfaceClass)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isSpecificClassStaplerRelevant(@Nonnull Class> clazz) {
+ if (clazz.isAnnotationPresent(StaplerAccessibleType.class)) {
+ return true;
+ }
+
+ // Classes implementing these Stapler types can be considered routable
+ if (StaplerProxy.class.isAssignableFrom(clazz)) {
+ return true;
+ }
+ if (StaplerFallback.class.isAssignableFrom(clazz)) {
+ return true;
+ }
+ if (StaplerOverridable.class.isAssignableFrom(clazz)) {
+ return true;
+ }
+
+ for (Method m : clazz.getMethods()) {
+ if (isRoutableMethod(m)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean isRoutableMethod(@Nonnull Method m) {
+ for (Annotation a : m.getDeclaredAnnotations()) {
+ if (WebMethodConstants.WEB_METHOD_ANNOTATION_NAMES.contains(a.annotationType().getName())) {
+ return true;
+ }
+ if (a.annotationType().isAnnotationPresent(InterceptorAnnotation.class)) {
+ // This is a Stapler interceptor annotation like RequirePOST or JsonResponse
+ return true;
+ }
+ }
+
+ for (Annotation[] set : m.getParameterAnnotations()) {
+ for (Annotation a : set) {
+ if (WebMethodConstants.WEB_METHOD_PARAMETER_ANNOTATION_NAMES.contains(a.annotationType().getName())) {
+ return true;
+ }
+ }
+ }
+
+ for (Class> parameterType : m.getParameterTypes()) {
+ if (WebMethodConstants.WEB_METHOD_PARAMETERS_NAMES.contains(parameterType.getName())) {
+ return true;
+ }
+ }
+
+ return WebApp.getCurrent().getFilterForDoActions().keep(new Function.InstanceFunction(m));
+ }
+
+ @Override
+ public boolean keep(@Nonnull FieldRef fieldRef) {
+
+ if (fieldRef.getAnnotation(StaplerNotDispatchable.class) != null) {
+ // explicitly marked as an invalid field
+ return false;
+ }
+
+ if (fieldRef.getAnnotation(StaplerDispatchable.class) != null) {
+ // explicitly marked as a valid field
+ return true;
+ }
+
+ String signature = fieldRef.getSignature();
+
+ // check whitelist
+ ExtensionList decisionProviders = ExtensionList.lookup(RoutingDecisionProvider.class);
+ if (decisionProviders.size() > 0) {
+ for (RoutingDecisionProvider provider : decisionProviders) {
+ RoutingDecisionProvider.Decision fieldDecision = provider.decide(signature);
+ if (fieldDecision == RoutingDecisionProvider.Decision.ACCEPTED) {
+ LOGGER.log(Level.CONFIG, "Field {0} is acceptable because it is whitelisted by {1}", new Object[]{signature, provider});
+ return true;
+ }
+ if (fieldDecision == RoutingDecisionProvider.Decision.REJECTED) {
+ LOGGER.log(Level.CONFIG, "Field {0} is not acceptable because it is blacklisted by {1}", new Object[]{signature, provider});
+ return false;
+ }
+ Class> type = fieldRef.getReturnType();
+ if (type != null) {
+ String typeSignature = "class " + type.getCanonicalName();
+ RoutingDecisionProvider.Decision fieldTypeDecision = provider.decide(typeSignature);
+ if (fieldTypeDecision == RoutingDecisionProvider.Decision.ACCEPTED) {
+ LOGGER.log(Level.CONFIG, "Field {0} is acceptable because its type is whitelisted by {1}", new Object[]{signature, provider});
+ return true;
+ }
+ if (fieldTypeDecision == RoutingDecisionProvider.Decision.REJECTED) {
+ LOGGER.log(Level.CONFIG, "Field {0} is not acceptable because its type is blacklisted by {1}", new Object[]{signature, provider});
+ return false;
+ }
+ }
+ }
+ }
+
+ if (PROHIBIT_STATIC_ACCESS && fieldRef.isStatic()) {
+ // unless whitelisted or marked as routable, reject static fields
+ return false;
+ }
+
+
+ Class> returnType = fieldRef.getReturnType();
+
+ boolean isOk = isClassAcceptable(returnType);
+ LOGGER.log(Level.FINE, "Field analyzed: {0} => {1}", new Object[]{fieldRef.getName(), isOk});
+ return isOk;
+ }
+
+ @Override
+ public boolean keep(@Nonnull Function function) {
+
+ if (function.getAnnotation(StaplerNotDispatchable.class) != null) {
+ // explicitly marked as an invalid getter
+ return false;
+ }
+
+ if (function.getAnnotation(StaplerDispatchable.class) != null) {
+ // explicitly marked as a valid getter
+ return true;
+ }
+
+ String signature = function.getSignature();
+
+ // check whitelist
+ ExtensionList decision = ExtensionList.lookup(RoutingDecisionProvider.class);
+ if (decision.size() > 0) {
+ for (RoutingDecisionProvider provider : decision) {
+ RoutingDecisionProvider.Decision methodDecision = provider.decide(signature);
+ if (methodDecision == RoutingDecisionProvider.Decision.ACCEPTED) {
+ LOGGER.log(Level.CONFIG, "Function {0} is acceptable because it is whitelisted by {1}", new Object[]{signature, provider});
+ return true;
+ }
+ if (methodDecision == RoutingDecisionProvider.Decision.REJECTED) {
+ LOGGER.log(Level.CONFIG, "Function {0} is not acceptable because it is blacklisted by {1}", new Object[]{signature, provider});
+ return false;
+ }
+
+ Class> type = function.getReturnType();
+ if (type != null) {
+ String typeSignature = "class " + type.getCanonicalName();
+ RoutingDecisionProvider.Decision returnTypeDecision = provider.decide(typeSignature);
+ if (returnTypeDecision == RoutingDecisionProvider.Decision.ACCEPTED) {
+ LOGGER.log(Level.CONFIG, "Function {0} is acceptable because its type is whitelisted by {1}", new Object[]{signature, provider});
+ return true;
+ }
+ if (returnTypeDecision == RoutingDecisionProvider.Decision.REJECTED) {
+ LOGGER.log(Level.CONFIG, "Function {0} is not acceptable because its type is blacklisted by {1}", new Object[]{signature, provider});
+ return false;
+ }
+ }
+ }
+ }
+
+ if (PROHIBIT_STATIC_ACCESS && function.isStatic()) {
+ // unless whitelisted or marked as routable, reject static methods
+ return false;
+ }
+
+ if (function.getName().equals("getDynamic")) {
+ Class[] parameterTypes = function.getParameterTypes();
+ if (parameterTypes.length > 0 && parameterTypes[0] == String.class) {
+ // While this is more general than what Stapler can invoke on these types,
+ // The above is the only criterion for Stapler to attempt dispatch.
+ // Therefore prohibit this as a regular getter.
+ return false;
+ }
+ }
+
+ if (function.getName().equals("getStaplerFallback") && function.getParameterTypes().length == 0) {
+ // A parameter-less #getStaplerFallback() implements special fallback behavior for the
+ // StaplerFallback interface. We do not check for the presence of the interface on the current
+ // class, or the return type, as that could change since the implementing component was last built.
+ return false;
+ }
+
+ if (function.getName().equals("getTarget") && function.getParameterTypes().length == 0) {
+ // A parameter-less #getTarget() implements special redirection behavior for the
+ // StaplerProxy interface. We do not check for the presence of the interface on the current
+ // class, or the return type, as that could change since the implementing component was last built.
+ return false;
+ }
+
+ Class> returnType = function.getReturnType();
+
+ boolean isOk = isClassAcceptable(returnType);
+ LOGGER.log(Level.FINE, "Function analyzed: {0} => {1}", new Object[]{signature, isOk});
+ return isOk;
+ }
+
+ @Restricted(NoExternalUse.class)
+ public static boolean SKIP_TYPE_CHECK = SystemProperties.getBoolean(TypedFilter.class.getName() + ".skipTypeCheck");
+
+ @Restricted(NoExternalUse.class)
+ public static boolean PROHIBIT_STATIC_ACCESS = SystemProperties.getBoolean(TypedFilter.class.getName() + ".prohibitStaticAccess", true);
+}
diff --git a/core/src/main/java/jenkins/security/stapler/WebMethodConstants.java b/core/src/main/java/jenkins/security/stapler/WebMethodConstants.java
new file mode 100644
index 0000000000..d8f678e4b0
--- /dev/null
+++ b/core/src/main/java/jenkins/security/stapler/WebMethodConstants.java
@@ -0,0 +1,101 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2018, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package jenkins.security.stapler;
+
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.stapler.AncestorInPath;
+import org.kohsuke.stapler.Header;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+import org.kohsuke.stapler.WebMethod;
+import org.kohsuke.stapler.bind.JavaScriptMethod;
+import org.kohsuke.stapler.json.JsonBody;
+import org.kohsuke.stapler.json.SubmittedForm;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.annotation.Annotation;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Restricted(NoExternalUse.class)
+final class WebMethodConstants {
+ /**
+ * If a method has at least one of those parameters, it is considered as an implicit web method
+ */
+ private static final List> WEB_METHOD_PARAMETERS = Collections.unmodifiableList(Arrays.asList(
+ StaplerRequest.class,
+ HttpServletRequest.class,
+ StaplerResponse.class,
+ HttpServletResponse.class
+ ));
+
+ static final Set WEB_METHOD_PARAMETERS_NAMES = Collections.unmodifiableSet(
+ WEB_METHOD_PARAMETERS.stream()
+ .map(Class::getName)
+ .collect(Collectors.toSet())
+ );
+
+ /**
+ * If a method is annotated with one of those annotations,
+ * the method is considered as an explicit web method
+ */
+ static final List> WEB_METHOD_ANNOTATIONS = Collections.singletonList(
+ WebMethod.class
+ // plus every annotation that's annotated with InterceptorAnnotation
+ // JavaScriptMethod.class not taken here because it's a special case
+ );
+
+ static final Set WEB_METHOD_ANNOTATION_NAMES;
+ static {
+ Set webMethodAnnotationNames = WEB_METHOD_ANNOTATIONS.stream()
+ .map(Class::getName)
+ .collect(Collectors.toSet());
+ webMethodAnnotationNames.add(JavaScriptMethod.class.getName());
+ WEB_METHOD_ANNOTATION_NAMES = Collections.unmodifiableSet(webMethodAnnotationNames);
+ }
+
+ /**
+ * If at least one parameter of the method is annotated with one of those annotations,
+ * the method is considered as an implicit web method
+ */
+ private static final List> WEB_METHOD_PARAMETER_ANNOTATIONS = Collections.unmodifiableList(Arrays.asList(
+ QueryParameter.class,
+ AncestorInPath.class,
+ Header.class,
+ JsonBody.class,
+ SubmittedForm.class
+ ));
+
+ static final Set WEB_METHOD_PARAMETER_ANNOTATION_NAMES = Collections.unmodifiableSet(
+ WEB_METHOD_PARAMETER_ANNOTATIONS.stream()
+ .map(Class::getName)
+ .collect(Collectors.toSet())
+ );
+}
diff --git a/core/src/main/resources/jenkins/security/stapler/default-whitelist.txt b/core/src/main/resources/jenkins/security/stapler/default-whitelist.txt
new file mode 100644
index 0000000000..0938363779
--- /dev/null
+++ b/core/src/main/resources/jenkins/security/stapler/default-whitelist.txt
@@ -0,0 +1,177 @@
+# This file contains the built-in whitelist for Stapler request dispatching.
+# It's a tool for retaining compatibility with unusual plugin behavior after introducing the SECURITY-595 security fix.
+# To provide your own custom whitelist, create/edit $JENKINS_HOME/stapler-whitelist.txt
+
+# Determine the whitelist entries for methods in a known class from the script console:
+# com.acme.package.ClassName.class.methods.each {
+# println new org.kohsuke.stapler.Function.InstanceFunction(it).signature
+# }
+# com.acme.package.ClassName.class.fields.each {
+# println org.kohsuke.stapler.lang.FieldRef.wrap(it).signature
+# }
+# return
+
+#######################################################################################################################
+###################################################### Whitelist ######################################################
+#######################################################################################################################
+
+
+######################
+# Credentials Plugin #
+######################
+# Used where credentials are used (e.g. SCM config), without this, 'Add' button will break as its dialog is at:
+# /descriptor/….CredentialsSelectHelper/resolver/….CredentialsSelectHelper$SystemContextResolver/provider/….SystemCredentialsProvider$ProviderImpl/context/jenkins/dialog
+method com.cloudbees.plugins.credentials.CredentialsSelectHelper getResolver java.lang.String
+method com.cloudbees.plugins.credentials.CredentialsSelectHelper$WrappedContextResolver getProvider java.lang.String
+# Used by Credentials Plugin's FingerprintTest and Git Plugin's GitSCMTest, as well as others:
+method com.cloudbees.plugins.credentials.CredentialsStoreAction$DomainWrapper getCredentials
+
+
+################
+# JUnit Plugin #
+################
+# Allow various #getHistory() as these only have resources and #getGraph()
+class hudson.tasks.junit.History
+
+
+##################
+# Metrics Plugin #
+##################
+# Method returns Object for no clear reason
+method jenkins.metrics.api.MetricsRootAction getCurrentUser
+
+
+#########################
+# Pipeline Plugin Suite #
+#########################
+# Used in the 'Pipeline Steps' UI, the Execution has Nodes but no UI of its own
+method org.jenkinsci.plugins.workflow.job.WorkflowRun getExecution
+# FlowGraphTable only has a Jelly view, and nothing else that would indicate Stapler-routability
+method org.jenkinsci.plugins.workflow.job.views.FlowGraphTableAction getFlowGraph
+
+
+############################
+# Maven Integration Plugin #
+############################
+# Advertised in https://github.com/jenkinsci/maven-plugin/blob/7ac83fa85fda0c4d1d02663059644f0655823879/src/main/resources/hudson/maven/reporters/MavenArtifactRecord/_api.jelly#L31
+field hudson.maven.reporters.MavenArtifactRecord attachedArtifacts
+
+
+###########################################
+# Static Analysis Plugins (analysis-core) #
+###########################################
+# Methods return Object for no clear reason
+method hudson.plugins.analysis.core.AbstractProjectAction getTrendGraph
+method hudson.plugins.analysis.core.AbstractProjectAction getTrendDetails
+method hudson.plugins.analysis.core.AbstractProjectAction getTrendDetails org.kohsuke.stapler.StaplerRequest org.kohsuke.stapler.StaplerResponse
+
+
+###########################
+# Blue Ocean Plugin Suite #
+###########################
+# Methods return Object for no clear reason
+method io.jenkins.blueocean.service.embedded.rest.AbstractRunImpl getLog
+method io.jenkins.blueocean.service.embedded.rest.QueuedBlueRun getLog
+method io.jenkins.blueocean.rest.impl.pipeline.PipelineNodeImpl getLog
+method io.jenkins.blueocean.rest.impl.pipeline.PipelineStepImpl getLog
+
+
+##########################
+# Promoted Builds Plugin #
+##########################
+# Only subtypes look Stapler-relevant
+method hudson.plugins.promoted_builds.PromotionProcess getPromotionCondition java.lang.String
+
+
+##########################
+# Robot Framework Plugin #
+##########################
+# Unsure whether this is needed, but RobotSuiteResult is supposed to be reachable.
+method hudson.plugins.robot.model.RobotResult getSuites
+
+
+########################
+# Maven Invoker Plugin #
+########################
+# MavenInvokerResult only has an index view, and nothing else that would indicate Stapler-routability
+method org.jenkinsci.plugins.maveninvoker.MavenInvokerBuildAction getResult java.lang.String
+
+
+###########################
+# Cloud Statistics Plugin #
+###########################
+# Used via CloudStatistics#getUrl(ProvisioningActivity, PhaseExecution, PhaseExecutionAttachment) for attempts.groovy
+method org.jenkinsci.plugins.cloudstats.CloudStatistics getActivity java.lang.String
+method org.jenkinsci.plugins.cloudstats.ProvisioningActivity getPhase java.lang.String
+
+
+####################
+# SLOCCount Plugin #
+####################
+# Support various getters in the same type
+class hudson.plugins.sloccount.SloccountResult
+
+
+##############################
+# Project Inheritance Plugin #
+##############################
+# do* methods with no indication they're supposed to be routable (return String, no args, no annotations)
+method hudson.plugins.project_inheritance.projects.InheritanceProject doGetParamDefaultsAsXML
+method hudson.plugins.project_inheritance.projects.InheritanceProject doGetParamExpansionsAsXML
+method hudson.plugins.project_inheritance.projects.InheritanceProject doGetVersionsAsCompressedXML
+method hudson.plugins.project_inheritance.projects.InheritanceProject doGetVersionsAsXML
+method hudson.plugins.project_inheritance.projects.InheritanceProject doRenderSVGRelationGraph
+
+
+###############################################
+# Changes since last successfull build Plugin # (sic)
+###############################################
+# Only has an index.jelly view, so needs to be explicit
+class com.cloudbees.jenkins.plugins.changelog.Changes
+
+
+###################
+# Fitnesse Plugin #
+###################
+# return hudson.plugins.fitnesse.History with only getters for hudson.util.Graph, like analysis-core
+method hudson.plugins.fitnesse.FitnesseProjectAction getTrend
+
+
+###########################
+# Build Time Blame Plugin #
+###########################
+# Only has resources and a getter for a Graph, so needs whitelisting when not scanning
+class org.jenkins.ci.plugins.buildtimeblame.analysis.BlameReport
+
+
+##########################
+# Azure VM Agents Plugin #
+##########################
+# Declared to return String, no args, and always returns null…?
+method com.microsoft.azure.vmagent.AzureVMAgentTemplate$DescriptorImpl doFillImageReferenceTypeItems
+
+
+################################
+# TestComplete support plug-in #
+################################
+method com.smartbear.jenkins.plugins.testcomplete.TcSummaryAction getReports
+
+
+#####################
+# Job Cacher Plugin #
+#####################
+# UI linking to this is ArbitraryFileCache/cache-entry.jelly, used from CacheProjectAction/index.jelly, and ultimately handled by ArbitraryFileCache#doDynamic
+method jenkins.plugins.jobcacher.CacheProjectAction getCaches
+
+
+#########################
+# Dimensions SCM Plugin #
+#########################
+# Needs whitelisting due to not following the usual naming scheme
+method hudson.plugins.dimensionsscm.DimensionsSCM$DescriptorImpl domanadatoryFieldCheck org.kohsuke.stapler.StaplerRequest org.kohsuke.stapler.StaplerResponse
+
+
+##############################
+# Google Health Check Plugin #
+##############################
+method com.google.jenkins.plugins.health.lib.DerivedPageAction getZone java.lang.String
diff --git a/core/src/test/java/jenkins/security/stapler/StaplerSignaturesTest.java b/core/src/test/java/jenkins/security/stapler/StaplerSignaturesTest.java
new file mode 100644
index 0000000000..1e7a791561
--- /dev/null
+++ b/core/src/test/java/jenkins/security/stapler/StaplerSignaturesTest.java
@@ -0,0 +1,116 @@
+package jenkins.security.stapler;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.kohsuke.stapler.Function;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+import org.kohsuke.stapler.json.JsonResponse;
+import org.kohsuke.stapler.lang.FieldRef;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class StaplerSignaturesTest {
+ @Test
+ public void testSignaturesSimple() throws Exception {
+ Set methodSignatures = Arrays.stream(SomeClass.class.getMethods()).map(it -> new Function.InstanceFunction(it).getSignature()).collect(Collectors.toSet());
+ Assert.assertEquals(SomeClass.METHOD_SIGNATURES, methodSignatures);
+
+ Set fieldSignatures = Arrays.stream(SomeClass.class.getFields()).map(it -> FieldRef.wrap(it).getSignature()).collect(Collectors.toSet());
+ Assert.assertEquals(SomeClass.FIELD_SIGNATURES, fieldSignatures);
+ }
+
+ @Test
+ public void testSignaturesInheritance() throws Exception {
+ Set methodSignatures = Arrays.stream(SomeSubclass.class.getMethods()).map(it -> new Function.InstanceFunction(it).getSignature()).collect(Collectors.toSet());
+ Assert.assertEquals(SomeSubclass.METHOD_SIGNATURES, methodSignatures);
+
+ Set fieldSignatures = Arrays.stream(SomeSubclass.class.getFields()).map(it -> FieldRef.wrap(it).getSignature()).collect(Collectors.toSet());
+ Assert.assertEquals(SomeSubclass.FIELD_SIGNATURES, fieldSignatures);
+ }
+
+ public static class SomeClass {
+ static Set METHOD_SIGNATURES = new HashSet<>(Arrays.asList(
+ "method jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo",
+ "method jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo java.lang.String",
+ "staticMethod jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo int",
+ "staticMethod jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo long",
+ "method jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo jenkins.security.stapler.StaplerSignaturesTest$SomeClass",
+ "method jenkins.security.stapler.StaplerSignaturesTest$SomeClass doFoo org.kohsuke.stapler.StaplerRequest org.kohsuke.stapler.StaplerResponse",
+ "method jenkins.security.stapler.StaplerSignaturesTest$SomeClass doWhatever java.lang.String",
+ "method java.lang.Object getClass",
+ "method java.lang.Object equals java.lang.Object",
+ "method java.lang.Object hashCode",
+ "method java.lang.Object notify",
+ "method java.lang.Object notifyAll",
+ "method java.lang.Object toString",
+ "method java.lang.Object wait long int",
+ "method java.lang.Object wait long",
+ "method java.lang.Object wait"
+ ));
+ public void getFoo() {}
+ public void getFoo(String arg) {}
+ public static void getFoo(int arg) {}
+ public static void getFoo(long arg) {}
+ public void getFoo(SomeClass arg) {}
+ public void doFoo(StaplerRequest req, StaplerResponse rsp) {}
+ @StaplerDispatchable @JsonResponse
+ public void doWhatever(@QueryParameter String arg) {}
+
+ static Set FIELD_SIGNATURES = new HashSet<>(Arrays.asList(
+ "field jenkins.security.stapler.StaplerSignaturesTest$SomeClass whatever",
+ "field jenkins.security.stapler.StaplerSignaturesTest$SomeClass thing",
+ "staticField jenkins.security.stapler.StaplerSignaturesTest$SomeClass staticField",
+ "field jenkins.security.stapler.StaplerSignaturesTest$SomeClass stringList"
+ ));
+ public String whatever;
+ public Object thing;
+ public static Object staticField;
+ public List stringList;
+
+ }
+
+ public static class SomeSubclass extends SomeClass {
+ static Set METHOD_SIGNATURES = new HashSet<>(Arrays.asList(
+ "method jenkins.security.stapler.StaplerSignaturesTest$SomeSubclass getFoo",
+ "method jenkins.security.stapler.StaplerSignaturesTest$SomeSubclass subtypeExclusive",
+ "method jenkins.security.stapler.StaplerSignaturesTest$SomeSubclass subtypeExclusive java.lang.String",
+ "method jenkins.security.stapler.StaplerSignaturesTest$SomeSubclass varargMethod [Ljava.lang.String;",
+ "method jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo java.lang.String",
+ "staticMethod jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo int",
+ "staticMethod jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo long",
+ "method jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo jenkins.security.stapler.StaplerSignaturesTest$SomeClass",
+ "method jenkins.security.stapler.StaplerSignaturesTest$SomeClass doFoo org.kohsuke.stapler.StaplerRequest org.kohsuke.stapler.StaplerResponse",
+ "method jenkins.security.stapler.StaplerSignaturesTest$SomeClass doWhatever java.lang.String",
+ "method java.lang.Object getClass",
+ "method java.lang.Object equals java.lang.Object",
+ "method java.lang.Object hashCode",
+ "method java.lang.Object notify",
+ "method java.lang.Object notifyAll",
+ "method java.lang.Object toString",
+ "method java.lang.Object wait long int",
+ "method java.lang.Object wait long",
+ "method java.lang.Object wait"
+ ));
+ public void getFoo() {}
+ public void subtypeExclusive(){}
+ public void subtypeExclusive(String arg){}
+ public void varargMethod(String... args){}
+
+ static Set FIELD_SIGNATURES = new HashSet<>(Arrays.asList(
+ "field jenkins.security.stapler.StaplerSignaturesTest$SomeSubclass whatever",
+ "field jenkins.security.stapler.StaplerSignaturesTest$SomeClass whatever",
+ "field jenkins.security.stapler.StaplerSignaturesTest$SomeClass thing",
+ "staticField jenkins.security.stapler.StaplerSignaturesTest$SomeSubclass staticField",
+ "staticField jenkins.security.stapler.StaplerSignaturesTest$SomeClass staticField",
+ "field jenkins.security.stapler.StaplerSignaturesTest$SomeClass stringList"
+ ));
+ public String whatever;
+ public static Object staticField;
+ }
+}
diff --git a/test/pom.xml b/test/pom.xml
index fb587c3640..868fb368bf 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -68,6 +68,12 @@ THE SOFTWARE.
2.0
test