Moved the rules into a separate class

This commit is contained in:
Kohsuke Kawaguchi committed 2014-10-16 16:55:17 -07:00
1 parent cb65dd65f3
commit 356764dced
8 files changed
+336 -283

No files matched your search

@@ -10,11 +10,7 @@ import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* Report any rejected {@link Callable}s and {@link FilePath} executions and allow
@@ -29,51 +25,11 @@ public class AdminCallableMonitor extends AdministrativeMonitor {
Jenkins jenkins;
@Inject
AdminCallableWhitelist whitelist;
private interface OpMatcher {
boolean matches(String op);
}
private class FilePathRule {
final Pattern path;
final OpMatcher op;
final boolean allow;
private FilePathRule(Pattern path, OpMatcher op, boolean allow) {
this.path = path;
this.op = op;
this.allow = allow;
}
}
private final List<FilePathRule> filePathRules = new ArrayList<FilePathRule>();
public boolean checkFileAccess(String op, File path) throws SecurityException {
String pathStr = null;
for (FilePathRule rule : filePathRules) {
if (rule.op.matches(op)) {
if (pathStr==null)
// do not canonicalize nor absolutize, so that JENKINS_HOME that spans across
// multiple volumes via symlinks can look logically like one unit.
pathStr = path.getPath();
if (rule.path.matcher(pathStr).matches()) {
// exclusion rule is only to bypass later path rules within #filePathRules,
// and we still want other FilePathFilters to whitelist/blacklist access.
// therefore I'm not throwing a SecurityException here
return rule.allow;
}
}
}
return false;
}
AdminWhitelistRule rule;
@Override
public boolean isActivated() {
return whitelist.hasRejection();
return rule.hasRejection();
}
@Override
@@ -82,8 +38,8 @@ public class AdminCallableMonitor extends AdministrativeMonitor {
}
// bind this to URL
public AdminCallableWhitelist getWhitelist() {
return whitelist;
public AdminWhitelistRule getRule() {
return rule;
}
/**
@@ -94,7 +50,7 @@ public class AdminCallableMonitor extends AdministrativeMonitor {
disable(true);
return HttpResponses.redirectViaContextPath("/manage");
} else {
return HttpResponses.redirectTo("whitelist/");
return HttpResponses.redirectTo("rule/");
}
}
}
@@ -1,39 +1,13 @@
package jenkins.security.admin;
import com.google.common.collect.ImmutableSet;
import hudson.Extension;
import hudson.PluginWrapper;
import hudson.Util;
import hudson.init.Initializer;
import hudson.remoting.Callable;
import hudson.util.HttpResponses;
import hudson.util.TextFile;
import jenkins.model.Jenkins;
import jenkins.security.CallableWhitelist;
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 javax.annotation.CheckForNull;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import static hudson.init.InitMilestone.*;
/**
* Whitelists {@link Callable}s that are approved by the admins.
@@ -47,210 +21,12 @@ import static hudson.init.InitMilestone.*;
* @author Kohsuke Kawaguchi
*/
@Extension(ordinal=-100)
public class AdminCallableWhitelist extends CallableWhitelist implements StaplerProxy {
/**
* Ones that we rejected but want to run by admins.
*/
private final Set<Class> rejected = Collections.synchronizedSet(new LinkedHashSet<Class>());
// CoW
private volatile Set<String> whitelisted = Collections.emptySet();
public class AdminCallableWhitelist extends CallableWhitelist {
@Inject
Jenkins jenkins;
AdminWhitelistRule rule;
@Override
public boolean isWhitelisted(RoleSensitive subject, Collection<Role> expected, Object context) {
String name = subject.getClass().getName();
if (whitelisted.contains(name))
return true; // whitelisted by admin
// otherwise record the problem and refuse to execute that
boolean changed = rejected.add(subject.getClass());
if (changed) {
saveRejected();
}
return false;
return rule.isWhitelisted(subject,expected,context);
}
/**
* Returns the currently rejected callables.
*/
public List<RejectedCallable> getRejectedList() {
List<RejectedCallable> l = new ArrayList<RejectedCallable>();
synchronized (rejected) {
for (Class c : rejected) {
l.add(new RejectedCallable(c));
}
}
return l;
}
public boolean hasRejection() {
return !rejected.isEmpty();
}
/**
* Persists {@link #rejected}
*/
private void saveRejected() {
try {
synchronized (rejected) {
StringBuilder buf = new StringBuilder();
for (Class c : rejected) {
if (buf.length()>0) buf.append('\n');
buf.append(c.getName());
}
getRejectedFile().write(buf.toString());
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to persist "+getRejectedFile(), e);
}
}
public TextFile getRejectedFile() {
// 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/
return new TextFile(new File(jenkins.getRootDir(),"secrets/rejected-callables.txt"));
}
private void appendWhiteList(String addition) throws IOException {
String s = getWhitelistFile().read();
if (!s.endsWith("\n"))
s += "\n";
s+= addition;
setWhiteList(s);
}
private void setWhiteList(String newWhiteList) throws IOException {
jenkins.checkPermission(Jenkins.ADMINISTER);
getWhitelistFile().write(newWhiteList);
loadWhitelist();
}
public TextFile getWhitelistFile() {
return new TextFile(new File(jenkins.getRootDir(),"secrets/whitelisted-callables.txt"));
}
@RequirePOST
public HttpResponse doSubmit(StaplerRequest req) throws IOException {
jenkins.checkPermission(Jenkins.ADMINISTER);
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";
}
}
setWhiteList(whitelist);
return HttpResponses.redirectToDot();
}
/**
* Approves all the currently rejected subjects
*/
@RequirePOST
public HttpResponse doApproveAll() throws IOException {
synchronized (rejected) {
StringBuilder buf = new StringBuilder();
for (Class c : rejected) {
buf.append(c.getName()).append('\n');
}
appendWhiteList(buf.toString());
return HttpResponses.ok();
}
}
/**
* Approves specific callables by their names.
*/
@RequirePOST
public HttpResponse doApprove(@QueryParameter String value) throws IOException {
appendWhiteList(value);
return HttpResponses.ok();
}
@Initializer(after=EXTENSIONS_AUGMENTED,fatal=false)
public static void init() throws IOException {
CallableWhitelist.all().get(AdminCallableWhitelist.class).load();
}
public void load() throws IOException {
loadRejected();
loadWhitelist();
}
private void loadRejected() throws IOException {
if (getRejectedFile().exists()) {
for (String line : getRejectedFile().read().split("\n")) {
try {
Class<?> c = jenkins.pluginManager.uberClassLoader.loadClass(line.trim());
rejected.add(c);
} catch (ClassNotFoundException e) {
// no longer present in the system?
}
}
}
}
private void loadWhitelist() throws IOException {
if (getWhitelistFile().exists()) {
List<String> whitelist = new ArrayList<String>();
for (String line : getWhitelistFile().read().split("\n")) {
whitelist.add(line.trim());
}
this.whitelisted = ImmutableSet.copyOf(whitelist);
}
// remove whitelisted ones from the reject list
boolean changed = false;
synchronized (rejected) {
Iterator<Class> itr = rejected.iterator();
while (itr.hasNext()) {
Class next = itr.next();
if (whitelisted.contains(next.getName())) {
itr.remove();
changed = true;
}
}
}
if (changed)
saveRejected();
}
/**
* Restricts the access to administrator.
*/
@Override
public Object getTarget() {
jenkins.checkPermission(Jenkins.ADMINISTER);
return this;
}
public class RejectedCallable {
public final Class clazz;
public RejectedCallable(Class clazz) {
this.clazz = clazz;
}
public @CheckForNull PluginWrapper getPlugin() {
return jenkins.pluginManager.whichPlugin(clazz);
}
}
private static final Logger LOGGER = Logger.getLogger(AdminCallableWhitelist.class.getName());
}
@@ -14,32 +14,32 @@ import java.io.File;
* {@link FilePathFilter} that allows admins to whitelist specific file access.
*
* <p>
* This class is just a glue, and the real logic happens inside {@link AdminCallableMonitor}
* This class is just a glue, and the real logic happens inside {@link AdminWhitelistRule}
*
* @author Kohsuke Kawaguchi
* @since 1.THU
*/
public class AdminFilePathFilter extends ReflectiveFilePathFilter {
private final AdminCallableMonitor monitor;
private final AdminWhitelistRule rule;
public AdminFilePathFilter(AdminCallableMonitor monitor) {
this.monitor = monitor;
public AdminFilePathFilter(AdminWhitelistRule rule) {
this.rule = rule;
}
@Override
protected boolean op(String op, File path) throws SecurityException {
return monitor.checkFileAccess(op,path);
return rule.checkFileAccess(op,path);
}
@Extension
public static class ChannelConfiguratorImpl extends ChannelConfigurator {
@Inject
AdminCallableMonitor monitor;
AdminWhitelistRule rule;
@Override
public void onChannelBuilding(ChannelBuilder builder, @Nullable Object context) {
new AdminFilePathFilter(monitor).installTo(builder);
new AdminFilePathFilter(rule).installTo(builder);
}
}
}
@@ -0,0 +1,263 @@
package jenkins.security.admin;
import com.google.common.collect.ImmutableSet;
import hudson.CopyOnWrite;
import hudson.Extension;
import hudson.FilePath;
import hudson.Util;
import hudson.init.Initializer;
import hudson.util.HttpResponses;
import hudson.util.TextFile;
import jenkins.model.Jenkins;
import jenkins.security.CallableWhitelist;
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 javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import static hudson.init.InitMilestone.*;
/**
* Rules of whitelisting for {@link RoleSensitive} objects and {@link FilePath}s.
*
* @author Kohsuke Kawaguchi
*/
@Extension
public class AdminWhitelistRule implements StaplerProxy {
@Inject
Jenkins jenkins;
/**
* Ones that we rejected but want to run by admins.
*/
private final Set<Class> rejected = Collections.synchronizedSet(new LinkedHashSet<Class>());
@CopyOnWrite
private volatile Set<String> whitelisted = Collections.emptySet();
@CopyOnWrite
private final List<FilePathRule> filePathRules = Collections.emptyList();
public boolean checkFileAccess(String op, File path) throws SecurityException {
String pathStr = null;
for (FilePathRule rule : filePathRules) {
if (rule.op.matches(op)) {
if (pathStr==null)
// do not canonicalize nor absolutize, so that JENKINS_HOME that spans across
// multiple volumes via symlinks can look logically like one unit.
pathStr = path.getPath();
if (rule.path.matcher(pathStr).matches()) {
// exclusion rule is only to bypass later path rules within #filePathRules,
// and we still want other FilePathFilters to whitelist/blacklist access.
// therefore I'm not throwing a SecurityException here
return rule.allow;
}
}
}
return false;
}
public boolean isWhitelisted(RoleSensitive subject, Collection<Role> expected, Object context) {
String name = subject.getClass().getName();
if (whitelisted.contains(name))
return true; // whitelisted by admin
// otherwise record the problem and refuse to execute that
boolean changed = rejected.add(subject.getClass());
if (changed) {
saveRejected();
}
return false;
}
/**
* Returns the currently rejected callables.
*/
public List<RejectedCallable> getRejectedList() {
List<RejectedCallable> l = new ArrayList<RejectedCallable>();
synchronized (rejected) {
for (Class c : rejected) {
l.add(new RejectedCallable(c));
}
}
return l;
}
public boolean hasRejection() {
return !rejected.isEmpty();
}
/**
* Persists {@link #rejected}
*/
private void saveRejected() {
try {
synchronized (rejected) {
StringBuilder buf = new StringBuilder();
for (Class c : rejected) {
if (buf.length()>0) buf.append('\n');
buf.append(c.getName());
}
getRejectedFile().write(buf.toString());
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to persist "+getRejectedFile(), e);
}
}
public TextFile getRejectedFile() {
// 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/
return new TextFile(new File(jenkins.getRootDir(),"secrets/rejected-callables.txt"));
}
private void appendWhiteList(String addition) throws IOException {
String s = getWhitelistFile().read();
if (!s.endsWith("\n"))
s += "\n";
s+= addition;
setWhiteList(s);
}
private void setWhiteList(String newWhiteList) throws IOException {
jenkins.checkPermission(Jenkins.ADMINISTER);
getWhitelistFile().write(newWhiteList);
loadWhitelist();
}
public TextFile getWhitelistFile() {
return new TextFile(new File(jenkins.getRootDir(),"secrets/whitelisted-callables.txt"));
}
@RequirePOST
public HttpResponse doSubmit(StaplerRequest req) throws IOException {
jenkins.checkPermission(Jenkins.ADMINISTER);
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";
}
}
setWhiteList(whitelist);
return HttpResponses.redirectToDot();
}
/**
* Approves all the currently rejected subjects
*/
@RequirePOST
public HttpResponse doApproveAll() throws IOException {
synchronized (rejected) {
StringBuilder buf = new StringBuilder();
for (Class c : rejected) {
buf.append(c.getName()).append('\n');
}
appendWhiteList(buf.toString());
return HttpResponses.ok();
}
}
/**
* Approves specific callables by their names.
*/
@RequirePOST
public HttpResponse doApprove(@QueryParameter String value) throws IOException {
appendWhiteList(value);
return HttpResponses.ok();
}
@Initializer(after=EXTENSIONS_AUGMENTED,fatal=false)
public static void init() throws IOException {
CallableWhitelist.all().get(AdminCallableWhitelist.class).load();
}
public void load() throws IOException {
loadRejected();
loadWhitelist();
}
private void loadRejected() throws IOException {
if (getRejectedFile().exists()) {
for (String line : getRejectedFile().read().split("\n")) {
try {
Class<?> c = jenkins.pluginManager.uberClassLoader.loadClass(line.trim());
rejected.add(c);
} catch (ClassNotFoundException e) {
// no longer present in the system?
}
}
}
}
private void loadWhitelist() throws IOException {
if (getWhitelistFile().exists()) {
List<String> whitelist = new ArrayList<String>();
for (String line : getWhitelistFile().read().split("\n")) {
whitelist.add(line.trim());
}
this.whitelisted = ImmutableSet.copyOf(whitelist);
}
// remove whitelisted ones from the reject list
boolean changed = false;
synchronized (rejected) {
Iterator<Class> itr = rejected.iterator();
while (itr.hasNext()) {
Class next = itr.next();
if (whitelisted.contains(next.getName())) {
itr.remove();
changed = true;
}
}
}
if (changed)
saveRejected();
}
/**
* Restricts the access to administrator.
*/
@Override
public Object getTarget() {
jenkins.checkPermission(Jenkins.ADMINISTER);
return this;
}
private static final Logger LOGGER = Logger.getLogger(AdminWhitelistRule.class.getName());
}
@@ -0,0 +1,22 @@
package jenkins.security.admin;
import hudson.FilePath;
import java.util.regex.Pattern;
/**
* One entry of the {@link FilePath} access rule.
*
* @author Kohsuke Kawaguchi
*/
class FilePathRule {
final Pattern path;
final OpMatcher op;
final boolean allow;
FilePathRule(Pattern path, OpMatcher op, boolean allow) {
this.path = path;
this.op = op;
this.allow = allow;
}
}
@@ -0,0 +1,14 @@
package jenkins.security.admin;
import jenkins.ReflectiveFilePathFilter;
import java.io.File;
/**
* Tests a match against file operation name of {@link ReflectiveFilePathFilter#op(String, File)}.
*
* @author Kohsuke Kawaguchi
*/
interface OpMatcher {
boolean matches(String op);
}
@@ -0,0 +1,22 @@
package jenkins.security.admin;
import hudson.PluginWrapper;
import jenkins.model.Jenkins;
import javax.annotation.CheckForNull;
/**
* @author Kohsuke Kawaguchi
*/
public /*for Jelly*/ class RejectedCallable {
public final Class clazz;
/*package*/ RejectedCallable(Class clazz) {
this.clazz = clazz;
}
public @CheckForNull
PluginWrapper getPlugin() {
return Jenkins.getInstance().pluginManager.whichPlugin(clazz);
}
}