Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-40533] Allow definition of sandboxed libraries at global scope #129

Merged
merged 11 commits into from
Jun 17, 2024
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
<changelist>999999-SNAPSHOT</changelist>
<jenkins.version>2.414.3</jenkins.version>
<gitHubRepo>jenkinsci/${project.artifactId}-plugin</gitHubRepo>
<useBeta>true</useBeta>
</properties>
<dependencyManagement>
<dependencies>
Expand Down Expand Up @@ -135,6 +136,13 @@
</dependency>

<!-- test only plugins -->
<dependency>
<!-- GlobalUntrustedLibrariesTest#configRoundtrip -->
<groupId>io.jenkins.plugins</groupId>
<artifactId>manage-permission</artifactId>
<version>1.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-support</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* The MIT License
*
* Copyright 2024 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 org.jenkinsci.plugins.workflow.libs;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.model.ItemGroup;
import hudson.model.Job;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import jenkins.model.GlobalConfiguration;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;

/**
* Common code between {@link GlobalLibraries} and {@link GlobalUntrustedLibraries}.
*/
public abstract class AbstractGlobalLibraries extends GlobalConfiguration {
private List<LibraryConfiguration> libraries = new ArrayList<>();

protected AbstractGlobalLibraries() {
load();
}

public abstract String getDescription();

public List<LibraryConfiguration> getLibraries() {
return libraries;
}

public void setLibraries(List<LibraryConfiguration> libraries) {
this.libraries = libraries;
save();
}

@Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
if (Jenkins.get().hasPermission(getRequiredGlobalConfigPagePermission())) {
setLibraries(Collections.emptyList()); // allow last library to be deleted
return super.configure(req, json);
} else {
return true;
}
}

