/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.realm;

import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.ServletSecurityElement;
import javax.servlet.annotation.ServletSecurity;

import org.junit.Assert;
import org.junit.Test;

import org.apache.catalina.Context;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.deploy.SecurityCollection;
import org.apache.catalina.deploy.SecurityConstraint;
import org.apache.catalina.startup.TesterMapRealm;
import org.apache.tomcat.unittest.TesterContext;
import org.apache.tomcat.unittest.TesterRequest;
import org.apache.tomcat.unittest.TesterResponse;

public class TestRealmBase {

    private static final String USER1 = "user1";
    private static final String USER2 = "user2";
    private static final String USER99 = "user99";
    private static final String PWD = "password";
    public static final String ROLE1 = "role1";
    private static final String ROLE2 = "role2";
    private static final String ROLE3 = "role3";
    private static final String ROLE99 = "role99";

    // All digested passwords are the digested form of "password"
    private static final String PWD_MD5 = "5f4dcc3b5aa765d61d8327deb882cf99";
    private static final String PWD_SHA = "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8";
    private static final String PWD_MD5_PREFIX =
            "{MD5}X03MO1qnZdYdgyfeuILPmQ==";
    private static final String PWD_SHA_PREFIX =
            "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=";
    // Salt added to "password" is "salttoprotectpassword"
    private static final String PWD_SSHA_PREFIX =
            "{SSHA}oFLhvfQVqFykEWu8v1pPE6nN0QRzYWx0dG9wcm90ZWN0cGFzc3dvcmQ=";

    private static final String ROLE_ALL_ROLES="*";


    @Test
    public void testDigestMD5() throws Exception {
        doTestDigestDigestPasswords(PWD, "MD5", PWD_MD5);
    }

    @Test
    public void testDigestSHA() throws Exception {
        doTestDigestDigestPasswords(PWD, "SHA", PWD_SHA);
    }

    @Test
    public void testDigestMD5Prefix() throws Exception {
        doTestDigestDigestPasswords(PWD, "MD5", PWD_MD5_PREFIX);
    }

    @Test
    public void testDigestSHAPrefix() throws Exception {
        doTestDigestDigestPasswords(PWD, "SHA", PWD_SHA_PREFIX);
    }

    @Test
    public void testDigestSSHAPrefix() throws Exception {
        doTestDigestDigestPasswords(PWD, "SHA", PWD_SSHA_PREFIX);
    }

    private void doTestDigestDigestPasswords(String password,
            String digest, String digestedPassword) throws Exception {
        Context context = new TesterContext();
        TesterMapRealm realm = new TesterMapRealm();
        realm.setContainer(context);
        realm.setDigest(digest);
        realm.start();

        realm.addUser(USER1, digestedPassword);

        Principal p = realm.authenticate(USER1, password);

        Assert.assertNotNull(p);
        Assert.assertEquals(USER1, p.getName());
    }

    @Test
    public void testUserWithSingleRole() throws IOException {
        List<String> userRoles = new ArrayList<String>();
        List<String> constraintRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        // Configure this test
        userRoles.add(ROLE1);
        constraintRoles.add(ROLE1);
        applicationRoles.add(ROLE1);

        doRoleTest(userRoles, constraintRoles, applicationRoles, true);
    }


    @Test
    public void testUserWithNoRoles() throws IOException {
        List<String> userRoles = new ArrayList<String>();
        List<String> constraintRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        // Configure this test
        constraintRoles.add(ROLE1);
        applicationRoles.add(ROLE1);

        doRoleTest(userRoles, constraintRoles, applicationRoles, false);
    }


    @Test
    public void testUserWithSingleRoleAndAllRoles() throws IOException {
        List<String> userRoles = new ArrayList<String>();
        List<String> constraintRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        // Configure this test
        userRoles.add(ROLE1);
        applicationRoles.add(ROLE1);
        constraintRoles.add(ROLE_ALL_ROLES);

        doRoleTest(userRoles, constraintRoles, applicationRoles, true);
    }


    @Test
    public void testUserWithoutNoRolesAndAllRoles() throws IOException {
        List<String> userRoles = new ArrayList<String>();
        List<String> constraintRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        // Configure this test
        constraintRoles.add(ROLE_ALL_ROLES);
        applicationRoles.add(ROLE1);

        doRoleTest(userRoles, constraintRoles, applicationRoles, false);
    }


    @Test
    public void testAllRolesWithNoAppRole() throws IOException {
        List<String> userRoles = new ArrayList<String>();
        List<String> constraintRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        // Configure this test
        userRoles.add(ROLE1);
        constraintRoles.add(ROLE_ALL_ROLES);

        doRoleTest(userRoles, constraintRoles, applicationRoles, false);
    }


