Skip to content

Commit 6c50589

Browse files
authored
Add telemetry for password field masking (jenkinsci#26195)
* Add telemetry for password field masking * Collect actionable info * Always send data and clear collection
1 parent 02c84fa commit 6c50589

File tree

3 files changed

+159
-0
lines changed

3 files changed

+159
-0
lines changed

core/src/main/java/hudson/Functions.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
import jenkins.model.details.Detail;
171171
import jenkins.model.details.DetailFactory;
172172
import jenkins.model.details.DetailGroup;
173+
import jenkins.telemetry.impl.PasswordMasking;
173174
import jenkins.util.SystemProperties;
174175
import org.apache.commons.jelly.JellyContext;
175176
import org.apache.commons.jelly.JellyTagException;
@@ -2256,32 +2257,64 @@ public String getPasswordValue(Object o) {
22562257
if (o instanceof Secret || Secret.BLANK_NONSECRET_PASSWORD_FIELDS_WITHOUT_ITEM_CONFIGURE) {
22572258
if (req != null) {
22582259
if (NON_RECURSIVE_PASSWORD_MASKING_PERMISSION_CHECK) {
2260+
List<Ancestor> ancestors = req.getAncestors();
2261+
String closestAncestor = ancestors.isEmpty() ? "unknown" :
2262+
ancestors.getLast().getObject().getClass().getName();
2263+
22592264
Item item = req.findAncestorObject(Item.class);
22602265
if (item != null && !item.hasPermission(Item.CONFIGURE)) {
2266+
PasswordMasking.recordMasking(
2267+
item.getClass().getName(),
2268+
closestAncestor,
2269+
getJellyViewsInformationForCurrentRequest()
2270+
);
22612271
return "********";
22622272
}
22632273
Computer computer = req.findAncestorObject(Computer.class);
22642274
if (computer != null && !computer.hasPermission(Computer.CONFIGURE)) {
2275+
PasswordMasking.recordMasking(
2276+
computer.getClass().getName(),
2277+
closestAncestor,
2278+
getJellyViewsInformationForCurrentRequest()
2279+
);
22652280
return "********";
22662281
}
22672282
} else {
22682283
List<Ancestor> ancestors = req.getAncestors();
2284+
String closestAncestor = ancestors.isEmpty() ? "unknown" :
2285+
ancestors.getLast().getObject().getClass().getName();
2286+
22692287
for (Ancestor ancestor : Iterators.reverse(ancestors)) {
22702288
Object type = ancestor.getObject();
22712289
if (type instanceof Item item) {
22722290
if (!item.hasPermission(Item.CONFIGURE)) {
2291+
PasswordMasking.recordMasking(
2292+
item.getClass().getName(),
2293+
closestAncestor,
2294+
getJellyViewsInformationForCurrentRequest()
2295+
);
22732296
return "********";
22742297
}
22752298
break;
22762299
}
22772300
if (type instanceof Computer computer) {
22782301
if (!computer.hasPermission(Computer.CONFIGURE)) {
2302+
PasswordMasking.recordMasking(
2303+
computer.getClass().getName(),
2304+
closestAncestor,
2305+
getJellyViewsInformationForCurrentRequest()
2306+
);
22792307
return "********";
22802308
}
22812309
break;
22822310
}
22832311
if (type instanceof View view) {
22842312
if (!view.hasPermission(View.CONFIGURE)) {
2313+
PasswordMasking.recordMasking(
2314+
view.getClass().getName(),
2315+
closestAncestor,
2316+
getJellyViewsInformationForCurrentRequest()
2317+
);
22852318
return "********";
22862319
}
22872320
break;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2026, CloudBees, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package jenkins.telemetry.impl;
26+
27+
import edu.umd.cs.findbugs.annotations.NonNull;
28+
import hudson.Extension;
29+
import java.time.LocalDate;
30+
import java.util.Map;
31+
import java.util.concurrent.ConcurrentHashMap;
32+
import java.util.concurrent.atomic.AtomicLong;
33+
import jenkins.telemetry.Telemetry;
34+
import net.sf.json.JSONArray;
35+
import net.sf.json.JSONObject;
36+
import org.kohsuke.accmod.Restricted;
37+
import org.kohsuke.accmod.restrictions.NoExternalUse;
38+
39+
/**
40+
* Telemetry implementation gathering information about password field masking.
41+
*/
42+
@Extension
43+
@Restricted(NoExternalUse.class)
44+
public class PasswordMasking extends Telemetry {
45+
46+
private static final Map<String, AtomicLong> maskingCounts = new ConcurrentHashMap<>();
47+
48+
/**
49+
* Records when password masking occurs.
50+
*
51+
* @param className the class name of the object
52+
* @param closestAncestor the closest ancestor class name
53+
* @param jellyView the Jelly view where masking occurred
54+
*/
55+
public static void recordMasking(String className, String closestAncestor, String jellyView) {
56+
if (Telemetry.isDisabled()) {
57+
return;
58+
}
59+
60+
String key = className + "|" + closestAncestor + "|" + jellyView;
61+
maskingCounts.computeIfAbsent(key, k -> new AtomicLong(0)).incrementAndGet();
62+
}
63+
64+
@NonNull
65+
@Override
66+
public String getDisplayName() {
67+
return "Password field masking";
68+
}
69+
70+
@NonNull
71+
@Override
72+
public LocalDate getStart() {
73+
return LocalDate.of(2026, 1, 26);
74+
}
75+
76+
@NonNull
77+
@Override
78+
public LocalDate getEnd() {
79+
return LocalDate.of(2026, 7, 26);
80+
}
81+
82+
@Override
83+
public JSONObject createContent() {
84+
JSONArray events = new JSONArray();
85+
for (Map.Entry<String, AtomicLong> entry : maskingCounts.entrySet()) {
86+
String[] parts = entry.getKey().split("\\|", 3);
87+
if (parts.length == 3) {
88+
String className = parts[0];
89+
String closestAncestor = parts[1];
90+
String jellyView = parts[2];
91+
long count = entry.getValue().longValue();
92+
93+
JSONObject event = new JSONObject();
94+
event.put("className", className);
95+
event.put("closestAncestor", closestAncestor);
96+
event.put("jellyView", jellyView);
97+
event.put("count", count);
98+
events.add(event);
99+
}
100+
}
101+
102+
maskingCounts.clear();
103+
104+
JSONObject payload = new JSONObject();
105+
payload.put("components", buildComponentInformation());
106+
payload.put("masking", events);
107+
108+
return payload;
109+
}
110+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?jelly escape-by-default='true'?>
2+
<j:jelly xmlns:j="jelly:core">
3+
This trial collects detailed information about password field masking when users lack Configure permission on Items, Computers, or Views:
4+
<ul>
5+
<li>The class name</li>
6+
<li>The closest ancestor class name</li>
7+
<li>The Jelly view hierarchy where masking occurred</li>
8+
<li>The frequency of each combination</li>
9+
</ul>
10+
<p>
11+
This masking behavior is a fallback that should normally be rare, as views rendering content for users without Configure permission should use read-only mode instead.
12+
</p>
13+
<p>
14+
Additionally, this trial collects the list of installed plugins, their version, and the version of Jenkins.
15+
</p>
16+
</j:jelly>

0 commit comments

Comments
 (0)