|
24 | 24 |
|
25 | 25 | package lib.form;
|
26 | 26 |
|
| 27 | +import static java.nio.file.Files.readString; |
27 | 28 | import static org.hamcrest.MatcherAssert.assertThat;
|
28 | 29 | import static org.hamcrest.Matchers.containsString;
|
29 | 30 | import static org.hamcrest.Matchers.is;
|
|
40 | 41 | import hudson.Launcher;
|
41 | 42 | import hudson.cli.CopyJobCommand;
|
42 | 43 | import hudson.cli.GetJobCommand;
|
| 44 | +import hudson.cli.GetNodeCommand; |
| 45 | +import hudson.cli.GetViewCommand; |
43 | 46 | import hudson.model.AbstractProject;
|
44 | 47 | import hudson.model.Action;
|
45 | 48 | import hudson.model.Computer;
|
|
48 | 51 | import hudson.model.Job;
|
49 | 52 | import hudson.model.JobProperty;
|
50 | 53 | import hudson.model.JobPropertyDescriptor;
|
| 54 | +import hudson.model.ListView; |
| 55 | +import hudson.model.Node; |
51 | 56 | import hudson.model.RootAction;
|
52 | 57 | import hudson.model.Run;
|
53 | 58 | import hudson.model.TaskListener;
|
54 | 59 | import hudson.model.User;
|
| 60 | +import hudson.model.View; |
| 61 | +import hudson.model.ViewProperty; |
| 62 | +import hudson.security.ACL; |
| 63 | +import hudson.slaves.DumbSlave; |
| 64 | +import hudson.slaves.NodeProperty; |
55 | 65 | import hudson.tasks.BuildStepDescriptor;
|
56 | 66 | import hudson.tasks.Builder;
|
57 | 67 | import hudson.util.FormValidation;
|
58 | 68 | import hudson.util.Secret;
|
59 | 69 | import java.io.ByteArrayOutputStream;
|
| 70 | +import java.io.File; |
60 | 71 | import java.io.IOException;
|
61 | 72 | import java.io.PrintStream;
|
62 | 73 | import java.util.Arrays;
|
63 | 74 | import java.util.Collection;
|
64 | 75 | import java.util.List;
|
65 | 76 | import java.util.Locale;
|
| 77 | +import java.util.Map; |
66 | 78 | import java.util.regex.Pattern;
|
67 | 79 | import jenkins.model.GlobalConfiguration;
|
68 | 80 | import jenkins.model.Jenkins;
|
69 | 81 | import jenkins.model.TransientActionFactory;
|
| 82 | +import jenkins.security.ExtendedReadRedaction; |
70 | 83 | import jenkins.security.ExtendedReadSecretRedaction;
|
71 | 84 | import jenkins.tasks.SimpleBuildStep;
|
72 | 85 | import org.htmlunit.Page;
|
@@ -124,6 +137,145 @@ public String getUrlName() {
|
124 | 137 | }
|
125 | 138 | }
|
126 | 139 |
|
| 140 | + @For({ExtendedReadRedaction.class, ExtendedReadSecretRedaction.class}) |
| 141 | + @Issue("SECURITY-3495") |
| 142 | + @Test |
| 143 | + public void testNodeSecrets() throws Exception { |
| 144 | + Computer.EXTENDED_READ.setEnabled(true); |
| 145 | + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); |
| 146 | + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("alice").grant(Jenkins.READ, Computer.EXTENDED_READ).everywhere().to("bob")); |
| 147 | + |
| 148 | + final DumbSlave onlineSlave = j.createOnlineSlave(); |
| 149 | + final String secretText = "t0ps3cr3td4t4_node"; |
| 150 | + final Secret encryptedSecret = Secret.fromString(secretText); |
| 151 | + final String encryptedSecretText = encryptedSecret.getEncryptedValue(); |
| 152 | + |
| 153 | + onlineSlave.getNodeProperties().add(new NodePropertyWithSecret(encryptedSecret)); |
| 154 | + onlineSlave.save(); |
| 155 | + |
| 156 | + assertThat(readString(new File(onlineSlave.getRootDir(), "config.xml").toPath()), containsString(encryptedSecretText)); |
| 157 | + |
| 158 | + |
| 159 | + { // admin can see encrypted value |
| 160 | + GetNodeCommand command = new GetNodeCommand(); |
| 161 | + try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) { |
| 162 | + final Page page = wc.goTo(onlineSlave.getComputer().getUrl() + "config.xml", "application/xml"); |
| 163 | + final String content = page.getWebResponse().getContentAsString(); |
| 164 | + |
| 165 | + assertThat(content, not(containsString(secretText))); |
| 166 | + assertThat(content, containsString(encryptedSecretText)); |
| 167 | + assertThat(content, containsString("<secret>" + encryptedSecretText + "</secret>")); |
| 168 | + |
| 169 | + var baos = new ByteArrayOutputStream(); |
| 170 | + try (var unused = ACL.as(User.get("alice", true, Map.of()))) { |
| 171 | + command.setTransportAuth2(Jenkins.getAuthentication2()); |
| 172 | + command.main(List.of(onlineSlave.getNodeName()), Locale.US, System.in, new PrintStream(baos), System.err); |
| 173 | + } |
| 174 | + assertEquals(content, baos.toString(page.getWebResponse().getContentCharset())); |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + { // extended reader gets only redacted value |
| 179 | + GetNodeCommand command = new GetNodeCommand(); |
| 180 | + try (JenkinsRule.WebClient wc = j.createWebClient().login("bob")) { |
| 181 | + final Page page = wc.goTo(onlineSlave.getComputer().getUrl() + "config.xml", "application/xml"); |
| 182 | + final String content = page.getWebResponse().getContentAsString(); |
| 183 | + |
| 184 | + assertThat(content, not(containsString(secretText))); |
| 185 | + assertThat(content, not(containsString(encryptedSecretText))); |
| 186 | + assertThat(content, containsString("<secret>********</secret>")); |
| 187 | + |
| 188 | + var baos = new ByteArrayOutputStream(); |
| 189 | + try (var unused = ACL.as(User.get("bob", true, Map.of()))) { |
| 190 | + command.setTransportAuth2(Jenkins.getAuthentication2()); |
| 191 | + command.main(List.of(onlineSlave.getNodeName()), Locale.US, System.in, new PrintStream(baos), System.err); |
| 192 | + } |
| 193 | + assertEquals(content, baos.toString(page.getWebResponse().getContentCharset())); |
| 194 | + } |
| 195 | + } |
| 196 | + } |
| 197 | + |
| 198 | + public static class NodePropertyWithSecret extends NodeProperty<Node> { |
| 199 | + private final Secret secret; |
| 200 | + |
| 201 | + public NodePropertyWithSecret(Secret secret) { |
| 202 | + this.secret = secret; |
| 203 | + } |
| 204 | + |
| 205 | + public Secret getSecret() { |
| 206 | + return secret; |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + @For({ExtendedReadRedaction.class, ExtendedReadSecretRedaction.class}) |
| 211 | + @Issue("SECURITY-3496") |
| 212 | + @Test |
| 213 | + public void testViewSecrets() throws Exception { |
| 214 | + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); |
| 215 | + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("alice").grant(Jenkins.READ, View.READ).everywhere().to("bob")); |
| 216 | + |
| 217 | + final String secretText = "t0ps3cr3td4t4_view"; |
| 218 | + final Secret encryptedSecret = Secret.fromString(secretText); |
| 219 | + final String encryptedSecretText = encryptedSecret.getEncryptedValue(); |
| 220 | + |
| 221 | + final ListView v = new ListView("security-3496"); |
| 222 | + v.getProperties().add(new ViewPropertyWithSecret(encryptedSecret)); |
| 223 | + j.jenkins.addView(v); |
| 224 | + |
| 225 | + assertThat(readString(new File(j.jenkins.getRootDir(), "config.xml").toPath()), containsString(encryptedSecretText)); |
| 226 | + |
| 227 | + |
| 228 | + { // admin can see encrypted value |
| 229 | + var command = new GetViewCommand(); |
| 230 | + try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) { |
| 231 | + final Page page = wc.goTo(v.getUrl() + "config.xml", "application/xml"); |
| 232 | + final String content = page.getWebResponse().getContentAsString(); |
| 233 | + |
| 234 | + assertThat(content, not(containsString(secretText))); |
| 235 | + assertThat(content, containsString(encryptedSecretText)); |
| 236 | + assertThat(content, containsString("<secret>" + encryptedSecretText + "</secret>")); |
| 237 | + |
| 238 | + var baos = new ByteArrayOutputStream(); |
| 239 | + try (var unused = ACL.as(User.get("alice", true, Map.of()))) { |
| 240 | + command.setTransportAuth2(Jenkins.getAuthentication2()); |
| 241 | + command.main(List.of(v.getViewName()), Locale.US, System.in, new PrintStream(baos), System.err); |
| 242 | + } |
| 243 | + assertEquals(content, baos.toString(page.getWebResponse().getContentCharset())); |
| 244 | + } |
| 245 | + } |
| 246 | + |
| 247 | + { // extended reader gets only redacted value |
| 248 | + var command = new GetViewCommand(); |
| 249 | + try (JenkinsRule.WebClient wc = j.createWebClient().login("bob")) { |
| 250 | + final Page page = wc.goTo(v.getUrl() + "config.xml", "application/xml"); |
| 251 | + final String content = page.getWebResponse().getContentAsString(); |
| 252 | + |
| 253 | + assertThat(content, not(containsString(secretText))); |
| 254 | + assertThat(content, not(containsString(encryptedSecretText))); |
| 255 | + assertThat(content, containsString("<secret>********</secret>")); |
| 256 | + |
| 257 | + var baos = new ByteArrayOutputStream(); |
| 258 | + try (var unused = ACL.as(User.get("bob", true, Map.of()))) { |
| 259 | + command.setTransportAuth2(Jenkins.getAuthentication2()); |
| 260 | + command.main(List.of(v.getViewName()), Locale.US, System.in, new PrintStream(baos), System.err); |
| 261 | + } |
| 262 | + assertEquals(content, baos.toString(page.getWebResponse().getContentCharset())); |
| 263 | + } |
| 264 | + } |
| 265 | + } |
| 266 | + |
| 267 | + public static class ViewPropertyWithSecret extends ViewProperty { |
| 268 | + private final Secret secret; |
| 269 | + |
| 270 | + public ViewPropertyWithSecret(Secret secret) { |
| 271 | + this.secret = secret; |
| 272 | + } |
| 273 | + |
| 274 | + public Secret getSecret() { |
| 275 | + return secret; |
| 276 | + } |
| 277 | + } |
| 278 | + |
127 | 279 | @Issue({"SECURITY-266", "SECURITY-304"})
|
128 | 280 | @Test
|
129 | 281 | @For(ExtendedReadSecretRedaction.class)
|
|
0 commit comments