-
Notifications
You must be signed in to change notification settings - Fork 245
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8ff5df2
commit 4ebfab7
Showing
4 changed files
with
131 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
src/main/java/com/cloudbees/plugins/credentials/SecretBytesReactionExtension.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package com.cloudbees.plugins.credentials; | ||
|
||
import hudson.ExtensionList; | ||
import hudson.init.Initializer; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
import jenkins.security.ExtendedReadRedaction; | ||
import org.kohsuke.accmod.Restricted; | ||
import org.kohsuke.accmod.restrictions.NoExternalUse; | ||
|
||
@Restricted(NoExternalUse.class) | ||
public class SecretBytesReactionExtension { | ||
|
||
public static final Logger LOGGER = Logger.getLogger(SecretBytesReactionExtension.class.getName()); | ||
|
||
// TODO Delete this and annotate `SecretBytesRedaction` with `@Extension` once the core dependency is >= 2.479 | ||
@Initializer | ||
public static void create() { | ||
try { | ||
ExtensionList.lookup(ExtendedReadRedaction.class).add(new SecretBytesRedaction()); | ||
} catch (NoClassDefFoundError unused) { | ||
LOGGER.log(Level.WARNING, "Failed to register SecretBytesRedaction. Update Jenkins to add support for redacting credentials in config.xml files from users with ExtendedRead permission. Learn more: https://www.jenkins.io/redirect/plugin/credentials/SecretBytesRedaction/"); | ||
} | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
src/main/java/com/cloudbees/plugins/credentials/SecretBytesRedaction.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.cloudbees.plugins.credentials; | ||
|
||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
import jenkins.security.ExtendedReadRedaction; | ||
import org.kohsuke.accmod.Restricted; | ||
import org.kohsuke.accmod.restrictions.NoExternalUse; | ||
|
||
@Restricted(NoExternalUse.class) | ||
// @Extension | ||
// See SecretBytesReactionExtension | ||
public class SecretBytesRedaction implements ExtendedReadRedaction { | ||
private static final Pattern SECRET_BYTES_PATTERN = Pattern.compile(">(" + SecretBytes.ENCRYPTED_VALUE_PATTERN + ")<"); | ||
|
||
@Override | ||
public String apply(String configDotXml) { | ||
Matcher matcher = SECRET_BYTES_PATTERN.matcher(configDotXml); | ||
StringBuilder cleanXml = new StringBuilder(); | ||
while (matcher.find()) { | ||
if (SecretBytes.isSecretBytes(matcher.group(1))) { | ||
matcher.appendReplacement(cleanXml, ">********<"); | ||
} | ||
} | ||
matcher.appendTail(cleanXml); | ||
return cleanXml.toString(); | ||
} | ||
} |
78 changes: 78 additions & 0 deletions
78
src/test/java/com/cloudbees/plugins/credentials/SecretBytesRedactionTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package com.cloudbees.plugins.credentials; | ||
|
||
import com.cloudbees.hudson.plugins.folder.Folder; | ||
import com.cloudbees.plugins.credentials.domains.Domain; | ||
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials; | ||
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; | ||
import hudson.model.Item; | ||
import hudson.model.ModelObject; | ||
import java.util.Base64; | ||
import java.util.Iterator; | ||
import jenkins.model.Jenkins; | ||
import org.htmlunit.Page; | ||
import org.junit.Rule; | ||
import org.junit.Test; | ||
import org.jvnet.hudson.test.JenkinsRule; | ||
import org.jvnet.hudson.test.MockAuthorizationStrategy; | ||
|
||
import static org.hamcrest.CoreMatchers.containsString; | ||
import static org.hamcrest.CoreMatchers.not; | ||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.junit.Assert.assertEquals; | ||
import static org.junit.Assert.assertTrue; | ||
|
||
public class SecretBytesRedactionTest { | ||
@Rule | ||
public JenkinsRule j = new JenkinsRule(); | ||
|
||
@Test | ||
public void testRedaction() throws Exception { | ||
final String usernamePasswordPassword = "thisisthe_theuserpassword"; | ||
final SecretBytes secretBytes = SecretBytes.fromString("thisis_theTestData"); | ||
|
||
Item.EXTENDED_READ.setEnabled(true); | ||
|
||
final Folder folder = j.jenkins.createProject(Folder.class, "F"); | ||
final CredentialsStore store = lookupStore(folder); | ||
final UsernamePasswordCredentialsImpl usernamePasswordCredentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "passwordid", null, "theusername", usernamePasswordPassword); | ||
store.addCredentials(Domain.global(), usernamePasswordCredentials); | ||
store.addCredentials(Domain.global(), new SecretBytesCredential(CredentialsScope.GLOBAL, "certid", "thedesc", secretBytes)); | ||
|
||
j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); | ||
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("alice").grant(Item.READ, Item.EXTENDED_READ, Jenkins.READ).everywhere().to("bob")); | ||
|
||
try (JenkinsRule.WebClient webClient = j.createWebClient().login("alice")) { | ||
final Page page = webClient.goTo("job/F/config.xml", "application/xml"); | ||
final String content = page.getWebResponse().getContentAsString(); | ||
assertThat(content, containsString(usernamePasswordCredentials.getPassword().getEncryptedValue())); | ||
assertThat(content, containsString(Base64.getEncoder().encodeToString(secretBytes.getEncryptedData()))); | ||
} | ||
try (JenkinsRule.WebClient webClient = j.createWebClient().login("bob")) { | ||
final Page page = webClient.goTo("job/F/config.xml", "application/xml"); | ||
final String content = page.getWebResponse().getContentAsString(); | ||
assertThat(content, not(containsString(usernamePasswordCredentials.getPassword().getEncryptedValue()))); | ||
assertThat(content, not(containsString(Base64.getEncoder().encodeToString(secretBytes.getEncryptedData())))); | ||
assertThat(content, containsString("<password>********</password>")); | ||
assertThat(content, containsString("<mySecretBytes>********</mySecretBytes>")); | ||
} | ||
} | ||
|
||
// Stolen from BaseStandardCredentialsTest | ||
private static CredentialsStore lookupStore(ModelObject object) { | ||
Iterator<CredentialsStore> stores = CredentialsProvider.lookupStores(object).iterator(); | ||
assertTrue(stores.hasNext()); | ||
CredentialsStore store = stores.next(); | ||
assertEquals("we got the expected store", object, store.getContext()); | ||
return store; | ||
} | ||
|
||
// This would be nicer with a real credential like `FileCredentialsImpl` but another test falls over if we add `plain-credentials` to the test scope | ||
public static class SecretBytesCredential extends BaseStandardCredentials { | ||
private final SecretBytes mySecretBytes; | ||
|
||
public SecretBytesCredential(CredentialsScope scope, String id, String description, SecretBytes bytes) { | ||
super(scope, id, description); | ||
this.mySecretBytes = bytes; | ||
} | ||
} | ||
} |