/*
 * 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 should 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();
            }
        }
    }
}
