package jenkins.security;
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.html.DomNodeUtil;
import com.gargoylesoftware.htmlunit.html.HtmlButton;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.trilead.ssh2.crypto.Base64;
import hudson.FilePath;
import hudson.Util;
import hudson.util.Secret;
import hudson.util.SecretHelper;
import org.apache.commons.io.FileUtils;
import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.recipes.Recipe.Runner;
import org.xml.sax.SAXException;
import javax.crypto.Cipher;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
/**
* @author Kohsuke Kawaguchi
*/
public class RekeySecretAdminMonitorTest extends HudsonTestCase {
@Inject
RekeySecretAdminMonitor monitor;
@Override
protected void setUp() throws Exception {
SecretHelper.set(TEST_KEY);
super.setUp();
monitor.setNeeded();
}
@Override
protected void tearDown() throws Exception {
SecretHelper.set(null);
super.tearDown();
}
@Override
protected void recipe() throws Exception {
super.recipe();
recipes.add(new Runner() {
@Override
public void setup(HudsonTestCase testCase, Annotation recipe) throws Exception {
}
@Override
public void decorateHome(HudsonTestCase testCase, File home) throws Exception {
if (getName().endsWith("testScanOnBoot")) {
// schedule a scan on boot
File f = new File(home, RekeySecretAdminMonitor.class.getName() + "/scanOnBoot");
f.getParentFile().mkdirs();
new FilePath(f).touch(0);
// and stage some data
putSomeOldData(home);
}
}
@Override
public void tearDown(HudsonTestCase testCase, Annotation recipe) throws Exception {
}
});
}
private void putSomeOldData(File dir) throws Exception {
File xml = new File(dir, "foo.xml");
FileUtils.writeStringToFile(xml,"<foo>" + encryptOld(TEST_KEY) + "</foo>");
}
private void verifyRewrite(File dir) throws Exception {
File xml = new File(dir, "foo.xml");
assertEquals("<foo>" + encryptNew(TEST_KEY) + "</foo>".trim(),
FileUtils.readFileToString(xml).trim());
}
// TODO sometimes fails: "Invalid request submission: {json=[Ljava.lang.String;@2c46358e, .crumb=[Ljava.lang.String;@35661457}"
public void _testBasicWorkflow() throws Exception {
putSomeOldData(jenkins.getRootDir());
WebClient wc = createWebClient();
// one should see the warning. try scheduling it
assertTrue(!monitor.isScanOnBoot());
HtmlForm form = getRekeyForm(wc);
submit(form, "schedule");
assertTrue(monitor.isScanOnBoot());
form = getRekeyForm(wc);
assertTrue(getButton(form, 1).isDisabled());
// run it now
assertTrue(!monitor.getLogFile().exists());
submit(form, "background");
assertTrue(monitor.getLogFile().exists());
// should be no warning/error now
HtmlPage manage = wc.goTo("manage");
assertEquals(0, DomNodeUtil.selectNodes(manage, "//*[class='error']").size());
assertEquals(0, DomNodeUtil.selectNodes(manage, "//*[class='warning']").size());
// and the data should be rewritten
verifyRewrite(jenkins.getRootDir());
assertTrue(monitor.isDone());
// dismiss and the message will be gone
assertTrue(monitor.isEnabled());
form = getRekeyForm(wc);
submit(form, "dismiss");
assertFalse(monitor.isEnabled());
try {
getRekeyForm(wc);
fail();
} catch (ElementNotFoundException e) {
// expected
}
}
private HtmlForm getRekeyForm(WebClient wc) throws IOException, SAXException {
return wc.goTo("manage").getFormByName("rekey");
}
private HtmlButton getButton(HtmlForm form, int index) {
return form.<HtmlButton>getHtmlElementsByTagName("button").get(index);
}
public void testScanOnBoot() throws Exception {
WebClient wc = createWebClient();
// scan on boot should have run the scan
assertTrue(monitor.getLogFile().exists());
assertTrue("scan on boot should have turned this off",!monitor.isScanOnBoot());
// and data should be migrated
verifyRewrite(jenkins.getRootDir());
// should be no warning/error now
HtmlPage manage = wc.goTo("/manage");
assertEquals(0, DomNodeUtil.selectNodes(manage, "//*[class='error']").size());
assertEquals(0, DomNodeUtil.selectNodes(manage, "//*[class='warning']").size());
}
private String encryptOld(String str) throws Exception {
Cipher cipher = Secret.getCipher("AES");
cipher.init(Cipher.ENCRYPT_MODE, Util.toAes128Key(TEST_KEY));
return new String(Base64.encode(cipher.doFinal((str + "::::MAGIC::::").getBytes("UTF-8"))));
}
private String encryptNew(String str) {
return Secret.fromString(str).getEncryptedValue();
}
private static final String TEST_KEY = "superDuperSecretWasNotSoSecretAfterAll";
}