* Changing this permission check to return {@link Jenkins#SYSTEM_READ} will make the active
* administrative monitor appear on {@code manage.jelly} and on the globally visible
@@ -191,6 +191,10 @@ public void doDisable(StaplerRequest req, StaplerResponse rsp) throws IOExceptio
* {@link #doDisable(StaplerRequest, StaplerResponse)} will still always require Administer permission.
*
*
+ * This method only allows for a single permission to be returned. If more complex permission checks are required,
+ * override {@link #checkRequiredPermission()} and {@link #hasRequiredPermission()} instead.
+ *
+ *
* Implementers need to ensure that {@code doAct} and other web methods perform necessary permission checks:
* Users with System Read permissions are expected to be limited to read-only access.
* Form UI elements that change system state, e.g. toggling a feature on or off, need to be hidden from users
@@ -201,13 +205,50 @@ public Permission getRequiredPermission() {
return Jenkins.ADMINISTER;
}
+ /**
+ * Checks if the current user has the minimum required permission to view this administrative monitor.
+ *
+ * Subclasses may override this method and {@link #hasRequiredPermission()} instead of {@link #getRequiredPermission()} to perform more complex permission checks,
+ * for example, checking either {@link Jenkins#MANAGE} or {@link Jenkins#SYSTEM_READ}.
+ *
+ * @see #getRequiredPermission()
+ * @see #hasRequiredPermission()
+ */
+ public void checkRequiredPermission() {
+ Jenkins.get().checkPermission(getRequiredPermission());
+ }
+
+ /**
+ * Checks if the current user has the minimum required permission to view this administrative monitor.
+ *
+ * Subclasses may override this method and {@link #checkRequiredPermission} instead of {@link #getRequiredPermission()} to perform more complex permission checks,
+ * for example, checking either {@link Jenkins#MANAGE} or {@link Jenkins#SYSTEM_READ}.
+ *
+ * @see #getRequiredPermission()
+ * @see #checkRequiredPermission()
+ */
+ public boolean hasRequiredPermission() {
+ return Jenkins.get().hasPermission(getRequiredPermission());
+ }
+
+ /**
+ * Checks if the current user has the minimum required permission to view any administrative monitor.
+ *
+ * @return true if the current user has the minimum required permission to view any administrative monitor.
+ *
+ * @since TODO
+ */
+ public static boolean hasPermissionToDisplay() {
+ return Jenkins.get().hasAnyPermission(Jenkins.SYSTEM_READ, Jenkins.MANAGE);
+ }
+
/**
* Ensure that URLs in this administrative monitor are only accessible to users with {@link #getRequiredPermission()}.
*/
@Override
@Restricted(NoExternalUse.class)
public Object getTarget() {
- Jenkins.get().checkPermission(getRequiredPermission());
+ checkRequiredPermission();
return this;
}
diff --git a/core/src/main/java/hudson/model/MyViewsProperty.java b/core/src/main/java/hudson/model/MyViewsProperty.java
index 9d7b8b651d1c..71809d788eae 100644
--- a/core/src/main/java/hudson/model/MyViewsProperty.java
+++ b/core/src/main/java/hudson/model/MyViewsProperty.java
@@ -29,6 +29,7 @@
import hudson.Extension;
import hudson.Util;
import hudson.model.Descriptor.FormException;
+import hudson.model.userproperty.UserPropertyCategory;
import hudson.security.ACL;
import hudson.util.FormValidation;
import hudson.views.MyViewsTabBar;
@@ -246,6 +247,11 @@ public String getDisplayName() {
public UserProperty newInstance(User user) {
return new MyViewsProperty();
}
+
+ @Override
+ public @NonNull UserPropertyCategory getUserPropertyCategory() {
+ return UserPropertyCategory.get(UserPropertyCategory.Preferences.class);
+ }
}
@Override
diff --git a/core/src/main/java/hudson/model/PaneStatusProperties.java b/core/src/main/java/hudson/model/PaneStatusProperties.java
index 29a460349440..4807020ca714 100644
--- a/core/src/main/java/hudson/model/PaneStatusProperties.java
+++ b/core/src/main/java/hudson/model/PaneStatusProperties.java
@@ -2,7 +2,9 @@
import static java.lang.String.format;
+import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
+import hudson.model.userproperty.UserPropertyCategory;
import hudson.util.PersistedList;
import java.io.IOException;
import javax.servlet.http.HttpSession;
@@ -56,6 +58,10 @@ public boolean isEnabled() {
return false;
}
+ @Override
+ public @NonNull UserPropertyCategory getUserPropertyCategory() {
+ return UserPropertyCategory.get(UserPropertyCategory.Invisible.class);
+ }
}
private static class PaneStatusPropertiesSessionFallback extends PaneStatusProperties {
diff --git a/core/src/main/java/hudson/model/TimeZoneProperty.java b/core/src/main/java/hudson/model/TimeZoneProperty.java
index bdf39c58527e..2675448dad80 100644
--- a/core/src/main/java/hudson/model/TimeZoneProperty.java
+++ b/core/src/main/java/hudson/model/TimeZoneProperty.java
@@ -4,6 +4,7 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.Util;
+import hudson.model.userproperty.UserPropertyCategory;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import hudson.util.ListBoxModel.Option;
@@ -106,6 +107,10 @@ public FormValidation doCheckTimeZoneName(@QueryParameter String timeZoneName) {
}
}
+ @Override
+ public @NonNull UserPropertyCategory getUserPropertyCategory() {
+ return UserPropertyCategory.get(UserPropertyCategory.Account.class);
+ }
}
@CheckForNull
diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java
index 03ab31314372..218cbf6b96a4 100644
--- a/core/src/main/java/hudson/model/UpdateCenter.java
+++ b/core/src/main/java/hudson/model/UpdateCenter.java
@@ -1322,6 +1322,10 @@ public File download(DownloadJob job, URL src) throws IOException {
sha512 != null ? new DigestOutputStream(_out, sha512) : _out, sha256) : _out, sha1) : _out;
InputStream in = con.getInputStream();
CountingInputStream cin = new CountingInputStream(in)) {
+ if (LOGGER.isLoggable(Level.FINE)) {
+ var sourceUrlString = getSourceUrl(src, con);
+ LOGGER.fine(() -> "Downloading " + job.getName() + " from " + sourceUrlString);
+ }
while ((len = cin.read(buf)) >= 0) {
out.write(buf, 0, len);
final int count = cin.getCount();
@@ -1358,15 +1362,22 @@ public File download(DownloadJob job, URL src) throws IOException {
return tmp;
} catch (IOException e) {
// assist troubleshooting in case of e.g. "too many redirects" by printing actual URL
- String extraMessage = "";
- if (con != null && con.getURL() != null && !src.toString().equals(con.getURL().toString())) {
- // Two URLs are considered equal if different hosts resolve to same IP. Prefer to log in case of string inequality,
- // because who knows how the server responds to different host name in the request header?
- // Also, since it involved name resolution, it'd be an expensive operation.
- extraMessage = " (redirected to: " + con.getURL() + ")";
+ throw new IOException("Failed to download from " + getSourceUrl(src, con), e);
+ }
+ }
+
+ private static String getSourceUrl(@NonNull URL src, @CheckForNull URLConnection connection) {
+ var sourceUrlString = src.toExternalForm();
+ if (connection != null) {
+ var connectionURL = connection.getURL();
+ if (connectionURL != null) {
+ var finalUrlString = connectionURL.toExternalForm();
+ if (!sourceUrlString.equals(finalUrlString)) {
+ return sourceUrlString + " → " + finalUrlString;
+ }
}
- throw new IOException("Failed to download from " + src + extraMessage, e);
}
+ return sourceUrlString;
}
/**
diff --git a/core/src/main/java/hudson/model/User.java b/core/src/main/java/hudson/model/User.java
index a242385cb11a..e588a79b347a 100644
--- a/core/src/main/java/hudson/model/User.java
+++ b/core/src/main/java/hudson/model/User.java
@@ -39,13 +39,11 @@
import hudson.XmlFile;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
-import hudson.model.Descriptor.FormException;
import hudson.model.listeners.SaveableListener;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.security.SecurityRealm;
import hudson.security.UserMayOrMayNotExistException2;
-import hudson.util.FormApply;
import hudson.util.FormValidation;
import hudson.util.RunList;
import hudson.util.XStream2;
@@ -77,7 +75,6 @@
import jenkins.security.LastGrantedAuthoritiesProperty;
import jenkins.security.UserDetailsCache;
import jenkins.util.SystemProperties;
-import net.sf.json.JSONObject;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@@ -87,7 +84,6 @@
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.interceptor.RequirePOST;
-import org.kohsuke.stapler.verb.POST;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
@@ -342,6 +338,29 @@ public synchronized void addProperty(@NonNull UserProperty p) throws IOException
save();
}
+ /**
+ * Expand {@link #addProperty(UserProperty)} for multiple properties to be done at once.
+ * Expected to be used by the categorized configuration pages to update part of the properties.
+ * The properties not included in the list will be let untouched.
+ * It will call the {@link UserProperty#setUser(User)} method and at the end, {@link #save()} once.
+ *
+ * @since TODO
+ */
+ public synchronized void addProperties(@NonNull List multipleProperties) throws IOException {
+ List newProperties = new ArrayList<>(this.properties);
+ for (UserProperty property : multipleProperties) {
+ UserProperty oldProp = getProperty(property.getClass());
+ if (oldProp != null) {
+ newProperties.remove(oldProp);
+ }
+ newProperties.add(property);
+ property.setUser(this);
+ }
+
+ this.properties = newProperties;
+ this.save();
+ }
+
/**
* List of all {@link UserProperty}s exposed primarily for the remoting API.
*/
@@ -859,48 +878,6 @@ public Api getApi() {
return new Api(this);
}
- /**
- * Accepts submission from the configuration page.
- */
- @POST
- public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {
- checkPermission(Jenkins.ADMINISTER);
-
- JSONObject json = req.getSubmittedForm();
- String oldFullName = this.fullName;
- fullName = json.getString("fullName");
- description = json.getString("description");
-
- List props = new ArrayList<>();
- int i = 0;
- for (UserPropertyDescriptor d : UserProperty.all()) {
- UserProperty p = getProperty(d.clazz);
-
- JSONObject o = json.optJSONObject("userProperty" + i++);
- if (o != null) {
- if (p != null) {
- p = p.reconfigure(req, o);
- } else {
- p = d.newInstance(req, o);
- }
- }
-
- if (p != null) {
- p.setUser(this);
- props.add(p);
- }
- }
- this.properties = props;
-
- save();
-
- if (oldFullName != null && !oldFullName.equals(this.fullName)) {
- UserDetailsCache.get().invalidate(oldFullName);
- }
-
- FormApply.success(".").generateResponse(req, rsp, this);
- }
-
/**
* Deletes this user from Hudson.
*/
diff --git a/core/src/main/java/hudson/model/UserProperty.java b/core/src/main/java/hudson/model/UserProperty.java
index a9b9dbae7acd..6538a5fdf661 100644
--- a/core/src/main/java/hudson/model/UserProperty.java
+++ b/core/src/main/java/hudson/model/UserProperty.java
@@ -24,9 +24,13 @@
package hudson.model;
+import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.DescriptorExtensionList;
import hudson.ExtensionPoint;
import hudson.model.Descriptor.FormException;
+import hudson.model.userproperty.UserPropertyCategory;
+import java.util.ArrayList;
+import java.util.List;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
@@ -58,6 +62,10 @@ public abstract class UserProperty implements ReconfigurableDescribable all(
return Jenkins.get().getDescriptorList(UserProperty.class);
}
+ /**
+ * Returns all the registered {@link UserPropertyCategory} descriptors for a given category.
+ *
+ * @since TODO
+ */
+ public static List allByCategoryClass(@NonNull Class extends UserPropertyCategory> categoryClass) {
+ DescriptorExtensionList all = all();
+
+ List onlyForTheCategory = new ArrayList<>(all.size());
+ for (UserPropertyDescriptor descriptor : all) {
+ if (descriptor.getUserPropertyCategory().getClass().equals(categoryClass)) {
+ onlyForTheCategory.add(descriptor);
+ }
+ }
+
+ return onlyForTheCategory;
+ }
+
@Override
public UserProperty reconfigure(StaplerRequest req, JSONObject form) throws FormException {
return form == null ? null : getDescriptor().newInstance(req, form);
diff --git a/core/src/main/java/hudson/model/UserPropertyDescriptor.java b/core/src/main/java/hudson/model/UserPropertyDescriptor.java
index 22e05ea59aff..ff3171c1fbf7 100644
--- a/core/src/main/java/hudson/model/UserPropertyDescriptor.java
+++ b/core/src/main/java/hudson/model/UserPropertyDescriptor.java
@@ -24,6 +24,12 @@
package hudson.model;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.model.userproperty.UserPropertyCategory;
+import java.util.Optional;
+import org.jenkinsci.Symbol;
+
/**
* {@link Descriptor} for {@link UserProperty}.
*
@@ -73,4 +79,51 @@ protected UserPropertyDescriptor() {
public boolean isEnabled() {
return true;
}
+
+ /**
+ * Define the category for this user property descriptor.
+ *
+ * @return never null, always the same value for a given instance of {@link Descriptor}.
+ *
+ * @since TODO
+ */
+ public @NonNull UserPropertyCategory getUserPropertyCategory() {
+ // As this method is expected to be overloaded by subclasses
+ // the logic here is just done to support plugins with older core version
+ String categoryAsString = this.getUserPropertyCategoryAsString();
+ if (categoryAsString != null) {
+ Optional firstIfFound = UserPropertyCategory.all().stream()
+ .filter(cat -> {
+ Symbol symbolAnnotation = cat.getClass().getAnnotation(Symbol.class);
+ if (symbolAnnotation != null) {
+ for (String symbolValue : symbolAnnotation.value()) {
+ if (symbolValue.equalsIgnoreCase(categoryAsString)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ })
+ .findFirst();
+ if (firstIfFound.isPresent()) {
+ return firstIfFound.get();
+ }
+ }
+ return UserPropertyCategory.get(UserPropertyCategory.Unclassified.class);
+ }
+
+ /**
+ * Method proposed to prevent plugins to rely on too recent core version
+ * while keeping the possibility to use the categories.
+ *
+ * @deprecated This should only be used when the core requirement is below the version this method was added
+ *
+ * @return String name corresponding to the symbol of {@link #getUserPropertyCategory()}
+ *
+ * @since TODO
+ */
+ @Deprecated
+ protected @CheckForNull String getUserPropertyCategoryAsString() {
+ return null;
+ }
}
diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategory.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategory.java
new file mode 100644
index 000000000000..803b7e2d3527
--- /dev/null
+++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategory.java
@@ -0,0 +1,204 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2022, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.model.userproperty;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Extension;
+import hudson.ExtensionList;
+import hudson.ExtensionPoint;
+import hudson.model.ModelObject;
+import hudson.model.UserProperty;
+import hudson.model.UserPropertyDescriptor;
+import org.jenkinsci.Symbol;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.DoNotUse;
+
+/**
+ * Grouping of related {@link UserProperty}s.
+ *
+ *
+ * To facilitate the separation of the user properties into multiple pages, tabs, and so on,
+ * {@link UserProperty}s are classified into categories (such as "security", "preferences", as well
+ * as the catch-all "unclassified".) Categories themselves are extensible — plugins may introduce
+ * its own category as well, although that should only happen if you are creating a big enough subsystem.
+ *
+ * @since TODO
+ * @see UserProperty
+ */
+public abstract class UserPropertyCategory implements ExtensionPoint, ModelObject {
+ /**
+ * One-line plain text message that explains what this category is about.
+ * This can be used in the UI to help the user pick the right category.
+ *
+ * The text should be longer than {@link #getDisplayName()}
+ */
+ public abstract String getShortDescription();
+
+ /**
+ * Returns all the registered {@link UserPropertyCategory} descriptors.
+ */
+ public static ExtensionList all() {
+ return ExtensionList.lookup(UserPropertyCategory.class);
+ }
+
+ public static @NonNull T get(Class type) {
+ T category = all().get(type);
+ if (category == null) {
+ throw new AssertionError("Category not found. It seems the " + type + " is not annotated with @Extension and so not registered");
+ }
+ return category;
+ }
+
+ /**
+ * This category is used when the {@link hudson.model.UserPropertyDescriptor} has not implemented
+ * the {@link UserPropertyDescriptor#getUserPropertyCategory()} method
+ * (or the getUserPropertyCategoryAsString method for compatibility reason).
+ *
+ * If you do not know what to use, choose the {@link Account} instead of this one.
+ */
+ @Extension
+ @Symbol("unclassified")
+ @Restricted(DoNotUse.class)
+ public static class Unclassified extends UserPropertyCategory {
+ @Override
+ public String getDisplayName() {
+ return Messages.UserPropertyCategory_Unclassified_DisplayName();
+ }
+
+ @Override
+ public String getShortDescription() {
+ return Messages.UserPropertyCategory_Unclassified_ShortDescription();
+ }
+ }
+
+ /**
+ * User property related to account settings (e.g. timezone, email, ...).
+ *
+ * It could be seen as the default choice for {@link UserProperty} that are defining their category.
+ * Currently it has the same effect as {@link Unclassified} but the behavior could change in the future.
+ */
+ @Extension
+ @Symbol("account")
+ public static class Account extends UserPropertyCategory {
+ @Override
+ public String getDisplayName() {
+ return Messages.UserPropertyCategory_Account_DisplayName();
+ }
+
+ @Override
+ public String getShortDescription() {
+ return Messages.UserPropertyCategory_Account_ShortDescription();
+ }
+ }
+
+ /**
+ * Preferences related configurations (e.g. notification type, default view, ...).
+ */
+ @Extension
+ @Symbol("preferences")
+ public static class Preferences extends UserPropertyCategory {
+ @Override
+ public String getDisplayName() {
+ return Messages.UserPropertyCategory_Preferences_DisplayName();
+ }
+
+ @Override
+ public String getShortDescription() {
+ return Messages.UserPropertyCategory_Preferences_ShortDescription();
+ }
+ }
+
+ /**
+ * Per user feature flags (e.g. new design, ...).
+ */
+ @Extension
+ @Symbol("experimental")
+ public static class Experimental extends UserPropertyCategory {
+ @Override
+ public String getDisplayName() {
+ return Messages.UserPropertyCategory_Experimental_DisplayName();
+ }
+
+ @Override
+ public String getShortDescription() {
+ return Messages.UserPropertyCategory_Experimental_ShortDescription();
+ }
+ }
+
+ /**
+ * User interface related configurations (e.g. theme, language, ...).
+ *
+ * See also {@link jenkins.appearance.AppearanceCategory}.
+ */
+ @Extension
+ @Symbol("appearance")
+ public static class Appearance extends UserPropertyCategory {
+ @Override
+ public String getDisplayName() {
+ return Messages.UserPropertyCategory_Appearance_DisplayName();
+ }
+
+ @Override
+ public String getShortDescription() {
+ return Messages.UserPropertyCategory_Appearance_ShortDescription();
+ }
+ }
+
+
+ /**
+ * Security related configurations (e.g. API Token, SSH keys, ...).
+ * With this separation, we can more easily add control on their modifications.
+ */
+ @Extension
+ @Symbol("security")
+ public static class Security extends UserPropertyCategory {
+ @Override
+ public String getDisplayName() {
+ return Messages.UserPropertyCategory_Security_DisplayName();
+ }
+
+ @Override
+ public String getShortDescription() {
+ return Messages.UserPropertyCategory_Security_ShortDescription();
+ }
+ }
+
+ /**
+ * For user properties that are not expected to be displayed,
+ * typically automatically configured by automated behavior, without direct user interaction.
+ */
+ @Extension
+ @Symbol("invisible")
+ public static class Invisible extends UserPropertyCategory {
+ @Override
+ public String getDisplayName() {
+ return Messages.UserPropertyCategory_Invisible_DisplayName();
+ }
+
+ @Override
+ public String getShortDescription() {
+ return Messages.UserPropertyCategory_Invisible_ShortDescription();
+ }
+ }
+}
diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAccountAction.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAccountAction.java
new file mode 100644
index 000000000000..822cdc6f4c99
--- /dev/null
+++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAccountAction.java
@@ -0,0 +1,121 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2022, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.model.userproperty;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.DescriptorExtensionList;
+import hudson.Extension;
+import hudson.model.Action;
+import hudson.model.Descriptor;
+import hudson.model.TransientUserActionFactory;
+import hudson.model.User;
+import hudson.model.UserProperty;
+import hudson.model.UserPropertyDescriptor;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import javax.servlet.ServletException;
+import jenkins.model.Jenkins;
+import jenkins.security.UserDetailsCache;
+import net.sf.json.JSONObject;
+import org.jenkinsci.Symbol;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+import org.kohsuke.stapler.verb.POST;
+
+@Restricted(NoExternalUse.class)
+public class UserPropertyCategoryAccountAction extends UserPropertyCategoryAction implements Action {
+ public UserPropertyCategoryAccountAction(@NonNull User user) {
+ super(user);
+ }
+
+ @Override
+ public String getDisplayName() {
+ return Messages.UserPropertyCategoryAccountAction_DisplayName();
+ }
+
+ @Override
+ public String getIconFileName() {
+ return getTargetUser().hasPermission(Jenkins.ADMINISTER) ? "symbol-settings" : null;
+ }
+
+ @Override
+ public String getUrlName() {
+ return "account";
+ }
+
+ public @NonNull List getMyCategoryDescriptors() {
+ return allByTwoCategoryClasses(UserPropertyCategory.Unclassified.class, UserPropertyCategory.Account.class);
+ }
+
+ private static List allByTwoCategoryClasses(
+ @NonNull Class extends UserPropertyCategory> categoryClass1,
+ @NonNull Class extends UserPropertyCategory> categoryClass2
+ ) {
+ DescriptorExtensionList all = UserProperty.all();
+
+ List filteredList = new ArrayList<>(all.size());
+ for (UserPropertyDescriptor descriptor : all) {
+ Class extends UserPropertyCategory> currClass = descriptor.getUserPropertyCategory().getClass();
+ if (currClass.equals(categoryClass1) || currClass.equals(categoryClass2)) {
+ filteredList.add(descriptor);
+ }
+ }
+
+ return filteredList;
+ }
+
+ @POST
+ public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, Descriptor.FormException {
+ User targetUser = this.getTargetUser();
+ targetUser.checkPermission(Jenkins.ADMINISTER);
+
+ JSONObject json = req.getSubmittedForm();
+
+ String oldFullName = targetUser.getFullName();
+ targetUser.setFullName(json.getString("fullName"));
+ targetUser.setDescription(json.getString("description"));
+
+ super.doConfigSubmit(req, rsp);
+
+ if (!oldFullName.equals(targetUser.getFullName())) {
+ UserDetailsCache.get().invalidate(oldFullName);
+ }
+ }
+
+ /**
+ * Inject the outer class configuration page into the sidenav and the request routing of the user
+ */
+ @Extension(ordinal = 400)
+ @Symbol("account")
+ public static class AccountActionFactory extends TransientUserActionFactory {
+ public Collection extends Action> createFor(User target) {
+ return Collections.singleton(new UserPropertyCategoryAccountAction(target));
+ }
+ }
+}
diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAction.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAction.java
new file mode 100644
index 000000000000..caec7c1bdf88
--- /dev/null
+++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAction.java
@@ -0,0 +1,65 @@
+package hudson.model.userproperty;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.model.Descriptor;
+import hudson.model.User;
+import hudson.model.UserProperty;
+import hudson.model.UserPropertyDescriptor;
+import hudson.util.FormApply;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import javax.servlet.ServletException;
+import jenkins.model.Jenkins;
+import net.sf.json.JSONObject;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+import org.kohsuke.stapler.verb.POST;
+
+public abstract class UserPropertyCategoryAction {
+
+ private final User targetUser;
+
+ public UserPropertyCategoryAction(User targetUser) {
+ this.targetUser = targetUser;
+ }
+
+ public @NonNull User getTargetUser() {
+ return targetUser;
+ }
+
+ public @NonNull abstract List getMyCategoryDescriptors();
+
+ @POST
+ public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, Descriptor.FormException {
+ this.targetUser.checkPermission(Jenkins.ADMINISTER);
+
+ JSONObject json = req.getSubmittedForm();
+
+ List props = new ArrayList<>();
+ List myCategoryDescriptors = getMyCategoryDescriptors();
+ int i = 0;
+ for (UserPropertyDescriptor d : myCategoryDescriptors) {
+ UserProperty p = this.targetUser.getProperty(d.clazz);
+
+ JSONObject o = json.optJSONObject("userProperty" + i++);
+ if (o != null) {
+ if (p != null) {
+ p = p.reconfigure(req, o);
+ } else {
+ p = d.newInstance(req, o);
+ }
+ }
+
+ if (p != null) {
+ props.add(p);
+ }
+ }
+ this.targetUser.addProperties(props);
+
+ this.targetUser.save();
+
+ // we are in /user///, going to /user//
+ FormApply.success("..").generateResponse(req, rsp, this);
+ }
+}
diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAppearanceAction.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAppearanceAction.java
new file mode 100644
index 000000000000..88d08a8add70
--- /dev/null
+++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAppearanceAction.java
@@ -0,0 +1,76 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2022, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.model.userproperty;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Extension;
+import hudson.model.Action;
+import hudson.model.TransientUserActionFactory;
+import hudson.model.User;
+import hudson.model.UserProperty;
+import hudson.model.UserPropertyDescriptor;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import jenkins.model.Jenkins;
+import org.jenkinsci.Symbol;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+@Restricted(NoExternalUse.class)
+public class UserPropertyCategoryAppearanceAction extends UserPropertyCategoryAction implements Action {
+ public UserPropertyCategoryAppearanceAction(@NonNull User user) {
+ super(user);
+ }
+
+ @Override
+ public String getDisplayName() {
+ return Messages.UserPropertyCategoryAppearanceAction_DisplayName();
+ }
+
+ @Override
+ public String getIconFileName() {
+ return getTargetUser().hasPermission(Jenkins.ADMINISTER) ? "symbol-brush-outline" : null;
+ }
+
+ @Override
+ public String getUrlName() {
+ return "appearance";
+ }
+
+ public @NonNull List getMyCategoryDescriptors() {
+ return UserProperty.allByCategoryClass(UserPropertyCategory.Appearance.class);
+ }
+
+ /**
+ * Inject the outer class configuration page into the sidenav and the request routing of the user
+ */
+ @Extension(ordinal = 350)
+ @Symbol("appearance")
+ public static class AppearanceActionFactory extends TransientUserActionFactory {
+ public Collection extends Action> createFor(User target) {
+ return Collections.singleton(new UserPropertyCategoryAppearanceAction(target));
+ }
+ }
+}
diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryExperimentalAction.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryExperimentalAction.java
new file mode 100644
index 000000000000..bb242bacad7a
--- /dev/null
+++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryExperimentalAction.java
@@ -0,0 +1,76 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2022, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.model.userproperty;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Extension;
+import hudson.model.Action;
+import hudson.model.TransientUserActionFactory;
+import hudson.model.User;
+import hudson.model.UserProperty;
+import hudson.model.UserPropertyDescriptor;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import jenkins.model.Jenkins;
+import org.jenkinsci.Symbol;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+@Restricted(NoExternalUse.class)
+public class UserPropertyCategoryExperimentalAction extends UserPropertyCategoryAction implements Action {
+ public UserPropertyCategoryExperimentalAction(@NonNull User user) {
+ super(user);
+ }
+
+ @Override
+ public String getDisplayName() {
+ return Messages.UserPropertyCategoryExperimentalAction_DisplayName();
+ }
+
+ @Override
+ public String getIconFileName() {
+ return getTargetUser().hasPermission(Jenkins.ADMINISTER) ? "symbol-flask" : null;
+ }
+
+ @Override
+ public String getUrlName() {
+ return "experiments";
+ }
+
+ public @NonNull List getMyCategoryDescriptors() {
+ return UserProperty.allByCategoryClass(UserPropertyCategory.Experimental.class);
+ }
+
+ /**
+ * Inject the outer class configuration page into the sidenav and the request routing of the user
+ */
+ @Extension(ordinal = 100)
+ @Symbol("experimental")
+ public static class ExperimentalActionFactory extends TransientUserActionFactory {
+ public Collection extends Action> createFor(User target) {
+ return Collections.singleton(new UserPropertyCategoryExperimentalAction(target));
+ }
+ }
+}
diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryPreferencesAction.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryPreferencesAction.java
new file mode 100644
index 000000000000..7a74b702e6c0
--- /dev/null
+++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryPreferencesAction.java
@@ -0,0 +1,76 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2022, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.model.userproperty;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Extension;
+import hudson.model.Action;
+import hudson.model.TransientUserActionFactory;
+import hudson.model.User;
+import hudson.model.UserProperty;
+import hudson.model.UserPropertyDescriptor;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import jenkins.model.Jenkins;
+import org.jenkinsci.Symbol;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+@Restricted(NoExternalUse.class)
+public class UserPropertyCategoryPreferencesAction extends UserPropertyCategoryAction implements Action {
+ public UserPropertyCategoryPreferencesAction(@NonNull User user) {
+ super(user);
+ }
+
+ @Override
+ public String getDisplayName() {
+ return Messages.UserPropertyCategoryPreferencesAction_DisplayName();
+ }
+
+ @Override
+ public String getIconFileName() {
+ return getTargetUser().hasPermission(Jenkins.ADMINISTER) ? "symbol-parameters" : null;
+ }
+
+ @Override
+ public String getUrlName() {
+ return "preferences";
+ }
+
+ public @NonNull List getMyCategoryDescriptors() {
+ return UserProperty.allByCategoryClass(UserPropertyCategory.Preferences.class);
+ }
+
+ /**
+ * Inject the outer class configuration page into the sidenav and the request routing of the user
+ */
+ @Extension(ordinal = 300)
+ @Symbol("preferences")
+ public static class PreferencesActionFactory extends TransientUserActionFactory {
+ public Collection extends Action> createFor(User target) {
+ return Collections.singleton(new UserPropertyCategoryPreferencesAction(target));
+ }
+ }
+}
diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategorySecurityAction.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategorySecurityAction.java
new file mode 100644
index 000000000000..a6cb3e6ed3c4
--- /dev/null
+++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategorySecurityAction.java
@@ -0,0 +1,77 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2022, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.model.userproperty;
+
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Extension;
+import hudson.model.Action;
+import hudson.model.TransientUserActionFactory;
+import hudson.model.User;
+import hudson.model.UserProperty;
+import hudson.model.UserPropertyDescriptor;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import jenkins.model.Jenkins;
+import org.jenkinsci.Symbol;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+@Restricted(NoExternalUse.class)
+public class UserPropertyCategorySecurityAction extends UserPropertyCategoryAction implements Action {
+ public UserPropertyCategorySecurityAction(@NonNull User user) {
+ super(user);
+ }
+
+ @Override
+ public String getDisplayName() {
+ return Messages.UserPropertyCategorySecurityAction_DisplayName();
+ }
+
+ @Override
+ public String getIconFileName() {
+ return getTargetUser().hasPermission(Jenkins.ADMINISTER) ? "symbol-lock-closed" : null;
+ }
+
+ @Override
+ public String getUrlName() {
+ return "security";
+ }
+
+ public @NonNull List getMyCategoryDescriptors() {
+ return UserProperty.allByCategoryClass(UserPropertyCategory.Security.class);
+ }
+
+ /**
+ * Inject the outer class configuration page into the sidenav and the request routing of the user
+ */
+ @Extension(ordinal = 200)
+ @Symbol("security")
+ public static class SecurityActionFactory extends TransientUserActionFactory {
+ public Collection extends Action> createFor(User target) {
+ return Collections.singleton(new UserPropertyCategorySecurityAction(target));
+ }
+ }
+}
diff --git a/core/src/main/java/hudson/search/UserSearchProperty.java b/core/src/main/java/hudson/search/UserSearchProperty.java
index 7e9836944c0b..a3515dd08874 100644
--- a/core/src/main/java/hudson/search/UserSearchProperty.java
+++ b/core/src/main/java/hudson/search/UserSearchProperty.java
@@ -5,6 +5,7 @@
import hudson.model.User;
import hudson.model.UserProperty;
import hudson.model.UserPropertyDescriptor;
+import hudson.model.userproperty.UserPropertyCategory;
import net.sf.json.JSONObject;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.StaplerRequest;
@@ -54,6 +55,10 @@ public UserProperty newInstance(StaplerRequest req, JSONObject formData) throws
return new UserSearchProperty(formData.optBoolean("insensitiveSearch"));
}
+ @Override
+ public @NonNull UserPropertyCategory getUserPropertyCategory() {
+ return UserPropertyCategory.get(UserPropertyCategory.Preferences.class);
+ }
}
}
diff --git a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
index 53b798d941db..bd122244c7e2 100644
--- a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
+++ b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
@@ -39,6 +39,7 @@
import hudson.model.User;
import hudson.model.UserProperty;
import hudson.model.UserPropertyDescriptor;
+import hudson.model.userproperty.UserPropertyCategory;
import hudson.security.FederatedLoginService.FederatedIdentity;
import hudson.security.captcha.CaptchaSupport;
import hudson.util.FormValidation;
@@ -801,7 +802,6 @@ public boolean equals(Object o) {
public int hashCode() {
return getUsername().hashCode();
}
-
}
public static class ConverterImpl extends XStream2.PassthruConverter {
@@ -884,6 +884,11 @@ public boolean isEnabled() {
public UserProperty newInstance(User user) {
return null;
}
+
+ @Override
+ public @NonNull UserPropertyCategory getUserPropertyCategory() {
+ return UserPropertyCategory.get(UserPropertyCategory.Security.class);
+ }
}
}
diff --git a/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java b/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java
index 21cc0ba1df0c..5b94090f4e3b 100644
--- a/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java
+++ b/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java
@@ -139,7 +139,7 @@ private Collection getAllActiveAdministrativeMonitors() {
* @return the list of active monitors if we should display them, otherwise null.
*/
public Collection getMonitorsToDisplay() {
- if (!Jenkins.get().hasPermission(Jenkins.SYSTEM_READ)) {
+ if (!(AdministrativeMonitor.hasPermissionToDisplay())) {
return null;
}
diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java
index 4782e4b321f2..a3376f99d75e 100644
--- a/core/src/main/java/jenkins/model/Jenkins.java
+++ b/core/src/main/java/jenkins/model/Jenkins.java
@@ -2355,12 +2355,12 @@ public AdministrativeMonitor getAdministrativeMonitor(String id) {
* @since 2.64
*/
public List getActiveAdministrativeMonitors() {
- if (!Jenkins.get().hasPermission(SYSTEM_READ)) {
+ if (!AdministrativeMonitor.hasPermissionToDisplay()) {
return Collections.emptyList();
}
return administrativeMonitors.stream().filter(m -> {
try {
- return Jenkins.get().hasPermission(m.getRequiredPermission()) && m.isEnabled() && m.isActivated();
+ return m.hasRequiredPermission() && m.isEnabled() && m.isActivated();
} catch (Throwable x) {
LOGGER.log(Level.WARNING, null, x);
return false;
diff --git a/core/src/main/java/jenkins/model/experimentalflags/UserExperimentalFlagsProperty.java b/core/src/main/java/jenkins/model/experimentalflags/UserExperimentalFlagsProperty.java
index 6732f2e5d696..85332d26973c 100644
--- a/core/src/main/java/jenkins/model/experimentalflags/UserExperimentalFlagsProperty.java
+++ b/core/src/main/java/jenkins/model/experimentalflags/UserExperimentalFlagsProperty.java
@@ -31,6 +31,7 @@
import hudson.model.User;
import hudson.model.UserProperty;
import hudson.model.UserPropertyDescriptor;
+import hudson.model.userproperty.UserPropertyCategory;
import java.util.HashMap;
import java.util.Map;
import net.sf.json.JSONObject;
@@ -76,13 +77,19 @@ public static final class DescriptorImpl extends UserPropertyDescriptor {
public UserProperty newInstance(@Nullable StaplerRequest req, @NonNull JSONObject formData) throws FormException {
JSONObject flagsObj = formData.getJSONObject("flags");
Map flags = new HashMap<>();
- for (Object key : flagsObj.keySet()) {
- String value = (String) flagsObj.get((String) key);
+ for (String key : flagsObj.keySet()) {
+ String value = (String) flagsObj.get(key);
if (!value.isEmpty()) {
- flags.put((String) key, value);
+ flags.put(key, value);
}
}
return new UserExperimentalFlagsProperty(flags);
}
+
+ @NonNull
+ @Override
+ public UserPropertyCategory getUserPropertyCategory() {
+ return UserPropertyCategory.get(UserPropertyCategory.Experimental.class);
+ }
}
}
diff --git a/core/src/main/java/jenkins/security/ApiTokenProperty.java b/core/src/main/java/jenkins/security/ApiTokenProperty.java
index 69904a0e4785..464fdcbdf16c 100644
--- a/core/src/main/java/jenkins/security/ApiTokenProperty.java
+++ b/core/src/main/java/jenkins/security/ApiTokenProperty.java
@@ -33,6 +33,7 @@
import hudson.model.User;
import hudson.model.UserProperty;
import hudson.model.UserPropertyDescriptor;
+import hudson.model.userproperty.UserPropertyCategory;
import hudson.security.ACL;
import hudson.util.HttpResponses;
import hudson.util.Secret;
@@ -657,6 +658,11 @@ public HttpResponse doRevokeAllExcept(@AncestorInPath User u,
return HttpResponses.ok();
}
+
+ @Override
+ public @NonNull UserPropertyCategory getUserPropertyCategory() {
+ return UserPropertyCategory.get(UserPropertyCategory.Security.class);
+ }
}
/**
diff --git a/core/src/main/java/jenkins/security/LastGrantedAuthoritiesProperty.java b/core/src/main/java/jenkins/security/LastGrantedAuthoritiesProperty.java
index 52f74d452ffa..cb8b8accf295 100644
--- a/core/src/main/java/jenkins/security/LastGrantedAuthoritiesProperty.java
+++ b/core/src/main/java/jenkins/security/LastGrantedAuthoritiesProperty.java
@@ -6,6 +6,7 @@
import hudson.model.User;
import hudson.model.UserProperty;
import hudson.model.UserPropertyDescriptor;
+import hudson.model.userproperty.UserPropertyCategory;
import hudson.security.SecurityRealm;
import java.io.IOException;
import java.util.ArrayList;
@@ -171,6 +172,11 @@ public boolean isEnabled() {
public UserProperty newInstance(User user) {
return null;
}
+
+ @Override
+ public @NonNull UserPropertyCategory getUserPropertyCategory() {
+ return UserPropertyCategory.get(UserPropertyCategory.Invisible.class);
+ }
}
private static final Logger LOGGER = Logger.getLogger(LastGrantedAuthoritiesProperty.class.getName());
diff --git a/core/src/main/java/jenkins/security/seed/UserSeedProperty.java b/core/src/main/java/jenkins/security/seed/UserSeedProperty.java
index d7420910cb64..968ee9320f58 100644
--- a/core/src/main/java/jenkins/security/seed/UserSeedProperty.java
+++ b/core/src/main/java/jenkins/security/seed/UserSeedProperty.java
@@ -31,6 +31,7 @@
import hudson.model.User;
import hudson.model.UserProperty;
import hudson.model.UserPropertyDescriptor;
+import hudson.model.userproperty.UserPropertyCategory;
import hudson.util.HttpResponses;
import java.io.IOException;
import java.security.SecureRandom;
@@ -153,5 +154,10 @@ public synchronized HttpResponse doRenewSessionSeed(@AncestorInPath @NonNull Use
public boolean isEnabled() {
return !DISABLE_USER_SEED && !HIDE_USER_SEED_SECTION;
}
+
+ @Override
+ public @NonNull UserPropertyCategory getUserPropertyCategory() {
+ return UserPropertyCategory.get(UserPropertyCategory.Security.class);
+ }
}
}
diff --git a/core/src/main/resources/hudson/model/Messages.properties b/core/src/main/resources/hudson/model/Messages.properties
index 9cd382511856..d56c57ff5ee0 100644
--- a/core/src/main/resources/hudson/model/Messages.properties
+++ b/core/src/main/resources/hudson/model/Messages.properties
@@ -422,3 +422,5 @@ ManagementLink.Category.MISC=Other
ManagementLink.Category.UNCATEGORIZED=Uncategorized
FileParameterValue.IndexTitle=File Parameters
+
+UserPreferencesProperty.DisplayName=Preferences
diff --git a/core/src/main/resources/hudson/model/User/configure.jelly b/core/src/main/resources/hudson/model/User/configure.jelly
deleted file mode 100644
index 9b4100248559..000000000000
--- a/core/src/main/resources/hudson/model/User/configure.jelly
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/core/src/main/resources/hudson/model/User/sidepanel.jelly b/core/src/main/resources/hudson/model/User/sidepanel.jelly
index 834061995daf..f8024b0c21b8 100644
--- a/core/src/main/resources/hudson/model/User/sidepanel.jelly
+++ b/core/src/main/resources/hudson/model/User/sidepanel.jelly
@@ -33,7 +33,6 @@ THE SOFTWARE.
-
diff --git a/core/src/main/resources/hudson/model/userproperty/Messages.properties b/core/src/main/resources/hudson/model/userproperty/Messages.properties
new file mode 100644
index 000000000000..5e52b8d2906c
--- /dev/null
+++ b/core/src/main/resources/hudson/model/userproperty/Messages.properties
@@ -0,0 +1,48 @@
+# The MIT License
+#
+# Copyright (c) 2022, 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.
+
+UserPropertyCategory.Unclassified.DisplayName=Unclassified
+UserPropertyCategory.Unclassified.ShortDescription=User properties without a category yet
+
+UserPropertyCategory.Account.DisplayName=Account
+UserPropertyCategory.Account.ShortDescription=User properties related to the user account configuration (e.g. timezone, email, ...)
+
+UserPropertyCategory.Preferences.DisplayName=Preferences
+UserPropertyCategory.Preferences.ShortDescription=User properties related to the user preferences (e.g. notification type, default view, ...)
+
+UserPropertyCategory.Experimental.DisplayName=Experiments
+UserPropertyCategory.Experimental.ShortDescription=Per user flags to enable/disable experimental features (e.g. new design, ...)
+
+UserPropertyCategory.Appearance.DisplayName=Appearance
+UserPropertyCategory.Appearance.ShortDescription=User properties related to the appearance of Jenkins
+
+UserPropertyCategory.Security.DisplayName=Security
+UserPropertyCategory.Security.ShortDescription=User properties related to the security of the user account (e.g. API token, SSH keys, ...)
+
+UserPropertyCategory.Invisible.DisplayName=Invisible
+UserPropertyCategory.Invisible.ShortDescription=User properties that do not require a configuration page
+
+UserPropertyCategoryAccountAction.DisplayName=Account
+UserPropertyCategoryAppearanceAction.DisplayName=Appearance
+UserPropertyCategoryExperimentalAction.DisplayName=Experiments
+UserPropertyCategoryPreferencesAction.DisplayName=Preferences
+UserPropertyCategorySecurityAction.DisplayName=Security
diff --git a/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryAccountAction/index.jelly b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryAccountAction/index.jelly
new file mode 100644
index 000000000000..8d2a8e9368e7
--- /dev/null
+++ b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryAccountAction/index.jelly
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryAppearanceAction/index.properties b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryAppearanceAction/index.properties
new file mode 100644
index 000000000000..f24a3f6e3851
--- /dev/null
+++ b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryAppearanceAction/index.properties
@@ -0,0 +1,24 @@
+# The MIT License
+#
+# Copyright (c) 2022, 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.
+
+title=Appearance
+warningNoItems=No appearance items are available for configuration.
diff --git a/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryExperimentalAction/index.jelly b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryExperimentalAction/index.jelly
new file mode 100644
index 000000000000..0d90ad4b2d76
--- /dev/null
+++ b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryExperimentalAction/index.jelly
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+ ${%title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${%warningNoItems}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryExperimentalAction/index.properties b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryExperimentalAction/index.properties
new file mode 100644
index 000000000000..4e5e6b02073d
--- /dev/null
+++ b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryExperimentalAction/index.properties
@@ -0,0 +1,24 @@
+# The MIT License
+#
+# Copyright (c) 2022, 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.
+
+title=Experiments
+warningNoItems=No experiments are currently available.
diff --git a/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryPreferencesAction/index.jelly b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryPreferencesAction/index.jelly
new file mode 100644
index 000000000000..6971e5abf8d0
--- /dev/null
+++ b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryPreferencesAction/index.jelly
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+ ${%title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${%warningNoItems}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/resources/jenkins/model/experimentalflags/Messages_fr.properties b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryPreferencesAction/index.properties
similarity index 94%
rename from core/src/main/resources/jenkins/model/experimentalflags/Messages_fr.properties
rename to core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryPreferencesAction/index.properties
index 8e48740af070..89bd19371237 100644
--- a/core/src/main/resources/jenkins/model/experimentalflags/Messages_fr.properties
+++ b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategoryPreferencesAction/index.properties
@@ -20,4 +20,5 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-UserExperimentalFlagsProperty.DisplayName=Expérimentations
+title=Preferences
+warningNoItems=No preferences available.
diff --git a/core/src/main/resources/hudson/model/userproperty/UserPropertyCategorySecurityAction/index.jelly b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategorySecurityAction/index.jelly
new file mode 100644
index 000000000000..0d90501fbbb0
--- /dev/null
+++ b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategorySecurityAction/index.jelly
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+ ${%title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${%warningNoItems}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/resources/hudson/model/userproperty/UserPropertyCategorySecurityAction/index.properties b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategorySecurityAction/index.properties
new file mode 100644
index 000000000000..be9c8e93e5f2
--- /dev/null
+++ b/core/src/main/resources/hudson/model/userproperty/UserPropertyCategorySecurityAction/index.properties
@@ -0,0 +1,24 @@
+# The MIT License
+#
+# Copyright (c) 2022, 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.
+
+title=Security
+warningNoItems=No security configuration available.
diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/index.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/index.jelly
index 88939feeaca3..559e2370a720 100644
--- a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/index.jelly
+++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/index.jelly
@@ -63,7 +63,7 @@ THE SOFTWARE.
diff --git a/core/src/main/resources/jenkins/model/experimentalflags/Messages.properties b/core/src/main/resources/jenkins/model/experimentalflags/Messages.properties
index ddf8f546e32b..13e00e615459 100644
--- a/core/src/main/resources/jenkins/model/experimentalflags/Messages.properties
+++ b/core/src/main/resources/jenkins/model/experimentalflags/Messages.properties
@@ -20,4 +20,4 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-UserExperimentalFlagsProperty.DisplayName=Experiments
+UserExperimentalFlagsProperty.DisplayName=Feature preview
diff --git a/core/src/main/resources/jenkins/model/experimentalflags/UserExperimentalFlagsProperty/config.jelly b/core/src/main/resources/jenkins/model/experimentalflags/UserExperimentalFlagsProperty/config.jelly
index 4fe6097089bb..a70fddf3403e 100644
--- a/core/src/main/resources/jenkins/model/experimentalflags/UserExperimentalFlagsProperty/config.jelly
+++ b/core/src/main/resources/jenkins/model/experimentalflags/UserExperimentalFlagsProperty/config.jelly
@@ -40,7 +40,7 @@ THE SOFTWARE.