diff --git a/changelog.html b/changelog.html index 1b1128202ff893855342ab695219748cff82034d..41295cce38ff17cbbddfcab37c2e6a1b60ba8352 100644 --- a/changelog.html +++ b/changelog.html @@ -61,6 +61,9 @@ Upcoming changes
  • “Projects tied to slave” shows unrelated Maven module jobs. (issue 17451) +
  • + Executors running the builds can be now a subject of access control. + (issue 18285) diff --git a/core/pom.xml b/core/pom.xml index 880564c8c031c20f4f6846b258743ab17a657f9b..e89ab0599b72d4d09a1b16d91cb5d7942b43113d 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -192,13 +192,7 @@ THE SOFTWARE. com.infradna.tool bridge-method-annotation - 1.4 - - - annotation-indexer - org.jvnet.hudson - - + 1.8 @@ -655,7 +649,7 @@ THE SOFTWARE. com.infradna.tool bridge-method-injector - + 1.5-SNAPSHOT diff --git a/core/src/main/java/hudson/ExtensionListView.java b/core/src/main/java/hudson/ExtensionListView.java index 4c2e8d9604fc9a243a4db6540639c2fd3320daaf..95ea80068c636ffc393785fb27f53ce7e4517961 100644 --- a/core/src/main/java/hudson/ExtensionListView.java +++ b/core/src/main/java/hudson/ExtensionListView.java @@ -142,7 +142,7 @@ public class ExtensionListView { } @Override - public T[] toArray(T[] array) { + public T[] toArray(T[] array) { return storage().toArray(array); } diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java index 14571c350335e80ea2f3e91016aa7905faccea91..6ff9f95cdc80f31f9f1345364fcf14fd49ad6567 100644 --- a/core/src/main/java/hudson/model/AbstractProject.java +++ b/core/src/main/java/hudson/model/AbstractProject.java @@ -90,8 +90,12 @@ import jenkins.model.lazy.AbstractLazyLoadRunMap.Direction; import jenkins.scm.DefaultSCMCheckoutStrategyImpl; import jenkins.scm.SCMCheckoutStrategy; import jenkins.scm.SCMCheckoutStrategyDescriptor; +import jenkins.security.ProjectAuthenticator; +import jenkins.security.ProjectAuthenticatorConfiguration; +import jenkins.security.ProjectAuthenticatorConfiguration; import jenkins.util.TimeDuration; import net.sf.json.JSONObject; +import org.acegisecurity.Authentication; import org.acegisecurity.context.SecurityContext; import org.acegisecurity.context.SecurityContextHolder; import org.kohsuke.accmod.Restricted; @@ -1174,6 +1178,13 @@ public abstract class AbstractProject

    ,R extends A return this; } + /** + * Let the identity determined by {@link ProjectAuthenticator}. + */ + public Authentication getIdentity() { + return ProjectAuthenticatorConfiguration.get().authenticate(this); + } + /** * {@inheritDoc} * diff --git a/core/src/main/java/hudson/model/Executor.java b/core/src/main/java/hudson/model/Executor.java index b99b6fbb0296808ce7e4358b1f377ce7d7ff2198..f2aaf341cde9759d84350669b9fe7e50aac3392f 100644 --- a/core/src/main/java/hudson/model/Executor.java +++ b/core/src/main/java/hudson/model/Executor.java @@ -38,6 +38,8 @@ import hudson.security.ACL; import jenkins.model.InterruptedBuildAction; import jenkins.model.Jenkins; import org.acegisecurity.Authentication; +import org.acegisecurity.context.SecurityContext; +import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; @@ -236,10 +238,16 @@ public class Executor extends Thread implements ModelObject { ((Actionable) executable).addAction(action); } } - setName(threadName+" : executing "+executable.toString()); - if (LOGGER.isLoggable(FINE)) - LOGGER.log(FINE, getName()+" is now executing "+executable); - queue.execute(executable, task); + + final SecurityContext savedContext = ACL.impersonate(Tasks.getIdentityOf(task)); + try { + setName(threadName + " : executing " + executable.toString()); + if (LOGGER.isLoggable(FINE)) + LOGGER.log(FINE, getName()+" is now executing "+executable); + queue.execute(executable, task); + } finally { + SecurityContextHolder.setContext(savedContext); + } } catch (Throwable e) { // for some reason the executor died. this is really // a bug in the code, but we don't want the executor to die, diff --git a/core/src/main/java/hudson/model/Fingerprint.java b/core/src/main/java/hudson/model/Fingerprint.java index c1b40239aaa263207de189a3ce2de31af25ecd40..bf3b14cf8cd49567127511543b979c94fe777d54 100644 --- a/core/src/main/java/hudson/model/Fingerprint.java +++ b/core/src/main/java/hudson/model/Fingerprint.java @@ -794,21 +794,13 @@ public class Fingerprint implements ModelObject, Saveable { @Override public boolean add(FingerprintFacet e) { - try { - facets.add(e); - return true; - } catch (IOException x) { - throw new Error(x); - } + facets.add(e); + return true; } @Override public boolean remove(Object o) { - try { - return facets.remove((FingerprintFacet)o); - } catch (IOException x) { - throw new Error(x); - } + return facets.remove(o); } @Override diff --git a/core/src/main/java/hudson/model/Node.java b/core/src/main/java/hudson/model/Node.java index fe0a9dbcc8a51983f9dbd032fbcc0f233c5289e9..e8f7af4665461aa5d67053683b1206f2937ad8a1 100644 --- a/core/src/main/java/hudson/model/Node.java +++ b/core/src/main/java/hudson/model/Node.java @@ -62,6 +62,7 @@ import javax.annotation.CheckForNull; import jenkins.model.Jenkins; import jenkins.util.io.OnMaster; import net.sf.json.JSONObject; +import org.acegisecurity.Authentication; import org.jvnet.localizer.Localizable; import org.kohsuke.stapler.BindInterceptor; import org.kohsuke.stapler.Stapler; @@ -326,6 +327,13 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable if(l==null && getMode()== Mode.EXCLUSIVE) return CauseOfBlockage.fromMessage(Messages._Node_BecauseNodeIsReserved(getNodeName())); // this node is reserved for tasks that are tied to it + Authentication identity = item.task.getIdentity(); + if (!getACL().hasPermission(identity,AbstractProject.BUILD)) { + // doesn't have a permission + // TODO: does it make more sense to define a separate permission? + return CauseOfBlockage.fromMessage(Messages._Node_LackingBuildPermission(identity.getName(),getNodeName())); + } + // Check each NodeProperty to see whether they object to this node // taking the task for (NodeProperty prop: getNodeProperties()) { diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index ef98acb5848f613eb7e08e5958d22abe51cc3e8f..bbc78d44542f2c5cc56f6f655847dd762a323023 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -1523,7 +1523,7 @@ public abstract class Run ,RunT extends Run - * This mapping is done under two constraints: + * This mapping is done under the following constraints: * *

      *
    • @@ -65,6 +67,9 @@ import static java.lang.Math.*; * See {@link SubTask#getSameNodeConstraint()} *
    • * Label constraint. {@link SubTask}s can specify that it can be only run on nodes that has the label. + *
    • + * Permission constraint. {@link SubTask}s have {@linkplain SubTask#getIdentity() identities} that need to have + * permissions to build on the node. *
    * *

    @@ -111,6 +116,7 @@ public class MappingWorksheet { public final int index; public final Computer computer; public final Node node; + public final ACL nodeAcl; private ExecutorChunk(List base, int index) { super(base); @@ -118,14 +124,25 @@ public class MappingWorksheet { assert !base.isEmpty(); computer = base.get(0).getExecutor().getOwner(); node = computer.getNode(); + nodeAcl = node.getACL(); } /** * Is this executor chunk and the given work chunk compatible? Can the latter be run on the former? */ public boolean canAccept(WorkChunk c) { - return this.size() >= c.size() - && (c.assignedLabel==null || c.assignedLabel.contains(node)); + if (this.size() { public final int index; diff --git a/core/src/main/java/hudson/model/queue/QueueTaskFilter.java b/core/src/main/java/hudson/model/queue/QueueTaskFilter.java index b8b6e240c85ceb330ccc76c3d9a663936f92be3f..2399d103f9cd8b20ddd843c64a855bb3708931e0 100644 --- a/core/src/main/java/hudson/model/queue/QueueTaskFilter.java +++ b/core/src/main/java/hudson/model/queue/QueueTaskFilter.java @@ -30,6 +30,7 @@ import hudson.model.Queue; import hudson.model.Queue.Executable; import hudson.model.Queue.Task; import hudson.model.ResourceList; +import org.acegisecurity.Authentication; import java.io.IOException; import java.util.Collection; @@ -118,4 +119,8 @@ public abstract class QueueTaskFilter implements Queue.Task { public Object getSameNodeConstraint() { return base.getSameNodeConstraint(); } + + public Authentication getIdentity() { + return base.getIdentity(); + } } diff --git a/core/src/main/java/hudson/model/queue/SubTask.java b/core/src/main/java/hudson/model/queue/SubTask.java index 315adece5177b629a28b87e3064b119da131c9ac..6a31232625f2e31c3aa1b02392c264e74fea8d21 100644 --- a/core/src/main/java/hudson/model/queue/SubTask.java +++ b/core/src/main/java/hudson/model/queue/SubTask.java @@ -29,7 +29,10 @@ import hudson.model.Node; import hudson.model.Queue.Executable; import hudson.model.Queue.Task; import hudson.model.ResourceActivity; +import hudson.security.ACL; +import org.acegisecurity.Authentication; +import javax.annotation.Nonnull; import java.io.IOException; /** @@ -83,4 +86,17 @@ public interface SubTask extends ResourceActivity { * colocation constraint. */ Object getSameNodeConstraint(); + + /** + * Returns the identity that this task carries when it runs, for the purpose of access control. + * + * When the task execution touches other objects inside Jenkins, the access control is performed + * based on whether this {@link Authentication} is allowed to use them. Implementers, if you are unsure, + * return the identity of the user who queued the task, or {@link ACL#SYSTEM} to bypass the access control + * and run as the super user. + * + * @since 1.520 + * @see Tasks#getIdentityOf(SubTask) + */ + @Nonnull Authentication getIdentity(); } diff --git a/core/src/main/java/hudson/model/queue/Tasks.java b/core/src/main/java/hudson/model/queue/Tasks.java index cfa99357a0e7851a6bd0644990c32dfe8a1702aa..f4a1987782a64c9b7d28f4a7075631a05d2501c3 100644 --- a/core/src/main/java/hudson/model/queue/Tasks.java +++ b/core/src/main/java/hudson/model/queue/Tasks.java @@ -24,6 +24,8 @@ package hudson.model.queue; import hudson.model.Queue.Task; +import hudson.security.ACL; +import org.acegisecurity.Authentication; import java.util.Collection; import java.util.Collections; @@ -83,4 +85,21 @@ public class Tasks { return (Task)t; } } + + /** + * A pointless function to work around what appears to be a HotSpot problem. See JENKINS-5756 and bug 6933067 + * on BugParade for more details. + */ + private static Authentication _getIdentityOf(SubTask t) { + return t.getIdentity(); + } + + public static Authentication getIdentityOf(SubTask t) { + try { + return _getIdentityOf(t); + } catch (AbstractMethodError e) { + return ACL.SYSTEM; + } + } + } diff --git a/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java b/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java index 31dd6855e38c2f3d6341446c147f0b85a31be1ae..9ebe8aa81fc78237c9877ec5f1c20dc2b6597d52 100644 --- a/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java +++ b/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java @@ -50,6 +50,8 @@ import org.kohsuke.stapler.StaplerResponse; /** * Security configuration. * + * For historical reasons, most of the actual configuration values are stored in {@link Jenkins}. + * * @author Kohsuke Kawaguchi */ @Extension(ordinal = Integer.MAX_VALUE - 210) diff --git a/core/src/main/java/hudson/util/CopyOnWriteList.java b/core/src/main/java/hudson/util/CopyOnWriteList.java index 9ba5de2bf31213fc0c4ce909b1a634d099d08dd2..d5a5741277744e10c149429c3bf763df6ae3476f 100644 --- a/core/src/main/java/hudson/util/CopyOnWriteList.java +++ b/core/src/main/java/hudson/util/CopyOnWriteList.java @@ -138,7 +138,7 @@ public class CopyOnWriteList implements Iterable { this.core = new ArrayList(); } - public E[] toArray(E[] array) { + public E[] toArray(E[] array) { return core.toArray(array); } diff --git a/core/src/main/java/hudson/util/PersistedList.java b/core/src/main/java/hudson/util/PersistedList.java index 89325d0c058a165e99ca3503b420bf712a4162e6..41b268e1a890e62ab5e9396911639add25915805 100644 --- a/core/src/main/java/hudson/util/PersistedList.java +++ b/core/src/main/java/hudson/util/PersistedList.java @@ -23,6 +23,7 @@ */ package hudson.util; +import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.UnmarshallingContext; @@ -34,6 +35,7 @@ import hudson.model.Describable; import hudson.model.Saveable; import java.io.IOException; +import java.util.AbstractList; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; @@ -45,7 +47,7 @@ import java.util.List; * @author Kohsuke Kawaguchi * @since 1.MULTISOURCE */ -public class PersistedList implements Iterable { +public class PersistedList extends AbstractList { protected final CopyOnWriteList data = new CopyOnWriteList(); protected Saveable owner = Saveable.NOOP; @@ -64,14 +66,18 @@ public class PersistedList implements Iterable { this.owner = owner; } - public void add(T item) throws IOException { + @WithBridgeMethods(void.class) + public boolean add(T item) { data.add(item); - onModified(); + _onModified(); + return true; } - public void addAll(Collection items) throws IOException { + @WithBridgeMethods(void.class) + public boolean addAll(Collection items) { data.addAll(items); - onModified(); + _onModified(); + return true; } public void replaceBy(Collection col) throws IOException { @@ -133,9 +139,9 @@ public class PersistedList implements Iterable { data.replaceBy(copy); } - public boolean remove(T o) throws IOException { - boolean b = data.remove(o); - if (b) onModified(); + public boolean remove(Object o) { + boolean b = data.remove((T)o); + if (b) _onModified(); return b; } @@ -167,6 +173,17 @@ public class PersistedList implements Iterable { owner.save(); } + /** + * Version of {@link #_onModified()} that swallows an exception for compliance with {@link List}. + */ + private void _onModified() { + try { + onModified(); + } catch (IOException e) { + throw new Error(e); + } + } + /** * Returns the snapshot view of instances as list. */ @@ -177,7 +194,7 @@ public class PersistedList implements Iterable { /** * Gets all the {@link Describable}s in an array. */ - public T[] toArray(T[] array) { + public T[] toArray(T[] array) { return data.toArray(array); } diff --git a/core/src/main/java/jenkins/security/ProjectAuthenticator.java b/core/src/main/java/jenkins/security/ProjectAuthenticator.java new file mode 100644 index 0000000000000000000000000000000000000000..5942d465411b5b6e1d2662cd40dee50053e03a9e --- /dev/null +++ b/core/src/main/java/jenkins/security/ProjectAuthenticator.java @@ -0,0 +1,37 @@ +package jenkins.security; + +import hudson.ExtensionPoint; +import hudson.model.AbstractBuild; +import hudson.model.AbstractDescribableImpl; +import hudson.model.AbstractProject; +import hudson.security.ACL; +import org.acegisecurity.Authentication; + +/** + * Extension point to run {@link AbstractBuild}s under a specific identity for better access control. + * + * @author Kohsuke Kawaguchi + * @since 1.520 + * @see ProjectAuthenticatorConfiguration + * @see AbstractProject#getIdentity() + */ +public abstract class ProjectAuthenticator extends AbstractDescribableImpl implements ExtensionPoint { + /** + * Determines the identity in which the build will run as. + * + * @param project + * The project to be built. + * + * @return + * returning non-null will determine the identity. If null is returned, the next + * configured {@link ProjectAuthenticator} will be given a chance to authenticate + * the executor. If everything fails, fall back to the historical behaviour of + * {@link ACL#SYSTEM}. + */ + public abstract Authentication authenticate(AbstractProject project); + + @Override + public ProjectAuthenticatorDescriptor getDescriptor() { + return (ProjectAuthenticatorDescriptor)super.getDescriptor(); + } +} diff --git a/core/src/main/java/jenkins/security/ProjectAuthenticatorConfiguration.java b/core/src/main/java/jenkins/security/ProjectAuthenticatorConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..b60db097ef657fb3c5c04f4f3ff9933906717f63 --- /dev/null +++ b/core/src/main/java/jenkins/security/ProjectAuthenticatorConfiguration.java @@ -0,0 +1,61 @@ +package jenkins.security; + +import hudson.Extension; +import hudson.model.AbstractProject; +import hudson.security.ACL; +import hudson.util.DescribableList; +import jenkins.model.GlobalConfiguration; +import jenkins.model.GlobalConfigurationCategory; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; +import org.acegisecurity.Authentication; +import org.kohsuke.stapler.StaplerRequest; + +import java.io.IOException; + +/** + * Show the {@link ProjectAuthenticator} configurations on the system config page. + * + * @author Kohsuke Kawaguchi + */ +@Extension +public class ProjectAuthenticatorConfiguration extends GlobalConfiguration { + private final DescribableList authenticators + = new DescribableList(this); + + public ProjectAuthenticatorConfiguration() { + load(); + } + + @Override + public GlobalConfigurationCategory getCategory() { + return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); + } + + public DescribableList getAuthenticators() { + return authenticators; + } + + @Override + public boolean configure(StaplerRequest req, JSONObject json) throws FormException { + try { + authenticators.rebuildHetero(req,json, ProjectAuthenticatorDescriptor.all(),"authenticators"); + return true; + } catch (IOException e) { + throw new FormException(e,"authenticators"); + } + } + + public Authentication authenticate(AbstractProject project) { + for (ProjectAuthenticator auth : get().getAuthenticators()) { + Authentication a = auth.authenticate(project); + if (a!=null) + return a; + } + return ACL.SYSTEM; + } + + public static ProjectAuthenticatorConfiguration get() { + return Jenkins.getInstance().getInjector().getInstance(ProjectAuthenticatorConfiguration.class); + } +} diff --git a/core/src/main/java/jenkins/security/ProjectAuthenticatorDescriptor.java b/core/src/main/java/jenkins/security/ProjectAuthenticatorDescriptor.java new file mode 100644 index 0000000000000000000000000000000000000000..7177644db3da79f8700d837acd3f0879e38b0bb8 --- /dev/null +++ b/core/src/main/java/jenkins/security/ProjectAuthenticatorDescriptor.java @@ -0,0 +1,19 @@ +package jenkins.security; + +import hudson.DescriptorExtensionList; +import hudson.model.Descriptor; +import jenkins.model.Jenkins; + +/** + * {@link Descriptor} for {@link ProjectAuthenticator}. + * + * @author Kohsuke Kawaguchi + * @since 1.520 + */ +public abstract class ProjectAuthenticatorDescriptor extends Descriptor { + // nothing defined here yet + + public static DescriptorExtensionList all() { + return Jenkins.getInstance().getDescriptorList(ProjectAuthenticator.class); + } +} diff --git a/core/src/main/resources/hudson/model/Messages.properties b/core/src/main/resources/hudson/model/Messages.properties index 6f5df4cbdee1b23f562d23e434c9b1315541c41a..522758b7328ba8529972753967429b224dcf4546 100644 --- a/core/src/main/resources/hudson/model/Messages.properties +++ b/core/src/main/resources/hudson/model/Messages.properties @@ -184,8 +184,6 @@ Label.InvalidLabel=invalid label Label.ProvisionedFrom=Provisioned from {0} ManageJenkinsAction.DisplayName=Manage Jenkins MultiStageTimeSeries.EMPTY_STRING= -Node.BecauseNodeIsReserved={0} is reserved for jobs tied to it -Node.LabelMissing={0} doesn''t have label {1} Queue.AllNodesOffline=All nodes of label ''{0}'' are offline Queue.BlockedBy=Blocked by {0} Queue.HudsonIsAboutToShutDown=Jenkins is about to shut down @@ -309,6 +307,9 @@ ChoiceParameterDefinition.DisplayName=Choice RunParameterDefinition.DisplayName=Run Parameter PasswordParameterDefinition.DisplayName=Password Parameter +Node.BecauseNodeIsReserved={0} is reserved for jobs tied to it +Node.LabelMissing={0} doesn''t have label {1} +Node.LackingBuildPermission={0} doesn''t have a permission to run on {1} Node.Mode.NORMAL=Utilize this slave as much as possible Node.Mode.EXCLUSIVE=Leave this machine for tied jobs only diff --git a/core/src/main/resources/jenkins/security/ProjectAuthenticator/config.groovy b/core/src/main/resources/jenkins/security/ProjectAuthenticator/config.groovy new file mode 100644 index 0000000000000000000000000000000000000000..41d8916d73481003237a1a0e27abda4f994263b9 --- /dev/null +++ b/core/src/main/resources/jenkins/security/ProjectAuthenticator/config.groovy @@ -0,0 +1,2 @@ +package jenkins.security.ProjectAuthenticator; +// the default is empty configuration \ No newline at end of file diff --git a/core/src/main/resources/jenkins/security/ProjectAuthenticatorConfiguration/config.groovy b/core/src/main/resources/jenkins/security/ProjectAuthenticatorConfiguration/config.groovy new file mode 100644 index 0000000000000000000000000000000000000000..d1662c00caca2d4a49bd98fc4de1e88097d2489b --- /dev/null +++ b/core/src/main/resources/jenkins/security/ProjectAuthenticatorConfiguration/config.groovy @@ -0,0 +1,36 @@ +/* + * The MIT License + * + * Copyright (c) 2011, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.ProjectAuthenticatorConfiguration + +import jenkins.security.ProjectAuthenticatorDescriptor; + +f=namespace(lib.FormTagLib) + +if (!ProjectAuthenticatorDescriptor.all().isEmpty()) { + f.section(title:_("Access Control for Builds")) { + f.block() { + f.repeatableHeteroProperty(field:"authenticators",hasHeader:true) + } + } +} diff --git a/test/src/test/java/hudson/model/QueueTest.java b/test/src/test/java/hudson/model/QueueTest.java index c19fc4e469907f0f88a71da80cb7d9493ea498e8..86c3125ffa27eabd1a796b08b2e44b08dee4bb71 100644 --- a/test/src/test/java/hudson/model/QueueTest.java +++ b/test/src/test/java/hudson/model/QueueTest.java @@ -33,6 +33,10 @@ import hudson.matrix.TextAxis; import hudson.model.Cause.*; import hudson.model.Queue.*; import hudson.model.queue.QueueTaskFuture; +import hudson.security.ACL; +import hudson.security.GlobalMatrixAuthorizationStrategy; +import hudson.security.SparseACL; +import hudson.slaves.DumbSlave; import hudson.tasks.Shell; import hudson.triggers.SCMTrigger.SCMTriggerCause; import hudson.triggers.TimerTrigger.TimerTriggerCause; @@ -43,6 +47,13 @@ import hudson.matrix.LabelAxis; import hudson.matrix.MatrixRun; import hudson.slaves.DummyCloudImpl; import hudson.slaves.NodeProvisioner; +import jenkins.model.Jenkins; +import jenkins.security.ProjectAuthenticator; +import jenkins.security.ProjectAuthenticatorConfiguration; +import org.acegisecurity.Authentication; +import org.acegisecurity.GrantedAuthority; +import org.acegisecurity.acls.sid.PrincipalSid; +import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; @@ -51,11 +62,13 @@ import org.jvnet.hudson.test.Bug; import org.jvnet.hudson.test.HudsonTestCase; import org.jvnet.hudson.test.SequenceLock; import org.jvnet.hudson.test.TestBuilder; +import org.jvnet.hudson.test.TestExtension; import org.mortbay.jetty.Server; import org.mortbay.jetty.bio.SocketConnector; import org.mortbay.jetty.servlet.ServletHandler; import org.mortbay.jetty.servlet.ServletHolder; +import javax.inject.Inject; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -354,4 +367,87 @@ public class QueueTest extends HudsonTestCase { FreeStyleBuild b2 = assertBuildStatusSuccess(v); assertSame(b,b2); } + + @Inject + ProjectAuthenticatorConfiguration pac; + + /** + * Make sure that the running build actually carries an credential. + */ + public void testAccessControl() throws Exception { + configureUserRealm(); + pac.getAuthenticators().add(new ProjectAuthenticatorImpl()); + FreeStyleProject p = createFreeStyleProject(); + p.getBuildersList().add(new TestBuilder() { + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + assertEquals(alice,Jenkins.getAuthentication()); + return true; + } + }); + assertBuildStatusSuccess(p.scheduleBuild2(0)); + } + + @TestExtension + public static class ProjectAuthenticatorImpl extends ProjectAuthenticator { + + @Override + public Authentication authenticate(AbstractProject project) { + return alice; + } + } + + private static Authentication alice = new UsernamePasswordAuthenticationToken("alice","alice",new GrantedAuthority[0]); + + + /** + * Make sure that the slave assignment honors the permissions. + * + * We do this test by letting a build run twice to determine its natural home, + * and then introduce a security restriction to prohibit that. + */ + public void testPermissionSensitiveSlaveAllocations() throws Exception { + jenkins.setNumExecutors(0); // restrict builds to those slaves + DumbSlave s1 = createSlave(); + DumbSlave s2 = createSlave(); + + configureUserRealm(); + pac.getAuthenticators().add(new ProjectAuthenticatorImpl()); + FreeStyleProject p = createFreeStyleProject(); + p.getBuildersList().add(new TestBuilder() { + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + assertEquals(alice,Jenkins.getAuthentication()); + return true; + } + }); + + final FreeStyleBuild b1 = assertBuildStatusSuccess(p.scheduleBuild2(0)); + final FreeStyleBuild b2 = assertBuildStatusSuccess(p.scheduleBuild2(0)); + + // scheduling algorithm would prefer running the same job on the same node + assertSame(b1.getBuiltOn(),b2.getBuiltOn()); + + // ACL that allow anyone to do anything except Alice can't build. + final SparseACL alicCantBuild = new SparseACL(null); + alicCantBuild.add(new PrincipalSid(alice), AbstractProject.BUILD, false); + alicCantBuild.add(new PrincipalSid("anonymous"), Jenkins.ADMINISTER, true); + + GlobalMatrixAuthorizationStrategy auth = new GlobalMatrixAuthorizationStrategy() { + @Override + public ACL getACL(Node node) { + if (node==b1.getBuiltOn()) + return alicCantBuild; + return super.getACL(node); + } + }; + auth.add(Jenkins.ADMINISTER,"anonymous"); + jenkins.setAuthorizationStrategy(auth); + + // now that we prohibit alice to do a build on the same node, the build should run elsewhere + for (int i=0; i<3; i++) { + FreeStyleBuild b3 = assertBuildStatusSuccess(p.scheduleBuild2(0)); + assertNotSame(b3.getBuiltOnStr(), b1.getBuiltOnStr()); + } + } }