    @Test
    public void testNoAuthConstraint() throws IOException {
        // No auth constraint == allow access for all
        List<String> applicationRoles = new ArrayList<String>();

        doRoleTest(null, null, applicationRoles, true);
    }


    /*
     * The combining constraints tests are based on the scenarios described in
     * section
     */

    @Test
    public void testCombineConstraints01() throws IOException {
        // Allowed roles should be the union of the roles in the constraints
        // User role is in first constraint
        List<String> userRoles = new ArrayList<String>();
        List<String> constraintOneRoles = new ArrayList<String>();
        List<String> constraintTwoRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        userRoles.add(ROLE1);
        constraintOneRoles.add(ROLE1);
        constraintTwoRoles.add(ROLE2);
        applicationRoles.add(ROLE1);
        applicationRoles.add(ROLE2);

        doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles,
                applicationRoles, true);
    }


    @Test
    public void testCombineConstraints02() throws IOException {
        // Allowed roles should be the union of the roles in the constraints
        // User role is in last constraint
        List<String> userRoles = new ArrayList<String>();
        List<String> constraintOneRoles = new ArrayList<String>();
        List<String> constraintTwoRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        userRoles.add(ROLE2);
        constraintOneRoles.add(ROLE1);
        constraintTwoRoles.add(ROLE2);
        applicationRoles.add(ROLE1);
        applicationRoles.add(ROLE2);

        doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles,
                applicationRoles, true);
    }


    @Test
    public void testCombineConstraints03() throws IOException {
        // Allowed roles should be the union of the roles in the constraints
        // User role is not in any constraint
        List<String> userRoles = new ArrayList<String>();
        List<String> constraintOneRoles = new ArrayList<String>();
        List<String> constraintTwoRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        userRoles.add(ROLE3);
        constraintOneRoles.add(ROLE1);
        constraintTwoRoles.add(ROLE2);
        applicationRoles.add(ROLE1);
        applicationRoles.add(ROLE2);

        doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles,
                applicationRoles, false);
    }


    @Test
    public void testCombineConstraints04() throws IOException {
        // Allowed roles should be the union of the roles in the constraints
        // * is any app role
        // User role is not in any constraint
        List<String> userRoles = new ArrayList<String>();
        List<String> constraintOneRoles = new ArrayList<String>();
        List<String> constraintTwoRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        userRoles.add(ROLE99);
        constraintOneRoles.add(ROLE1);
        constraintTwoRoles.add(ROLE_ALL_ROLES);
        applicationRoles.add(ROLE2);
        applicationRoles.add(ROLE3);

        doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles,
                applicationRoles, false);
    }


    @Test
    public void testCombineConstraints05() throws IOException {
        // Allowed roles should be the union of the roles in the constraints
        // * is any app role
        // User role is a non-app constraint role
        List<String> userRoles = new ArrayList<String>();
        List<String> constraintOneRoles = new ArrayList<String>();
        List<String> constraintTwoRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        userRoles.add(ROLE1);
        constraintOneRoles.add(ROLE1);
        constraintTwoRoles.add(ROLE_ALL_ROLES);
        applicationRoles.add(ROLE2);
        applicationRoles.add(ROLE3);

        doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles,
                applicationRoles, true);
    }


    @Test
    public void testCombineConstraints06() throws IOException {
        // Allowed roles should be the union of the roles in the constraints
        // * is any app role
        // User role is an app role
        List<String> userRoles = new ArrayList<String>();
        List<String> constraintOneRoles = new ArrayList<String>();
        List<String> constraintTwoRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        userRoles.add(ROLE2);
        constraintOneRoles.add(ROLE1);
        constraintTwoRoles.add(ROLE_ALL_ROLES);
        applicationRoles.add(ROLE2);
        applicationRoles.add(ROLE3);

        doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles,
                applicationRoles, true);
    }


    @Test
    public void testCombineConstraints07() throws IOException {
        // Allowed roles should be the union of the roles in the constraints
        // * is any app role
        // User has no role
        List<String> userRoles = new ArrayList<String>();
        List<String> constraintOneRoles = new ArrayList<String>();
        List<String> constraintTwoRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        constraintOneRoles.add(ROLE1);
        constraintTwoRoles.add(ROLE_ALL_ROLES);
        applicationRoles.add(ROLE2);
        applicationRoles.add(ROLE3);

        doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles,
                applicationRoles, false);
    }


    @Test
    public void testCombineConstraints12() throws IOException {
        // Allowed roles should be the union of the roles in the constraints
        // Constraint without role or implied role permits unauthenticated users
        // User is not authenticated
        List<String> constraintTwoRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        constraintTwoRoles.add(ROLE1);
        applicationRoles.add(ROLE1);

        doRoleTest(null, null, constraintTwoRoles,
                applicationRoles, true);
    }


    @Test
    public void testCombineConstraints13() throws IOException {
        // Allowed roles should be the union of the roles in the constraints
        // Constraint without role or implied role permits unauthenticated users
        // User is not authenticated
        List<String> constraintTwoRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        constraintTwoRoles.add(ROLE_ALL_ROLES);
        applicationRoles.add(ROLE1);

        doRoleTest(null, null, constraintTwoRoles,
                applicationRoles, true);
    }


    @Test
    public void testCombineConstraints15() throws IOException {
        // Allowed roles should be the union of the roles in the constraints
        // Constraint with empty auth section prevents all access
        // User has matching constraint role
        List<String> userRoles = new ArrayList<String>();
        List<String> constraintOneRoles = new ArrayList<String>();
        List<String> constraintTwoRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        userRoles.add(ROLE1);
        constraintTwoRoles.add(ROLE1);
        applicationRoles.add(ROLE1);

        doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles,
                applicationRoles, false);
    }


    @Test
    public void testCombineConstraints16() throws IOException {
        // Allowed roles should be the union of the roles in the constraints
        // Constraint with empty auth section prevents all access
        // User has matching role
        List<String> userRoles = new ArrayList<String>();
        List<String> constraintOneRoles = new ArrayList<String>();
        List<String> constraintTwoRoles = new ArrayList<String>();
        List<String> applicationRoles = new ArrayList<String>();

        userRoles.add(ROLE1);
        constraintTwoRoles.add(ROLE_ALL_ROLES);
        applicationRoles.add(ROLE1);

        doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles,
                applicationRoles, false);
    }


    /**
     * @param userRoles         <code>null</code> tests unauthenticated access
     *                          otherwise access is tested with an authenticated
     *                          user with the listed roles
     * @param constraintRoles   <code>null</code> is equivalent to no auth
     *                          constraint whereas an empty list is equivalent
     *                          to an auth constraint that defines no roles.
     */
    private void doRoleTest(List<String> userRoles,
            List<String> constraintRoles, List<String> applicationRoles,
            boolean expected) throws IOException {

        List<String> constraintTwoRoles = new ArrayList<String>();
        constraintTwoRoles.add(ROLE99);
        doRoleTest(userRoles, constraintRoles, constraintTwoRoles,
                applicationRoles, expected);
    }


    private void doRoleTest(List<String> userRoles,
            List<String> constraintOneRoles, List<String> constraintTwoRoles,
            List<String> applicationRoles, boolean expected)
            throws IOException {

        TesterMapRealm mapRealm = new TesterMapRealm();

        // Configure the security constraints for the resource
        SecurityConstraint constraintOne = new SecurityConstraint();
        if (constraintOneRoles != null) {
            constraintOne.setAuthConstraint(true);
            for (String constraintRole : constraintOneRoles) {
                constraintOne.addAuthRole(constraintRole);
            }
        }
        SecurityConstraint constraintTwo = new SecurityConstraint();
        if (constraintTwoRoles != null) {
            constraintTwo.setAuthConstraint(true);
            for (String constraintRole : constraintTwoRoles) {
                constraintTwo.addAuthRole(constraintRole);
            }
        }
        SecurityConstraint[] constraints =
                new SecurityConstraint[] { constraintOne, constraintTwo };

        // Set up the mock request and response
        Request request = new Request();
        Response response = new TesterResponse();
        Context context = new TesterContext();
        for (String applicationRole : applicationRoles) {
            context.addSecurityRole(applicationRole);
        }
        request.setContext(context);

        // Set up an authenticated user
        // Configure the users in the Realm
        if (userRoles != null) {
            GenericPrincipal gp = new GenericPrincipal(USER1, PWD, userRoles);
            request.setUserPrincipal(gp);
        }

        // Check if user meets constraints
        boolean result = mapRealm.hasResourcePermission(
                request, response, constraints, null);

        Assert.assertEquals(Boolean.valueOf(expected), Boolean.valueOf(result));
    }


    /**
     * This test case covers the special case in section 13.4.1 of the Servlet
     * 3.1 specification for {@link javax.servlet.annotation.HttpConstraint}.
     */
    @Test
    public void testHttpConstraint() throws IOException {
        // Get the annotation from the test case
        Class<TesterServletSecurity01> clazz = TesterServletSecurity01.class;
        ServletSecurity servletSecurity =
                clazz.getAnnotation(ServletSecurity.class);

        // Convert the annotation into constraints
        ServletSecurityElement servletSecurityElement =
                new ServletSecurityElement(servletSecurity);
        SecurityConstraint[] constraints =
                SecurityConstraint.createConstraints(
                        servletSecurityElement, "/*");

        // Create a separate constraint that covers DELETE
        SecurityConstraint deleteConstraint = new SecurityConstraint();
        deleteConstraint.addAuthRole(ROLE1);
        SecurityCollection deleteCollection = new SecurityCollection();
        deleteCollection.addMethod("DELETE");
        deleteCollection.addPattern("/*");
        deleteConstraint.addCollection(deleteCollection);

        TesterMapRealm mapRealm = new TesterMapRealm();

        // Set up the mock request and response
        TesterRequest request = new TesterRequest();
        Response response = new TesterResponse();
        Context context = request.getContext();
        context.addSecurityRole(ROLE1);
        context.addSecurityRole(ROLE2);
        request.setContext(context);

        // Create the principals
        List<String> userRoles1 = new ArrayList<String>();
        userRoles1.add(ROLE1);
        GenericPrincipal gp1 = new GenericPrincipal(USER1, PWD, userRoles1);

        List<String> userRoles2 = new ArrayList<String>();
        userRoles2.add(ROLE2);
        GenericPrincipal gp2 = new GenericPrincipal(USER2, PWD, userRoles2);

        List<String> userRoles99 = new ArrayList<String>();
        GenericPrincipal gp99 = new GenericPrincipal(USER99, PWD, userRoles99);

        // Add the constraints to the context
        for (SecurityConstraint constraint : constraints) {
            context.addConstraint(constraint);
        }
        context.addConstraint(deleteConstraint);

        // All users should be able to perform a GET
        request.setMethod("GET");

        SecurityConstraint[] constraintsGet =
                mapRealm.findSecurityConstraints(request, context);

        request.setUserPrincipal(null);
        Assert.assertTrue(mapRealm.hasResourcePermission(
                request, response, constraintsGet, null));
        request.setUserPrincipal(gp1);
        Assert.assertTrue(mapRealm.hasResourcePermission(
                request, response, constraintsGet, null));
        request.setUserPrincipal(gp2);
        Assert.assertTrue(mapRealm.hasResourcePermission(
                request, response, constraintsGet, null));
        request.setUserPrincipal(gp99);
        Assert.assertTrue(mapRealm.hasResourcePermission(
                request, response, constraintsGet, null));

        // Only user1 should be able to perform a POST as only that user has
        // role1.
        request.setMethod("POST");

        SecurityConstraint[] constraintsPost =
                mapRealm.findSecurityConstraints(request, context);

        request.setUserPrincipal(null);
        Assert.assertFalse(mapRealm.hasResourcePermission(
                request, response, constraintsPost, null));
        request.setUserPrincipal(gp1);
        Assert.assertTrue(mapRealm.hasResourcePermission(
                request, response, constraintsPost, null));
        request.setUserPrincipal(gp2);
        Assert.assertFalse(mapRealm.hasResourcePermission(
                request, response, constraintsPost, null));
        request.setUserPrincipal(gp99);
        Assert.assertFalse(mapRealm.hasResourcePermission(
                request, response, constraintsPost, null));

        // Only users with application roles (role1 or role2 so user1 or user2)
        // should be able to perform a PUT.
        request.setMethod("PUT");

        SecurityConstraint[] constraintsPut =
                mapRealm.findSecurityConstraints(request, context);

        request.setUserPrincipal(null);
        Assert.assertFalse(mapRealm.hasResourcePermission(
                request, response, constraintsPut, null));
        request.setUserPrincipal(gp1);
        Assert.assertTrue(mapRealm.hasResourcePermission(
                request, response, constraintsPut, null));
        request.setUserPrincipal(gp2);
        Assert.assertTrue(mapRealm.hasResourcePermission(
                request, response, constraintsPut, null));
        request.setUserPrincipal(gp99);
        Assert.assertFalse(mapRealm.hasResourcePermission(
                request, response, constraintsPut, null));

        // Only user1 should be able to perform a DELETE as only that user has
        // role1.
        request.setMethod("DELETE");

        SecurityConstraint[] constraintsDelete =
                mapRealm.findSecurityConstraints(request, context);

        request.setUserPrincipal(null);
        Assert.assertFalse(mapRealm.hasResourcePermission(
                request, response, constraintsDelete, null));
        request.setUserPrincipal(gp1);
        Assert.assertTrue(mapRealm.hasResourcePermission(
                request, response, constraintsDelete, null));
        request.setUserPrincipal(gp2);
        Assert.assertFalse(mapRealm.hasResourcePermission(
                request, response, constraintsDelete, null));
        request.setUserPrincipal(gp99);
        Assert.assertFalse(mapRealm.hasResourcePermission(
                request, response, constraintsDelete, null));
    }
}