abstract static class AbstractForJob extends LibraryResolver {
@NonNull
protected abstract AbstractGlobalLibraries getConfiguration();

@NonNull @Override public final Collection<LibraryConfiguration> forJob(@NonNull Job<?,?> job, @NonNull Map<String,String> libraryVersions) {
return getLibraries();
}

@NonNull @Override public final Collection<LibraryConfiguration> fromConfiguration(@NonNull StaplerRequest request) {
if (Jenkins.get().hasPermission(getConfiguration().getRequiredGlobalConfigPagePermission())) {
return getLibraries();
}
return Collections.emptySet();
}

@NonNull @Override public final Collection<LibraryConfiguration> suggestedConfigurations(@NonNull ItemGroup<?> group) {
return getLibraries();
}

private List<LibraryConfiguration> getLibraries() {
return getConfiguration()
.getLibraries()
.stream()
.map(this::mayWrapLibrary)
.collect(Collectors.toList());
}

@NonNull
protected abstract LibraryConfiguration mayWrapLibrary(@NonNull LibraryConfiguration library);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,78 +24,49 @@

package org.jenkinsci.plugins.workflow.libs;

import hudson.Extension;
import hudson.model.ItemGroup;
import hudson.model.Job;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import edu.umd.cs.findbugs.annotations.NonNull;
import jenkins.model.GlobalConfiguration;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
import hudson.Extension;
import hudson.ExtensionList;

/**
* Manages libraries available to any job in the system.
*/
@Extension public class GlobalLibraries extends GlobalConfiguration {

public static @NonNull GlobalLibraries get() {
GlobalLibraries instance = GlobalConfiguration.all().get(GlobalLibraries.class);
if (instance == null) { // TODO would be useful to have an ExtensionList.getOrFail
throw new IllegalStateException();
}
return instance;
}

private List<LibraryConfiguration> libraries = new ArrayList<>();
@Extension public class GlobalLibraries extends AbstractGlobalLibraries {

public GlobalLibraries() {
load();
super();
}

public List<LibraryConfiguration> getLibraries() {
return libraries;
@Override
public String getDescription() {
return Messages.GlobalLibraries_Description();
}

public void setLibraries(List<LibraryConfiguration> libraries) {
this.libraries = libraries;
save();
@NonNull
@Override
public String getDisplayName() {
return Messages.GlobalLibraries_DisplayName();
}

@Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
if (Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
setLibraries(Collections.emptyList()); // allow last library to be deleted
return super.configure(req, json);
} else {
return true;
}
public static @NonNull GlobalLibraries get() {
return ExtensionList.lookupSingleton(GlobalLibraries.class);
}

@Extension(ordinal=0) public static class ForJob extends LibraryResolver {

@Override public boolean isTrusted() {
return true;
}

@NonNull @Override public Collection<LibraryConfiguration> forJob(@NonNull Job<?,?> job, @NonNull Map<String,String> libraryVersions) {
return GlobalLibraries.get().getLibraries();
@Extension(ordinal=0) public static class ForJob extends AbstractForJob {
@NonNull
protected GlobalLibraries getConfiguration() {
return get();
}

@NonNull @Override public Collection<LibraryConfiguration> fromConfiguration(@NonNull StaplerRequest request) {
if (Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
return GlobalLibraries.get().getLibraries();
}
return Collections.emptySet();
@Override
public boolean isTrusted() {
return true;
}

@NonNull @Override public Collection<LibraryConfiguration> suggestedConfigurations(@NonNull ItemGroup<?> group) {
return GlobalLibraries.get().getLibraries();
@NonNull
@Override
protected LibraryConfiguration mayWrapLibrary(@NonNull LibraryConfiguration library) {
return library;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* The MIT License
*
* Copyright 2024 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 org.jenkinsci.plugins.workflow.libs;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.security.Permission;
import jenkins.model.Jenkins;

/**
* Manages untrusted libraries available to any job in the system.
*/
@Extension public class GlobalUntrustedLibraries extends AbstractGlobalLibraries {

public GlobalUntrustedLibraries() {
super();
}

@Override
public String getDescription() {
return Messages.GlobalUntrustedLibraries_Description();
}

@NonNull
@Override
public String getDisplayName() {
return Messages.GlobalUntrustedLibraries_DisplayName();
}

public static @NonNull GlobalUntrustedLibraries get() {
return ExtensionList.lookupSingleton(GlobalUntrustedLibraries.class);
}

@NonNull
@Override
public Permission getRequiredGlobalConfigPagePermission() {
return Jenkins.MANAGE;
}

@Extension(ordinal=0) public static class ForJob extends AbstractForJob {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this implementation needs to wrap the returned libraries in ResolvedLibraryConfiguration like FolderLibraries.ForJob to avoid introducing a new security issue like what was fixed by ace0de3.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the trust flag is enough though, I am not sure. Either way I would look at old security fixes here carefully to see if anything extra needs to be done to separate the two global configurations in all contexts.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, this is addressed in 98322fb

Copy link
Member

@dwnusbaum dwnusbaum Jun 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the old fix in more detail, I think it was probably fine as you had it originally because of this line:

I think folder-based libraries just needed special treatment to distinguish between folders with the same name defined at different levels since they use the same LibraryResolver class. Since the new untrusted global libraries use a distinct resolver class and only exist in a single place, things should be fine without wrapping LibraryConfigurations. Your changes use getClass().getName() anyway so the behavior should be identical. I didn't try testing it though.

@NonNull
protected GlobalUntrustedLibraries getConfiguration() {
return get();
}

@Override
public boolean isTrusted() {
return false;
}

@NonNull
@Override
protected LibraryConfiguration mayWrapLibrary(@NonNull LibraryConfiguration library) {
return new ResolvedLibraryConfiguration(library, getClass().getName());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ THE SOFTWARE.

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<j:if test="${h.hasPermission(app.ADMINISTER)}">
<f:section title="${%Global Pipeline Libraries}">
<j:if test="${h.hasPermission(instance.requiredGlobalConfigPagePermission)}">
<f:section title="${instance.displayName}">
<f:block>
<j:out value="${%blurb}"/>
<j:out value="${instance.description}"/>
</f:block>
<f:entry>
<f:repeatableProperty field="libraries" header="${%Library}">
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ LibraryDecorator.could_not_find_any_definition_of_librari=Could not find any def
ResourceStep.library_resource_ambiguous_among_librari=Library resource {0} ambiguous among libraries {1}
ResourceStep.no_such_library_resource_could_be_found_=No such library resource {0} could be found.
SCMSourceRetriever.library_path_no_double_dot=Library path may not contain ".."
GlobalLibraries.DisplayName=Global Trusted Pipeline Libraries
GlobalLibraries.Description=Sharable libraries available to any Pipeline jobs running on this system. \
These libraries will be trusted, meaning they run without \u201csandbox\u201d restrictions and may use <code>@Grab</code>.
GlobalUntrustedLibraries.Description=Sharable libraries available to any Pipeline jobs running on this system. \
These libraries will be untrusted, meaning they run with \u201csandbox\u201d restrictions and cannot use <code>@Grab</code>.
GlobalUntrustedLibraries.DisplayName=Global Untrusted Pipeline Libraries
Original file line number Diff line number Diff line change
Expand Up @@ -172,19 +172,10 @@ public class FolderLibrariesTest {

/** @see GrapeTest#outsideLibrarySandbox */
@Test public void noGrape() throws Exception {
sampleRepo1.init();
sampleRepo1.write("src/pkg/Wrapper.groovy",
"package pkg\n" +
"@Grab('commons-primitives:commons-primitives:1.0')\n" +
"import org.apache.commons.collections.primitives.ArrayIntList\n" +
"class Wrapper {static def list() {new ArrayIntList()}}");
sampleRepo1.git("add", "src");
sampleRepo1.git("commit", "--message=init");
Folder d = r.jenkins.createProject(Folder.class, "d");
d.getProperties().add(new FolderLibraries(Collections.singletonList(new LibraryConfiguration("grape", new SCMSourceRetriever(new GitSCMSource(null, sampleRepo1.toString(), "", "*", "", true))))));
d.getProperties().add(new FolderLibraries(List.of(LibraryTestUtils.defineLibraryUsingGrab("grape", sampleRepo1))));
WorkflowJob p = d.createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition("@Library('grape@master') import pkg.Wrapper; echo(/should not have been able to run ${pkg.Wrapper.list()}/)", true));
ScriptApproval.get().approveSignature("new org.apache.commons.collections.primitives.ArrayIntList");
r.assertLogContains("Annotation Grab cannot be used in the sandbox", r.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0)));
}

Expand Down
Loading