Skip to content

Commit 6f6b28b

Browse files
committed
Make dismissal of admin monitors on a per-user basis
fixes jenkinsci#18417
1 parent 7d78a7b commit 6f6b28b

File tree

8 files changed

+227
-39
lines changed

8 files changed

+227
-39
lines changed

core/src/main/java/hudson/model/AdministrativeMonitor.java

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.io.IOException;
3434
import java.util.Set;
3535
import jenkins.model.Jenkins;
36+
import jenkins.user.AdministrativeMonitorsProperty;
3637
import org.kohsuke.accmod.Restricted;
3738
import org.kohsuke.accmod.restrictions.NoExternalUse;
3839
import org.kohsuke.stapler.StaplerProxy;
@@ -124,27 +125,48 @@ public final String getSearchUrl() {
124125

125126
/**
126127
* Mark this monitor as disabled, to prevent this from showing up in the UI.
128+
*
129+
* <p>
130+
* This is a per-user setting if security is enabled.
131+
* </p>
127132
*/
128133
public void disable(boolean value) throws IOException {
129134
AbstractCIBase jenkins = Jenkins.get();
130-
Set<String> set = jenkins.getDisabledAdministrativeMonitors();
131-
if (value) {
132-
set.add(id);
135+
User user = User.current();
136+
if (user != null) {
137+
AdministrativeMonitorsProperty property = AdministrativeMonitorsProperty.get(user);
138+
property.disableMonitor(id, value);
133139
} else {
134-
set.remove(id);
140+
Set<String> set = jenkins.getDisabledAdministrativeMonitors();
141+
if (value) {
142+
set.add(id);
143+
} else {
144+
set.remove(id);
145+
}
146+
jenkins.setDisabledAdministrativeMonitors(set);
147+
jenkins.save();
135148
}
136-
jenkins.setDisabledAdministrativeMonitors(set);
137-
jenkins.save();
138149
}
139150

140151
/**
141152
* Returns true if this monitor {@link #disable(boolean) isn't disabled} earlier.
142153
*
143154
* <p>
144-
* This flag implements the ability for the admin to say "no thank you" to the monitor that
155+
* This flag implements the ability for the admin to say "no, thank you" to the monitor that
145156
* he wants to ignore.
157+
* </p>
158+
* <p>
159+
* This is a per-user setting if security is enabled. This means that it should not be used to decide if some
160+
* check should be performed or not.
161+
* If such behavior is desired, implementers should provide their own configuration mechanism.
162+
* </p>
146163
*/
147164
public boolean isEnabled() {
165+
User user = User.current();
166+
if (user != null) {
167+
AdministrativeMonitorsProperty property = AdministrativeMonitorsProperty.get(user);
168+
return property.isMonitorEnabled(id);
169+
}
148170
return !Jenkins.get().getDisabledAdministrativeMonitors().contains(id);
149171
}
150172

@@ -179,7 +201,7 @@ public boolean isSecurity() {
179201
*/
180202
@RequirePOST
181203
public void doDisable(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException {
182-
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
204+
checkRequiredPermission();
183205
disable(true);
184206
rsp.sendRedirect2(req.getContextPath() + "/manage");
185207
}

core/src/main/java/hudson/util/DoubleLaunchChecker.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ protected void execute() {
9595
File timestampFile = new File(home, ".owner");
9696

9797
long t = timestampFile.lastModified();
98-
if (t != 0 && lastWriteTime != 0 && t != lastWriteTime && isEnabled()) {
98+
if (t != 0 && lastWriteTime != 0 && t != lastWriteTime) {
9999
try {
100100
collidingId = Files.readString(Util.fileToPath(timestampFile), Charset.defaultCharset());
101101
} catch (IOException e) {
@@ -120,7 +120,7 @@ protected void execute() {
120120
/**
121121
* Figures out a string that identifies this instance of Hudson.
122122
*/
123-
public String getId() {
123+
private String getId() {
124124
return Long.toString(ProcessHandle.current().pid());
125125
}
126126

core/src/main/java/jenkins/management/AdministrativeMonitorsConfiguration.java

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@
2626

2727
import hudson.Extension;
2828
import hudson.model.AdministrativeMonitor;
29-
import java.io.IOException;
30-
import java.util.logging.Level;
29+
import java.util.Set;
3130
import java.util.logging.Logger;
3231
import jenkins.model.GlobalConfiguration;
32+
import jenkins.model.Jenkins;
3333
import net.sf.json.JSONArray;
3434
import net.sf.json.JSONObject;
3535
import org.kohsuke.accmod.Restricted;
@@ -39,21 +39,29 @@
3939
@Extension
4040
@Restricted(NoExternalUse.class)
4141
public class AdministrativeMonitorsConfiguration extends GlobalConfiguration {
42+
43+
public boolean isMonitorEnabled(String id) {
44+
return !Jenkins.get().getDisabledAdministrativeMonitors().contains(id);
45+
}
46+
4247
@Override
4348
public boolean configure(StaplerRequest2 req, JSONObject json) throws FormException {
4449
JSONArray monitors = json.optJSONArray("administrativeMonitor");
50+
Jenkins jenkins = Jenkins.get();
4551
for (AdministrativeMonitor am : AdministrativeMonitor.all()) {
46-
try {
47-
boolean disable;
48-
if (monitors != null) {
49-
disable = !monitors.contains(am.id);
50-
} else {
51-
disable = !am.id.equals(json.optString("administrativeMonitor"));
52-
}
53-
am.disable(disable);
54-
} catch (IOException e) {
55-
LOGGER.log(Level.WARNING, "Failed to process form submission for " + am.id, e);
52+
boolean disable;
53+
if (monitors != null) {
54+
disable = !monitors.contains(am.id);
55+
} else {
56+
disable = !am.id.equals(json.optString("administrativeMonitor"));
57+
}
58+
Set<String> set = jenkins.getDisabledAdministrativeMonitors();
59+
if (disable) {
60+
set.add(am.id);
61+
} else {
62+
set.remove(am.id);
5663
}
64+
jenkins.setDisabledAdministrativeMonitors(set);
5765
}
5866
return true;
5967
}

core/src/main/java/jenkins/monitor/OperatingSystemEndOfLifeAdminMonitor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public OperatingSystemEndOfLifeAdminMonitor() throws IOException {
9191
}
9292

9393
private void fillOperatingSystemList() throws IOException {
94-
if (Jenkins.getInstanceOrNull() != null && !isEnabled()) {
94+
if (Jenkins.getInstanceOrNull() != null) {
9595
/* If not enabled, do not read the data files or perform any checks */
9696
LOGGER.log(Level.FINEST, "Operating system end of life monitor is not enabled, reading no data");
9797
return;
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package jenkins.user;
2+
3+
import edu.umd.cs.findbugs.annotations.NonNull;
4+
import hudson.Extension;
5+
import hudson.model.AdministrativeMonitor;
6+
import hudson.model.Descriptor;
7+
import hudson.model.User;
8+
import hudson.model.UserProperty;
9+
import hudson.model.UserPropertyDescriptor;
10+
import hudson.model.userproperty.UserPropertyCategory;
11+
import java.io.IOException;
12+
import java.util.ArrayList;
13+
import java.util.HashSet;
14+
import java.util.List;
15+
import java.util.Set;
16+
import java.util.logging.Level;
17+
import java.util.logging.Logger;
18+
import net.sf.json.JSONArray;
19+
import net.sf.json.JSONObject;
20+
import org.jenkinsci.Symbol;
21+
import org.kohsuke.accmod.Restricted;
22+
import org.kohsuke.accmod.restrictions.NoExternalUse;
23+
import org.kohsuke.stapler.DataBoundConstructor;
24+
import org.kohsuke.stapler.StaplerRequest2;
25+
26+
/**
27+
* User property to track which administrative monitors a user has dismissed.
28+
*
29+
* @since TODO
30+
*/
31+
@Restricted(NoExternalUse.class)
32+
public class AdministrativeMonitorsProperty extends UserProperty {
33+
34+
private static final Logger LOGGER = Logger.getLogger(AdministrativeMonitorsProperty.class.getName());
35+
private final Set<String> dismissedMonitors = new HashSet<>();
36+
37+
@DataBoundConstructor
38+
public AdministrativeMonitorsProperty() {
39+
}
40+
41+
@Override
42+
public UserProperty reconfigure(StaplerRequest2 req, JSONObject json) throws Descriptor.FormException {
43+
JSONArray monitors = json.optJSONArray("administrativeMonitor");
44+
synchronized (dismissedMonitors) {
45+
for (AdministrativeMonitor am : AdministrativeMonitor.all()) {
46+
boolean disable;
47+
if (monitors != null) {
48+
disable = !monitors.contains(am.id);
49+
} else {
50+
disable = !am.id.equals(json.optString("administrativeMonitor"));
51+
}
52+
if (disable) {
53+
dismissedMonitors.add(am.id);
54+
} else {
55+
dismissedMonitors.remove(am.id);
56+
}
57+
}
58+
}
59+
return this;
60+
}
61+
62+
public boolean isMonitorEnabled(String monitorId) {
63+
synchronized (dismissedMonitors) {
64+
return !dismissedMonitors.contains(monitorId);
65+
}
66+
}
67+
68+
public void disableMonitor(String monitorId, boolean enabled) throws IOException {
69+
synchronized (dismissedMonitors) {
70+
if (enabled) {
71+
dismissedMonitors.add(monitorId);
72+
} else {
73+
dismissedMonitors.remove(monitorId);
74+
}
75+
}
76+
user.save();
77+
}
78+
79+
@NonNull
80+
public static AdministrativeMonitorsProperty get(@NonNull User user) {
81+
AdministrativeMonitorsProperty property = user.getProperty(AdministrativeMonitorsProperty.class);
82+
if (property == null) {
83+
property = new AdministrativeMonitorsProperty();
84+
try {
85+
user.addProperty(property);
86+
} catch (IOException e) {
87+
LOGGER.log(Level.WARNING, "Failed to save user " + user.getId() + " after adding AdministrativeMonitorsProperty", e);
88+
}
89+
}
90+
return property;
91+
}
92+
93+
public List<AdministrativeMonitor> getMonitors() {
94+
synchronized (dismissedMonitors) {
95+
List<AdministrativeMonitor> monitors = new ArrayList<>(AdministrativeMonitor.all());
96+
monitors.sort((m1, m2) -> m1.getDisplayName().compareTo(m2.getDisplayName()));
97+
return monitors;
98+
}
99+
}
100+
101+
@Extension
102+
@Symbol("administrativeMonitors")
103+
public static final class DescriptorImpl extends UserPropertyDescriptor {
104+
105+
@NonNull
106+
@Override
107+
public String getDisplayName() {
108+
return "Administrative Monitors";
109+
}
110+
111+
@Override
112+
public UserProperty newInstance(User user) {
113+
return new AdministrativeMonitorsProperty();
114+
}
115+
116+
@NonNull
117+
@Override
118+
public UserPropertyCategory getUserPropertyCategory() {
119+
User user = User.current();
120+
if (user != null) {
121+
if (!AdministrativeMonitor.hasPermissionToDisplay()) {
122+
return UserPropertyCategory.get(UserPropertyCategory.Invisible.class);
123+
}
124+
}
125+
return UserPropertyCategory.get(UserPropertyCategory.Preferences.class);
126+
}
127+
128+
}
129+
}

core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config.groovy

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,25 @@ import hudson.model.AdministrativeMonitor
2929
f = namespace(lib.FormTagLib)
3030
st = namespace("jelly:stapler")
3131

32-
f.section(title: _("Administrative monitors"), description: _("blurb")) {
33-
f.advanced(title: _("Administrative monitors")) {
34-
f.entry() {
35-
for (AdministrativeMonitor am : new ArrayList<>(AdministrativeMonitor.all())
36-
.sort({ o1, o2 -> o1.getDisplayName() <=> o2.getDisplayName() })) {
37-
div(style: "margin-bottom: 0.625rem") {
38-
div(class: "jenkins-checkbox-help-wrapper") {
39-
f.checkbox(name: "administrativeMonitor",
40-
title: am.displayName,
41-
checked: am.enabled,
42-
json: am.id)
43-
if (am.isSecurity()) {
44-
span(style: 'margin-left: 0.5rem', class: 'jenkins-badge', _("Security"))
32+
div(class: "${app.useSecurity ? 'jenkins-hidden' : null}") {
33+
f.section(title: _("Administrative monitors"), description: _("blurb")) {
34+
f.advanced(title: _("Administrative monitors")) {
35+
f.entry() {
36+
for (AdministrativeMonitor am : new ArrayList<>(AdministrativeMonitor.all())
37+
.sort({ o1, o2 -> o1.getDisplayName() <=> o2.getDisplayName() })) {
38+
div(style: "margin-bottom: 0.625rem") {
39+
div(class: "jenkins-checkbox-help-wrapper") {
40+
f.checkbox(name: "administrativeMonitor",
41+
title: am.displayName,
42+
checked: instance.isMonitorEnabled(am.id),
43+
json: am.id)
44+
if (am.isSecurity()) {
45+
span(style: 'margin-left: 0.5rem', class: 'jenkins-badge', _("Security"))
46+
}
47+
}
48+
div(class: "jenkins-checkbox__description") {
49+
st.include(it: am, page: "description", optional: true)
4550
}
46-
}
47-
div(class: "jenkins-checkbox__description") {
48-
st.include(it: am, page: "description", optional: true)
4951
}
5052
}
5153
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form">
3+
<div class="jenkins-section__description">${%blurb}</div>
4+
<f:advanced title="${%Administrative monitors}">
5+
<f:entry>
6+
<j:forEach var="am" items="${instance.monitors}">
7+
<div class="jenkins-!-margin-bottom-1">
8+
<div class="jenkins-checkbox-help-wrapper">
9+
<j:set var="readOnlyMode" value="${!am.hasRequiredPermission()}"/>
10+
<f:checkbox name="administrativeMonitor" title="${am.displayName}" json="${am.id}" checked="${instance.isMonitorEnabled(am.id)}"/>
11+
<j:if test="${am.security}">
12+
<span style="margin-left: 0.5rem" class="jenkins-badge">${%Security}</span>
13+
</j:if>
14+
</div>
15+
<div class="jenkins-checkbox__description">
16+
<st:include it="${am}" page="description" optional="true"/>
17+
</div>
18+
</div>
19+
</j:forEach>
20+
</f:entry>
21+
</f:advanced>
22+
</j:jelly>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
blurb = Administrative monitors are warnings shown to Jenkins administrators \
2+
about the state of the Jenkins instance. It is generally strongly \
3+
recommended to keep all administrative monitors enabled, but if you are \
4+
not interested in specific warnings, uncheck them here to permanently \
5+
hide them.

0 commit comments

Comments
 (0)