package jenkins.security.s2m;
import hudson.Extension;
import hudson.FilePath;
import hudson.Functions;
import hudson.Util;
import hudson.util.HttpResponses;
import jenkins.model.Jenkins;
import jenkins.util.io.FileBoolean;
import org.apache.commons.io.FileUtils;
import org.jenkinsci.remoting.Role;
import org.jenkinsci.remoting.RoleSensitive;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerProxy;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.interceptor.RequirePOST;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.Collection;
import java.util.Enumeration;
import java.util.logging.Logger;
import static java.util.logging.Level.*;
/**
* Rules of whitelisting for {@link RoleSensitive} objects and {@link FilePath}s.
*
* @author Kohsuke Kawaguchi
*/
@Extension
public class AdminWhitelistRule implements StaplerProxy {
/**
* Ones that we rejected but want to run by admins.
*/
public final CallableRejectionConfig rejected;
/**
* Callables that admins have whitelisted explicitly.
*/
public final CallableWhitelistConfig whitelisted;
/**
* FilePath access pattern rules specified by the admin
*/
public final FilePathRuleConfig filePathRules;
private final Jenkins jenkins;
private boolean masterKillSwitch;
public AdminWhitelistRule() throws IOException, InterruptedException {
this.jenkins = Jenkins.getInstance();
// while this file is not a secret, write access to this file is dangerous,
// so put this in the better-protected part of $JENKINS_HOME, which is in secrets/
// overwrite 30-default.conf with what we think is the best from the core.
// this file shouldn't be touched by anyone. For local customization, use other files in the conf dir.
// 0-byte file is used as a signal from the admin to prevent this overwriting
placeDefaultRule(
new File(jenkins.getRootDir(), "secrets/whitelisted-callables.d/default.conf"),
getClass().getResourceAsStream("callable.conf"));
placeDefaultRule(
new File(jenkins.getRootDir(), "secrets/filepath-filters.d/30-default.conf"),
transformForWindows(getClass().getResourceAsStream("filepath-filter.conf")));
this.whitelisted = new CallableWhitelistConfig(
new File(jenkins.getRootDir(),"secrets/whitelisted-callables.d/gui.conf"));
this.rejected = new CallableRejectionConfig(
new File(jenkins.getRootDir(),"secrets/rejected-callables.txt"),
whitelisted);
this.filePathRules = new FilePathRuleConfig(
new File(jenkins.getRootDir(),"secrets/filepath-filters.d/50-gui.conf"));
this.masterKillSwitch = loadMasterKillSwitchFile();
}
/**
* Reads the master kill switch.
*
* Instead of {@link FileBoolean}, we use a text file so that the admin can prevent Jenkins from
* writing this to file.
*/
private boolean loadMasterKillSwitchFile() {
File f = getMasterKillSwitchFile();
try {
if (!f.exists()) return true;
return Boolean.parseBoolean(FileUtils.readFileToString(f).trim());
} catch (IOException e) {
LOGGER.log(WARNING, "Failed to read "+f, e);
return false;
}
}
private File getMasterKillSwitchFile() {
return new File(jenkins.getRootDir(),"secrets/slave-to-master-security-kill-switch");
}
/**
* Transform path for Windows.
*/
private InputStream transformForWindows(InputStream src) throws IOException {
BufferedReader r = new BufferedReader(new InputStreamReader(src));
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintStream p = new PrintStream(out);
String line;
while ((line=r.readLine())!=null) {
if (!line.startsWith("#") && Functions.isWindows())
line = line.replace("/","\\\\");
p.println(line);
}
p.close();
return new ByteArrayInputStream(out.toByteArray());
}
private void placeDefaultRule(File f, InputStream src) throws IOException, InterruptedException {
try {
new FilePath(f).copyFrom(src);
} catch (IOException e) {
// we allow admins to create a read-only file here to block overwrite,
// so this can fail legitimately
if (!f.canWrite()) return;
LOGGER.log(WARNING, "Failed to generate "+f,e);
}
}
public boolean isWhitelisted(RoleSensitive subject, Collection<Role> expected, Object context) {
if (masterKillSwitch)
return true; // master kill switch is on. subsystem deactivated
String name = subject.getClass().getName();
if (whitelisted.contains(name))
return true; // whitelisted by admin
// otherwise record the problem and refuse to execute that
rejected.report(subject.getClass());
return false;
}
public boolean checkFileAccess(String op, File f) {
// if the master kill switch is off, we allow everything
if (masterKillSwitch)
return true;
return filePathRules.checkFileAccess(op, f);
}
@RequirePOST
public HttpResponse doSubmit(StaplerRequest req) throws IOException {
jenkins.checkPermission(Jenkins.RUN_SCRIPTS);
String whitelist = Util.fixNull(req.getParameter("whitelist"));
if (!whitelist.endsWith("\n"))
whitelist+="\n";
Enumeration e = req.getParameterNames();
while (e.hasMoreElements()) {
String name = (String) e.nextElement();
if (name.startsWith("class:")) {
whitelist += name.substring(6)+"\n";
}
}
whitelisted.set(whitelist);
String newRules = Util.fixNull(req.getParameter("filePathRules"));
filePathRules.parseTest(newRules); // test first before writing a potentially broken rules
filePathRules.set(newRules);
return HttpResponses.redirectToDot();
}
/**
* Approves all the currently rejected subjects
*/
@RequirePOST
public HttpResponse doApproveAll() throws IOException {
StringBuilder buf = new StringBuilder();
for (Class c : rejected.get()) {
buf.append(c.getName()).append('\n');
}
whitelisted.append(buf.toString());
return HttpResponses.ok();
}
/**
* Approves specific callables by their names.
*/
@RequirePOST
public HttpResponse doApprove(@QueryParameter String value) throws IOException {
whitelisted.append(value);
return HttpResponses.ok();
}
public boolean getMasterKillSwitch() {
return masterKillSwitch;
}
public void setMasterKillSwitch(boolean state) {
try {
jenkins.checkPermission(Jenkins.RUN_SCRIPTS);
FileUtils.writeStringToFile(getMasterKillSwitchFile(),Boolean.toString(state));
// treat the file as the canonical source of information in case write fails
masterKillSwitch = loadMasterKillSwitchFile();
} catch (IOException e) {
LOGGER.log(WARNING, "Failed to write master kill switch", e);
}
}
/**
* Restricts the access to administrator.
*/
@Override
public Object getTarget() {
jenkins.checkPermission(Jenkins.RUN_SCRIPTS);
return this;
}
private static final Logger LOGGER = Logger.getLogger(AdminWhitelistRule.class.getName());
}