/*
* The MIT License
*
* Copyright (c) 2015 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 hudson.model;
import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.security.AuthorizationMatrixProperty;
import hudson.security.Permission;
import hudson.security.ProjectMatrixAuthorizationStrategy;
import hudson.tasks.ArtifactArchiver;
import hudson.tasks.Fingerprinter;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.Jenkins;
import org.junit.Rule;
import org.junit.Test;
import org.junit.Before;
import org.jvnet.hudson.test.CreateFileBuilder;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.MockFolder;
import org.jvnet.hudson.test.SecuredMockFolder;
import org.jvnet.hudson.test.WorkspaceCopyFileBuilder;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
//TODO: Refactoring: Tests should be exchanged with FingerprinterTest somehow
/**
* Tests for the {@link Fingerprint} class.
* @author Oleg Nenashev
*/
public class FingerprintTest {
@Rule
public JenkinsRule rule = new JenkinsRule();
@Before
public void setupRealm() {
rule.jenkins.setSecurityRealm(rule.createDummySecurityRealm());
}
@Test
public void shouldCreateFingerprintsForWorkspace() throws Exception {
FreeStyleProject project = rule.createFreeStyleProject();
project.getBuildersList().add(new CreateFileBuilder("test.txt", "Hello, world!"));
project.getPublishersList().add(new Fingerprinter("test.txt", false));
FreeStyleBuild build = rule.buildAndAssertSuccess(project);
Fingerprint fp = getFingerprint(build, "test.txt");
}
@Test
public void shouldCreateFingerprintsForArtifacts() throws Exception {
FreeStyleProject project = rule.createFreeStyleProject();
project.getBuildersList().add(new CreateFileBuilder("test.txt", "Hello, world!"));
ArtifactArchiver archiver = new ArtifactArchiver("test.txt");
archiver.setFingerprint(true);
project.getPublishersList().add(archiver);
FreeStyleBuild build = rule.buildAndAssertSuccess(project);
Fingerprint fp = getFingerprint(build, "test.txt");
}
@Test
public void shouldCreateUsageLinks() throws Exception {
// Project 1
FreeStyleProject project = createAndRunProjectWithPublisher("fpProducer", "test.txt");
final FreeStyleBuild build = project.getLastBuild();
// Project 2
FreeStyleProject project2 = rule.createFreeStyleProject();
project2.getBuildersList().add(new WorkspaceCopyFileBuilder("test.txt", project.getName(), build.getNumber()));
project2.getPublishersList().add(new Fingerprinter("test.txt"));
FreeStyleBuild build2 = rule.buildAndAssertSuccess(project2);
Fingerprint fp = getFingerprint(build, "test.txt");
// Check references
Fingerprint.BuildPtr original = fp.getOriginal();
assertEquals("Original reference contains a wrong job name", project.getName(), original.getName());
assertEquals("Original reference contains a wrong build number", build.getNumber(), original.getNumber());
Hashtable<String, Fingerprint.RangeSet> usages = fp.getUsages();
assertTrue("Usages do not have a reference to " + project, usages.containsKey(project.getName()));
assertTrue("Usages do not have a reference to " + project2, usages.containsKey(project2.getName()));
}
@Test
@Issue("SECURITY-153")
public void shouldBeUnableToSeeJobsIfNoPermissions() throws Exception {
// Project 1
final FreeStyleProject project1 = createAndRunProjectWithPublisher("fpProducer", "test.txt");
final FreeStyleBuild build = project1.getLastBuild();
// Project 2
final FreeStyleProject project2 = rule.createFreeStyleProject("project2");
project2.getBuildersList().add(new WorkspaceCopyFileBuilder("test.txt", project1.getName(), build.getNumber()));
project2.getPublishersList().add(new Fingerprinter("test.txt"));
final FreeStyleBuild build2 = rule.buildAndAssertSuccess(project2);
// Get fingerprint
final Fingerprint fp = getFingerprint(build, "test.txt");
// Init Users
User user1 = User.get("user1"); // can access project1
User user2 = User.get("user2"); // can access project2
User user3 = User.get("user3"); // cannot access anything
// Project permissions
setupProjectMatrixAuthStrategy(Jenkins.READ);
setJobPermissionsOnce(project1, "user1", Item.READ, Item.DISCOVER);
setJobPermissionsOnce(project2, "user2", Item.READ, Item.DISCOVER);
try (ACLContext _ = ACL.as(user1)) {
Fingerprint.BuildPtr original = fp.getOriginal();
assertThat("user1 should be able to see the origin", fp.getOriginal(), notNullValue());
assertEquals("user1 should be able to see the origin's project name", project1.getName(), original.getName());
assertEquals("user1 should be able to see the origin's build number", build.getNumber(), original.getNumber());
assertEquals("Only one usage should be visible to user1", 1, fp._getUsages().size());
assertEquals("Only project1 should be visible to user1", project1.getFullName(), fp._getUsages().get(0).name);
}
try (ACLContext _ = ACL.as(user2)) {
assertThat("user2 should be unable to see the origin", fp.getOriginal(), nullValue());
assertEquals("Only one usage should be visible to user2", 1, fp._getUsages().size());
assertEquals("Only project2 should be visible to user2", project2.getFullName(), fp._getUsages().get(0).name);
}
try (ACLContext _ = ACL.as(user3)) {
Fingerprint.BuildPtr original = fp.getOriginal();
assertThat("user3 should be unable to see the origin", fp.getOriginal(), nullValue());
assertEquals("All usages should be invisible for user3", 0, fp._getUsages().size());
}
}
@Test
public void shouldBeAbleToSeeOriginalWithDiscoverPermissionOnly() throws Exception {
// Setup the environment
final FreeStyleProject project = createAndRunProjectWithPublisher("project", "test.txt");
final FreeStyleBuild build = project.getLastBuild();
final Fingerprint fingerprint = getFingerprint(build, "test.txt");
// Init Users and security
User user1 = User.get("user1");
setupProjectMatrixAuthStrategy(Jenkins.READ, Item.DISCOVER);
try (ACLContext _ = ACL.as(user1)) {
Fingerprint.BuildPtr original = fingerprint.getOriginal();
assertThat("user1 should able to see the origin", fingerprint.getOriginal(), notNullValue());
assertEquals("user1 sees the wrong original name with Item.DISCOVER", project.getFullName(), original.getName());
assertEquals("user1 sees the wrong original number with Item.DISCOVER", build.getNumber(), original.getNumber());
assertEquals("Usage ref in fingerprint should be visible to user1", 1, fingerprint._getUsages().size());
}
}
@Test
public void shouldBeAbleToSeeFingerprintsInReadableFolder() throws Exception {
final SecuredMockFolder folder = rule.jenkins.createProject(SecuredMockFolder.class, "folder");
final FreeStyleProject project = createAndRunProjectWithPublisher(folder, "project", "test.txt");
final FreeStyleBuild build = project.getLastBuild();
final Fingerprint fingerprint = getFingerprint(build, "test.txt");
// Init Users and security
User user1 = User.get("user1");
setupProjectMatrixAuthStrategy(false, Jenkins.READ, Item.DISCOVER);
setJobPermissionsOnce(project, "user1", Item.DISCOVER); // Prevents the fallback to the folder ACL
folder.setPermissions("user1", Item.READ);
// Ensure we can read the original from user account
try (ACLContext _ = ACL.as(user1)) {
assertTrue("Test framework issue: User1 should be able to read the folder", folder.hasPermission(Item.READ));
Fingerprint.BuildPtr original = fingerprint.getOriginal();
assertThat("user1 should able to see the origin", fingerprint.getOriginal(), notNullValue());
assertEquals("user1 sees the wrong original name with Item.DISCOVER", project.getFullName(), original.getName());
assertEquals("user1 sees the wrong original number with Item.DISCOVER", build.getNumber(), original.getNumber());
assertEquals("user1 should be able to see the job", 1, fingerprint._getUsages().size());
assertThat("User should be unable do retrieve the job due to the missing read", original.getJob(), nullValue());
}
}
@Test
public void shouldBeUnableToSeeFingerprintsInUnreadableFolder() throws Exception {
final SecuredMockFolder folder = rule.jenkins.createProject(SecuredMockFolder.class, "folder");
final FreeStyleProject project = createAndRunProjectWithPublisher(folder, "project", "test.txt");
final FreeStyleBuild build = project.getLastBuild();
final Fingerprint fingerprint = getFingerprint(build, "test.txt");
// Init Users and security
User user1 = User.get("user1"); // can access project1
setupProjectMatrixAuthStrategy(Jenkins.READ, Item.DISCOVER);
// Ensure we can read the original from user account
try (ACLContext _ = ACL.as(user1)) {
assertFalse("Test framework issue: User1 should be unable to read the folder", folder.hasPermission(Item.READ));
assertThat("user1 should be unable to see the origin", fingerprint.getOriginal(), nullValue());
assertEquals("No jobs should be visible to user1", 0, fingerprint._getUsages().size());
}
}
/**
* A common non-admin user should not be able to see references to a
* deleted job even if he used to have READ permissions before the deletion.
* @throws Exception Test error
*/
@Test
@Issue("SECURITY-153")
public void commonUserShouldBeUnableToSeeReferencesOfDeletedJobs() throws Exception {
// Setup the environment
FreeStyleProject project = createAndRunProjectWithPublisher("project", "test.txt");
FreeStyleBuild build = project.getLastBuild();
final Fingerprint fp = getFingerprint(build, "test.txt");
// Init Users and security
User user1 = User.get("user1");
setupProjectMatrixAuthStrategy(Jenkins.READ, Item.READ, Item.DISCOVER);
project.delete();
try (ACLContext _ = ACL.as(user1)) {
assertThat("user1 should be unable to see the origin", fp.getOriginal(), nullValue());
assertEquals("No jobs should be visible to user1", 0, fp._getUsages().size());
}
}
@Test
public void adminShouldBeAbleToSeeReferencesOfDeletedJobs() throws Exception {
// Setup the environment
final FreeStyleProject project = createAndRunProjectWithPublisher("project", "test.txt");
final FreeStyleBuild build = project.getLastBuild();
final Fingerprint fingerprint = getFingerprint(build, "test.txt");
// Init Users and security
User user1 = User.get("user1");
setupProjectMatrixAuthStrategy(Jenkins.ADMINISTER);
project.delete();
try (ACLContext _ = ACL.as(user1)) {
Fingerprint.BuildPtr original = fingerprint.getOriginal();
assertThat("user1 should able to see the origin", fingerprint.getOriginal(), notNullValue());
assertThat("Job has been deleted, so Job reference shoud return null", fingerprint.getOriginal().getJob(), nullValue());
assertEquals("user1 sees the wrong original name with Item.DISCOVER", project.getFullName(), original.getName());
assertEquals("user1 sees the wrong original number with Item.DISCOVER", build.getNumber(), original.getNumber());
assertEquals("user1 should be able to see the job in usages", 1, fingerprint._getUsages().size());
}
}
@Nonnull
private Fingerprint getFingerprint(@CheckForNull Run<?, ?> run, @Nonnull String filename) {
assertNotNull("Input run is null", run);
Fingerprinter.FingerprintAction action = run.getAction(Fingerprinter.FingerprintAction.class);
assertNotNull("Fingerprint action has not been created in " + run, action);
Map<String, Fingerprint> fingerprints = action.getFingerprints();
final Fingerprint fp = fingerprints.get(filename);
assertNotNull("No reference to '" + filename + "' from the Fingerprint action", fp);
return fp;
}
@Nonnull
private FreeStyleProject createAndRunProjectWithPublisher(String projectName, String fpFileName)
throws Exception {
return createAndRunProjectWithPublisher(null, projectName, fpFileName);
}
@Nonnull
private FreeStyleProject createAndRunProjectWithPublisher(@CheckForNull MockFolder folder,
String projectName, String fpFileName) throws Exception {
final FreeStyleProject project;
if (folder == null) {
project = rule.createFreeStyleProject(projectName);
} else {
project = folder.createProject(FreeStyleProject.class, projectName);
}
project.getBuildersList().add(new CreateFileBuilder(fpFileName, "Hello, world!"));
ArtifactArchiver archiver = new ArtifactArchiver(fpFileName);
archiver.setFingerprint(true);
project.getPublishersList().add(archiver);
rule.buildAndAssertSuccess(project);
return project;
}
private void setupProjectMatrixAuthStrategy(@Nonnull Permission ... permissions) {
setupProjectMatrixAuthStrategy(true, permissions);
}
private void setupProjectMatrixAuthStrategy(boolean inheritFromFolders, @Nonnull Permission ... permissions) {
ProjectMatrixAuthorizationStrategy str = inheritFromFolders
? new ProjectMatrixAuthorizationStrategy()
: new NoInheritanceProjectMatrixAuthorizationStrategy();
for (Permission p : permissions) {
str.add(p, "anonymous");
}
rule.jenkins.setAuthorizationStrategy(str);
}
//TODO: could be reworked to support multiple assignments
private void setJobPermissionsOnce(Job<?,?> job, String username, @Nonnull Permission ... s)
throws IOException {
assertThat("Cannot assign the property twice", job.getProperty(AuthorizationMatrixProperty.class), nullValue());
Map<Permission, Set<String>> permissions = new HashMap<Permission, Set<String>>();
HashSet<String> userSpec = new HashSet<String>(Arrays.asList(username));
for (Permission p : s) {
permissions.put(p, userSpec);
}
AuthorizationMatrixProperty property = new AuthorizationMatrixProperty(permissions);
job.addProperty(property);
}
/**
* Security strategy, which prevents the permission inheritance from upper folders.
*/
private static class NoInheritanceProjectMatrixAuthorizationStrategy extends ProjectMatrixAuthorizationStrategy {
@Override
public ACL getACL(Job<?, ?> project) {
AuthorizationMatrixProperty amp = project.getProperty(AuthorizationMatrixProperty.class);
if (amp != null) {
return amp.getACL().newInheritingACL(getRootACL());
} else {
return getRootACL();
}
}
}
}