Skip to content

Commit 3ee7380

Browse files
Kevin-CBjenkinsci-cert-ci
authored andcommitted
1 parent 4710d65 commit 3ee7380

File tree

4 files changed

+305
-7
lines changed

4 files changed

+305
-7
lines changed

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

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,12 @@ public class Functions{
203203
privatestaticfinalAtomicLongiota = newAtomicLong();
204204
privatestaticLoggerLOGGER = Logger.getLogger(Functions.class.getName());
205205

206+
/**
207+
* Escape hatch to use the non-recursive f:password masking.
208+
*/
209+
privatestatic/* non-final */booleanNON_RECURSIVE_PASSWORD_MASKING_PERMISSION_CHECK = SystemProperties.getBoolean(Functions.class.getName() + ".nonRecursivePasswordMaskingPermissionCheck");
210+
211+
206212
publicFunctions(){
207213
}
208214

@@ -2252,13 +2258,38 @@ public String getPasswordValue(Object o){
22522258
StaplerRequest2req = Stapler.getCurrentRequest2();
22532259
if (oinstanceofSecret || Secret.BLANK_NONSECRET_PASSWORD_FIELDS_WITHOUT_ITEM_CONFIGURE){
22542260
if (req != null){
2255-
Itemitem = req.findAncestorObject(Item.class);
2256-
if (item != null && !item.hasPermission(Item.CONFIGURE)){
2257-
return"********";
2258-
}
2259-
Computercomputer = req.findAncestorObject(Computer.class);
2260-
if (computer != null && !computer.hasPermission(Computer.CONFIGURE)){
2261-
return"********";
2261+
if (NON_RECURSIVE_PASSWORD_MASKING_PERMISSION_CHECK){
2262+
Itemitem = req.findAncestorObject(Item.class);
2263+
if (item != null && !item.hasPermission(Item.CONFIGURE)){
2264+
return"********";
2265+
}
2266+
Computercomputer = req.findAncestorObject(Computer.class);
2267+
if (computer != null && !computer.hasPermission(Computer.CONFIGURE)){
2268+
return"********";
2269+
}
2270+
} else{
2271+
List<Ancestor> ancestors = req.getAncestors();
2272+
for (Ancestorancestor : Iterators.reverse(ancestors)){
2273+
Objecttype = ancestor.getObject();
2274+
if (typeinstanceofItemitem){
2275+
if (!item.hasPermission(Item.CONFIGURE)){
2276+
return"********";
2277+
}
2278+
break;
2279+
}
2280+
if (typeinstanceofComputercomputer){
2281+
if (!computer.hasPermission(Computer.CONFIGURE)){
2282+
return"********";
2283+
}
2284+
break;
2285+
}
2286+
if (typeinstanceofViewview){
2287+
if (!view.hasPermission(View.CONFIGURE)){
2288+
return"********";
2289+
}
2290+
break;
2291+
}
2292+
}
22622293
}
22632294
}
22642295
}
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
packagejenkins.security;
2+
3+
importstaticorg.hamcrest.MatcherAssert.assertThat;
4+
importstaticorg.hamcrest.Matchers.containsString;
5+
importstaticorg.hamcrest.Matchers.not;
6+
7+
importcom.cloudbees.hudson.plugins.folder.Folder;
8+
importhudson.model.Action;
9+
importhudson.model.Computer;
10+
importhudson.model.FreeStyleBuild;
11+
importhudson.model.FreeStyleProject;
12+
importhudson.model.Item;
13+
importhudson.model.ListView;
14+
importhudson.model.View;
15+
importhudson.slaves.DumbSlave;
16+
importhudson.util.Secret;
17+
importjenkins.model.Jenkins;
18+
importorg.htmlunit.Page;
19+
importorg.junit.jupiter.api.BeforeEach;
20+
importorg.junit.jupiter.api.Test;
21+
importorg.jvnet.hudson.test.Issue;
22+
importorg.jvnet.hudson.test.JenkinsRule;
23+
importorg.jvnet.hudson.test.MockAuthorizationStrategy;
24+
importorg.jvnet.hudson.test.junit.jupiter.WithJenkins;
25+
26+
@WithJenkins
27+
classSecurity1809Test{
28+
29+
privateJenkinsRulej;
30+
31+
privatefinalStringpassword = "p4ssw0rd";
32+
33+
privatefinalSecretsecretPassword = Secret.fromString(password);
34+
35+
@BeforeEach
36+
voidsetUp(JenkinsRulerule){
37+
j = rule;
38+
}
39+
40+
@Test
41+
@Issue("SECURITY-1809")
42+
voidpasswordIsMaskedForView() throwsException{
43+
finalPasswordViewview = newPasswordView("view1", secretPassword);
44+
j.jenkins.addView(view);
45+
46+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
47+
j.jenkins.setAuthorizationStrategy(newMockAuthorizationStrategy()
48+
.grant(Jenkins.READ, View.READ).everywhere().to("readUser")
49+
.grant(Jenkins.READ, View.READ, View.CONFIGURE).everywhere().to("configureUser"));
50+
51+
Stringurl = view.getUrl() + "password";
52+
53+
// configure permission allow to see encrypted value
54+
assertContainsOnlyEncryptedSecret("configureUser", url);
55+
56+
// read permission get only redacted value
57+
assertContainsOnlyMaskedSecret("readUser", url);
58+
}
59+
60+
@Test
61+
@Issue("SECURITY-1809")
62+
voidpasswordIsMaskedForPrimaryView() throwsException{
63+
finalPasswordViewview = newPasswordView("view1", secretPassword);
64+
j.jenkins.addView(view);
65+
j.jenkins.setPrimaryView(view);
66+
67+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
68+
j.jenkins.setAuthorizationStrategy(newMockAuthorizationStrategy()
69+
.grant(Jenkins.READ).everywhere().to("readUser")
70+
.grant(Jenkins.READ, View.READ, View.CONFIGURE).everywhere().to("configureUser"));
71+
72+
Stringurl = "password";
73+
74+
// configure permission allow to see encrypted value
75+
assertContainsOnlyEncryptedSecret("configureUser", url);
76+
77+
// read permission get only redacted value
78+
assertContainsOnlyMaskedSecret("readUser", url);
79+
}
80+
81+
@Test
82+
voidpasswordIsMaskedForAgent() throwsException{
83+
finalDumbSlaveagent = j.createSlave("agent1", "", null);
84+
85+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
86+
j.jenkins.setAuthorizationStrategy(newMockAuthorizationStrategy()
87+
.grant(Jenkins.READ, Computer.CONFIGURE).everywhere().to("configureUser")
88+
.grant(Jenkins.READ).everywhere().to("readUser"));
89+
90+
agent.toComputer().addAction(newPasswordAction(secretPassword));
91+
92+
Stringurl = agent.toComputer().getUrl() + "password";
93+
94+
// configure permission allow to see encrypted value
95+
assertContainsOnlyEncryptedSecret("configureUser", url);
96+
97+
// read permission get only redacted value
98+
assertContainsOnlyMaskedSecret("readUser", url);
99+
}
100+
101+
@Test
102+
voidpasswordIsMaskedForJob() throwsException{
103+
finalFreeStyleProjectjob = j.createFreeStyleProject();
104+
FreeStyleBuildbuild = j.buildAndAssertSuccess(job);
105+
build.addAction(newPasswordAction(secretPassword));
106+
107+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
108+
j.jenkins.setAuthorizationStrategy(newMockAuthorizationStrategy()
109+
.grant(Jenkins.READ, Item.READ, Item.CONFIGURE).everywhere().to("configureUser")
110+
.grant(Jenkins.READ, Item.READ).everywhere().to("readUser"));
111+
112+
Stringurl = build.getUrl() + "password";
113+
114+
// configure permission allow to see encrypted value
115+
assertContainsOnlyEncryptedSecret("configureUser", url);
116+
117+
// read permission get only redacted value
118+
assertContainsOnlyMaskedSecret("readUser", url);
119+
}
120+
121+
@Test
122+
voidpermissionIsCheckedOnClosestAncestor() throwsException{
123+
finalPasswordViewview = newPasswordView("view1", secretPassword);
124+
j.jenkins.addView(view);
125+
126+
finalFreeStyleProjectjob = j.createFreeStyleProject("job1");
127+
FreeStyleBuildbuild = j.buildAndAssertSuccess(job);
128+
build.addAction(newActionWithView(view));
129+
130+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
131+
j.jenkins.setAuthorizationStrategy(newMockAuthorizationStrategy()
132+
.grant(Jenkins.READ, Item.READ, View.READ, Item.CONFIGURE).everywhere().to("itemConfigureUser")
133+
.grant(Jenkins.READ, Item.READ, View.READ, View.CONFIGURE).everywhere().to("viewConfigureUser"));
134+
135+
Stringurl = build.getUrl() + "myAction/view/password";
136+
137+
// View/Configure permission allow to see encrypted value
138+
assertContainsOnlyEncryptedSecret("viewConfigureUser", url);
139+
140+
// Item/Configure permission get only redacted value
141+
assertContainsOnlyMaskedSecret("itemConfigureUser", url);
142+
}
143+
144+
@Test
145+
@Issue("SECURITY-1809")
146+
voidpermissionIsCorrectlyCheckedOnNestedObject() throwsException{
147+
finalFolderfolder = j.jenkins.createProject(Folder.class, "folder1");
148+
finalFreeStyleProjectjob = folder.createProject(FreeStyleProject.class, "job1");
149+
FreeStyleBuildbuild = j.buildAndAssertSuccess(job);
150+
build.addAction(newPasswordAction(secretPassword));
151+
152+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
153+
j.jenkins.setAuthorizationStrategy(newMockAuthorizationStrategy()
154+
// Item.CONFIGURE on job1 but NOT on folder1
155+
.grant(Jenkins.READ, Item.READ).everywhere().to("jobConfigureUser")
156+
.grant(Item.CONFIGURE).onItems(job).to("jobConfigureUser")
157+
// Item.CONFIGURE on folder1 but NOT on job1
158+
.grant(Jenkins.READ, Item.READ).everywhere().to("folderConfigureUser")
159+
.grant(Item.CONFIGURE).onItems(folder).to("folderConfigureUser"));
160+
161+
Stringurl = build.getUrl() + "password";
162+
163+
// Item/Configure permission on job1 allow to see encrypted value
164+
assertContainsOnlyEncryptedSecret("jobConfigureUser", url);
165+
166+
// Item/Configure permission only on folder1 get only redacted value
167+
assertContainsOnlyMaskedSecret("folderConfigureUser", url);
168+
}
169+
170+
privatevoidassertContainsOnlyEncryptedSecret(Stringuser, Stringurl) throwsException{
171+
try (JenkinsRule.WebClientwc = j.createWebClient().login(user)){
172+
Pagepage = wc.goTo(url);
173+
Stringcontent = page.getWebResponse().getContentAsString();
174+
175+
assertThat(content, not(containsString(password)));
176+
assertThat(content, containsString(secretPassword.getEncryptedValue()));
177+
}
178+
}
179+
180+
privatevoidassertContainsOnlyMaskedSecret(Stringuser, Stringurl) throwsException{
181+
try (JenkinsRule.WebClientwc = j.createWebClient().login(user)){
182+
Pagepage = wc.goTo(url);
183+
Stringcontent = page.getWebResponse().getContentAsString();
184+
185+
assertThat(content, containsString("********"));
186+
assertThat(content, not(containsString(password)));
187+
assertThat(content, not(containsString(secretPassword.getEncryptedValue())));
188+
}
189+
}
190+
191+
publicstaticclassPasswordViewextendsListView{
192+
privatefinalSecretsecret;
193+
194+
PasswordView(Stringname, Secretsecret){
195+
super(name);
196+
this.secret = secret;
197+
}
198+
199+
publicSecretgetSecret(){
200+
returnsecret;
201+
}
202+
}
203+
204+
publicstaticclassPasswordActionimplementsAction{
205+
privatefinalSecretsecret;
206+
207+
PasswordAction(Secretsecret){
208+
this.secret = secret;
209+
}
210+
211+
publicSecretgetSecret(){
212+
returnsecret;
213+
}
214+
215+
@Override
216+
publicStringgetIconFileName(){returnnull}
217+
218+
@Override
219+
publicStringgetDisplayName(){returnnull}
220+
221+
@Override
222+
publicStringgetUrlName(){return"password"}
223+
}
224+
225+
publicstaticclassActionWithViewimplementsAction{
226+
privatefinalPasswordViewview;
227+
228+
ActionWithView(PasswordViewview){
229+
this.view = view;
230+
}
231+
232+
publicPasswordViewgetView(){
233+
returnview;
234+
}
235+
236+
@Override
237+
publicStringgetIconFileName(){returnnull}
238+
239+
@Override
240+
publicStringgetDisplayName(){returnnull}
241+
242+
@Override
243+
publicStringgetUrlName(){return"myAction"}
244+
}
245+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?jelly escape-by-default='true'?>
2+
<j:jellyxmlns:j="jelly:core"xmlns:f="/lib/form"xmlns:l="/lib/layout">
3+
<l:layouttitle="Test Password">
4+
<l:main-panel>
5+
<j:setvar="instance"value="${it}" />
6+
<f:entrytitle="Password"field="secret">
7+
<f:password />
8+
</f:entry>
9+
</l:main-panel>
10+
</l:layout>
11+
</j:jelly>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?jelly escape-by-default='true'?>
2+
<j:jellyxmlns:j="jelly:core"xmlns:f="/lib/form"xmlns:l="/lib/layout">
3+
<l:layouttitle="Test Password - View">
4+
<l:main-panel>
5+
<j:setvar="instance"value="${it}" />
6+
<f:entrytitle="Password"field="secret">
7+
<f:password />
8+
</f:entry>
9+
</l:main-panel>
10+
</l:layout>
11+
</j:jelly>

0 commit comments

Comments
(0)