/*
 *  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.qpid.server.security.access.plugins;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.Map;

import junit.framework.TestCase;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.qpid.server.security.access.ObjectProperties;
import org.apache.qpid.server.security.access.ObjectProperties.Property;
import org.apache.qpid.server.security.access.ObjectType;
import org.apache.qpid.server.security.access.Operation;
import org.apache.qpid.server.security.access.config.ConfigurationFile;
import org.apache.qpid.server.security.access.config.PlainConfiguration;
import org.apache.qpid.server.security.access.config.Rule;
import org.apache.qpid.server.security.access.config.RuleSet;

/**
 * These tests check that the ACL file parsing works correctly.
 *
 * For each message that can be returned in a {@link ConfigurationException}, an ACL file is created that should trigger this
 * particular message.
 */
public class PlainConfigurationTest extends TestCase
{
    private PlainConfiguration writeACLConfig(String...aclData) throws Exception
    {
        File acl = File.createTempFile(getClass().getName() + getName(), "acl");
        acl.deleteOnExit();

        // Write ACL file
        PrintWriter aclWriter = new PrintWriter(new FileWriter(acl));
        for (String line : aclData)
        {
            aclWriter.println(line);
        }
        aclWriter.close();

        // Load ruleset
        PlainConfiguration configFile = new PlainConfiguration(acl);
        configFile.load();
        return configFile;
    }

    public void testMissingACLConfig() throws Exception
    {
        try
        {
            // Load ruleset
            ConfigurationFile configFile = new PlainConfiguration(new File("doesnotexist"));
            configFile.load();

            fail("fail");
        }
        catch (ConfigurationException ce)
        {
            assertEquals(String.format(PlainConfiguration.CONFIG_NOT_FOUND_MSG, "doesnotexist"), ce.getMessage());
            assertTrue(ce.getCause() instanceof FileNotFoundException);
        }
    }

    public void testACLFileSyntaxContinuation() throws Exception
    {
        try
        {
            writeACLConfig("ACL ALLOW ALL \\ ALL");
            fail("fail");
        }
        catch (ConfigurationException ce)
        {
            assertEquals(String.format(PlainConfiguration.PREMATURE_CONTINUATION_MSG, 1), ce.getMessage());
        }
    }

    public void testACLFileSyntaxTokens() throws Exception
    {
        try
        {
            writeACLConfig("ACL unparsed ALL ALL");
            fail("fail");
        }
        catch (ConfigurationException ce)
        {
            assertEquals(String.format(PlainConfiguration.PARSE_TOKEN_FAILED_MSG, 1), ce.getMessage());
            assertTrue(ce.getCause() instanceof IllegalArgumentException);
            assertEquals("Not a valid permission: unparsed", ce.getCause().getMessage());
        }
    }

    public void testACLFileSyntaxNotEnoughGroup() throws Exception
    {
        try
        {
            writeACLConfig("GROUP blah");
            fail("fail");
        }
        catch (ConfigurationException ce)
        {
            assertEquals(String.format(PlainConfiguration.NOT_ENOUGH_GROUP_MSG, 1), ce.getMessage());
        }
    }

    public void testACLFileSyntaxNotEnoughACL() throws Exception
    {
        try
        {
            writeACLConfig("ACL ALLOW");
            fail("fail");
        }
        catch (ConfigurationException ce)
        {
            assertEquals(String.format(PlainConfiguration.NOT_ENOUGH_ACL_MSG, 1), ce.getMessage());
        }
    }

    public void testACLFileSyntaxNotEnoughConfig() throws Exception
    {
        try
        {
            writeACLConfig("CONFIG");
            fail("fail");
        }
        catch (ConfigurationException ce)
        {
            assertEquals(String.format(PlainConfiguration.NOT_ENOUGH_TOKENS_MSG, 1), ce.getMessage());
        }
    }

    public void testACLFileSyntaxNotEnough() throws Exception
    {
        try
        {
            writeACLConfig("INVALID");
            fail("fail");
        }
        catch (ConfigurationException ce)
        {
            assertEquals(String.format(PlainConfiguration.NOT_ENOUGH_TOKENS_MSG, 1), ce.getMessage());
        }
    }

    public void testACLFileSyntaxPropertyKeyOnly() throws Exception
    {
        try
        {
            writeACLConfig("ACL ALLOW adk CREATE QUEUE name");
            fail("fail");
        }
        catch (ConfigurationException ce)
        {
            assertEquals(String.format(PlainConfiguration.PROPERTY_KEY_ONLY_MSG, 1), ce.getMessage());
        }
    }

    public void testACLFileSyntaxPropertyNoEquals() throws Exception
    {
        try
        {
            writeACLConfig("ACL ALLOW adk CREATE QUEUE name test");
            fail("fail");
        }
        catch (ConfigurationException ce)
        {
            assertEquals(String.format(PlainConfiguration.PROPERTY_NO_EQUALS_MSG, 1), ce.getMessage());
        }
    }

    public void testACLFileSyntaxPropertyNoValue() throws Exception
    {
        try
        {
            writeACLConfig("ACL ALLOW adk CREATE QUEUE name =");
            fail("fail");
        }
        catch (ConfigurationException ce)
        {
            assertEquals(String.format(PlainConfiguration.PROPERTY_NO_VALUE_MSG, 1), ce.getMessage());
        }
    }

    /**
     * Tests interpretation of an acl rule with no object properties.
     *
     */
    public void testValidRule() throws Exception
    {
        final PlainConfiguration config = writeACLConfig("ACL DENY-LOG user1 ACCESS VIRTUALHOST");
        final RuleSet rs = config.getConfiguration();
        assertEquals(1, rs.getRuleCount());

        final Map<Integer, Rule> rules = rs.getAllRules();
        assertEquals(1, rules.size());
        final Rule rule = rules.get(0);
        assertEquals("Rule has unexpected identity", "user1", rule.getIdentity());
        assertEquals("Rule has unexpected operation", Operation.ACCESS, rule.getAction().getOperation());
        assertEquals("Rule has unexpected operation", ObjectType.VIRTUALHOST, rule.getAction().getObjectType());
        assertEquals("Rule has unexpected object properties", ObjectProperties.EMPTY, rule.getAction().getProperties());
    }

    /**
     * Tests interpretation of an acl rule with object properties quoted in single quotes.
     */
    public void testValidRuleWithSingleQuotedProperty() throws Exception
    {
        final PlainConfiguration config = writeACLConfig("ACL ALLOW all CREATE EXCHANGE name = \'value\'");
        final RuleSet rs = config.getConfiguration();
        assertEquals(1, rs.getRuleCount());

        final Map<Integer, Rule> rules = rs.getAllRules();
        assertEquals(1, rules.size());
        final Rule rule = rules.get(0);
        assertEquals("Rule has unexpected identity", "all", rule.getIdentity());
        assertEquals("Rule has unexpected operation", Operation.CREATE, rule.getAction().getOperation());
        assertEquals("Rule has unexpected operation", ObjectType.EXCHANGE, rule.getAction().getObjectType());
        final ObjectProperties expectedProperties = new ObjectProperties();
        expectedProperties.setName("value");
        assertEquals("Rule has unexpected object properties", expectedProperties, rule.getAction().getProperties());
    }

    /**
     * Tests interpretation of an acl rule with object properties quoted in double quotes.
     */
    public void testValidRuleWithDoubleQuotedProperty() throws Exception
    {
        final PlainConfiguration config = writeACLConfig("ACL ALLOW all CREATE EXCHANGE name = \"value\"");
        final RuleSet rs = config.getConfiguration();
        assertEquals(1, rs.getRuleCount());

        final Map<Integer, Rule> rules = rs.getAllRules();
        assertEquals(1, rules.size());
        final Rule rule = rules.get(0);
        assertEquals("Rule has unexpected identity", "all", rule.getIdentity());
        assertEquals("Rule has unexpected operation", Operation.CREATE, rule.getAction().getOperation());
        assertEquals("Rule has unexpected operation", ObjectType.EXCHANGE, rule.getAction().getObjectType());
        final ObjectProperties expectedProperties = new ObjectProperties();
        expectedProperties.setName("value");
        assertEquals("Rule has unexpected object properties", expectedProperties, rule.getAction().getProperties());
    }

    /**
     * Tests interpretation of an acl rule with many object properties.
     */
    public void testValidRuleWithManyProperties() throws Exception
    {
        final PlainConfiguration config = writeACLConfig("ACL ALLOW admin DELETE QUEUE name=name1 owner = owner1");
        final RuleSet rs = config.getConfiguration();
        assertEquals(1, rs.getRuleCount());

        final Map<Integer, Rule> rules = rs.getAllRules();
        assertEquals(1, rules.size());
        final Rule rule = rules.get(0);
        assertEquals("Rule has unexpected identity", "admin", rule.getIdentity());
        assertEquals("Rule has unexpected operation", Operation.DELETE, rule.getAction().getOperation());
        assertEquals("Rule has unexpected operation", ObjectType.QUEUE, rule.getAction().getObjectType());
        final ObjectProperties expectedProperties = new ObjectProperties();
        expectedProperties.setName("name1");
        expectedProperties.put(Property.OWNER, "owner1");
        assertEquals("Rule has unexpected operation", expectedProperties, rule.getAction().getProperties());
    }

    /**
     * Tests interpretation of an acl rule with object properties containing wildcards.  Values containing
     * hashes must be quoted otherwise they are interpreted as comments.
     */
    public void testValidRuleWithWildcardProperties() throws Exception
    {
        final PlainConfiguration config = writeACLConfig("ACL ALLOW all CREATE EXCHANGE routingKey = \'news.#\'",
                                                         "ACL ALLOW all CREATE EXCHANGE routingKey = \'news.co.#\'",
                                                         "ACL ALLOW all CREATE EXCHANGE routingKey = *.co.medellin");
        final RuleSet rs = config.getConfiguration();
        assertEquals(3, rs.getRuleCount());

        final Map<Integer, Rule> rules = rs.getAllRules();
        assertEquals(3, rules.size());
        final Rule rule1 = rules.get(0);
        assertEquals("Rule has unexpected identity", "all", rule1.getIdentity());
        assertEquals("Rule has unexpected operation", Operation.CREATE, rule1.getAction().getOperation());
        assertEquals("Rule has unexpected operation", ObjectType.EXCHANGE, rule1.getAction().getObjectType());
        final ObjectProperties expectedProperties1 = new ObjectProperties();
        expectedProperties1.put(Property.ROUTING_KEY,"news.#");
        assertEquals("Rule has unexpected object properties", expectedProperties1, rule1.getAction().getProperties());

        final Rule rule2 = rules.get(10);
        final ObjectProperties expectedProperties2 = new ObjectProperties();
        expectedProperties2.put(Property.ROUTING_KEY,"news.co.#");
        assertEquals("Rule has unexpected object properties", expectedProperties2, rule2.getAction().getProperties());

        final Rule rule3 = rules.get(20);
        final ObjectProperties expectedProperties3 = new ObjectProperties();
        expectedProperties3.put(Property.ROUTING_KEY,"*.co.medellin");
        assertEquals("Rule has unexpected object properties", expectedProperties3, rule3.getAction().getProperties());
    }

    /**
     * Tests that rules are case insignificant.
     */
    public void testMixedCaseRuleInterpretation() throws Exception
    {
        final PlainConfiguration config = writeACLConfig("AcL deny-LOG User1 BiND Exchange Name=AmQ.dIrect");
        final RuleSet rs = config.getConfiguration();
        assertEquals(1, rs.getRuleCount());

        final Map<Integer, Rule> rules = rs.getAllRules();
        assertEquals(1, rules.size());
        final Rule rule = rules.get(0);
        assertEquals("Rule has unexpected identity", "User1", rule.getIdentity());
        assertEquals("Rule has unexpected operation", Operation.BIND, rule.getAction().getOperation());
        assertEquals("Rule has unexpected operation", ObjectType.EXCHANGE, rule.getAction().getObjectType());
        final ObjectProperties expectedProperties = new ObjectProperties("AmQ.dIrect");
        assertEquals("Rule has unexpected object properties", expectedProperties, rule.getAction().getProperties());
    }

    /**
     * Tests whitespace is supported. Note that currently the Java implementation permits comments to
     * be introduced anywhere in the ACL, whereas the C++ supports only whitespace at the beginning of
     * of line.
     */
    public void testCommentsSuppported() throws Exception
    {
        final PlainConfiguration config = writeACLConfig("#Comment",
                                                         "ACL DENY-LOG user1 ACCESS VIRTUALHOST # another comment",
                                                         "  # final comment with leading whitespace");
        final RuleSet rs = config.getConfiguration();
        assertEquals(1, rs.getRuleCount());

        final Map<Integer, Rule> rules = rs.getAllRules();
        assertEquals(1, rules.size());
        final Rule rule = rules.get(0);
        assertEquals("Rule has unexpected identity", "user1", rule.getIdentity());
        assertEquals("Rule has unexpected operation", Operation.ACCESS, rule.getAction().getOperation());
        assertEquals("Rule has unexpected operation", ObjectType.VIRTUALHOST, rule.getAction().getObjectType());
        assertEquals("Rule has unexpected object properties", ObjectProperties.EMPTY, rule.getAction().getProperties());
    }

    /**
     * Tests interpretation of an acl rule using mixtures of tabs/spaces as token separators.
     *
     */
    public void testWhitespace() throws Exception
    {
        final PlainConfiguration config = writeACLConfig("ACL\tDENY-LOG\t\t user1\t \tACCESS VIRTUALHOST");
        final RuleSet rs = config.getConfiguration();
        assertEquals(1, rs.getRuleCount());

        final Map<Integer, Rule> rules = rs.getAllRules();
        assertEquals(1, rules.size());
        final Rule rule = rules.get(0);
        assertEquals("Rule has unexpected identity", "user1", rule.getIdentity());
        assertEquals("Rule has unexpected operation", Operation.ACCESS, rule.getAction().getOperation());
        assertEquals("Rule has unexpected operation", ObjectType.VIRTUALHOST, rule.getAction().getObjectType());
        assertEquals("Rule has unexpected object properties", ObjectProperties.EMPTY, rule.getAction().getProperties());
    }

    /**
     * Tests interpretation of an acl utilising line continuation.
     */
    public void testLineContination() throws Exception
    {
        final PlainConfiguration config = writeACLConfig("ACL DENY-LOG user1 \\",
                                                         "ACCESS VIRTUALHOST");
        final RuleSet rs = config.getConfiguration();
        assertEquals(1, rs.getRuleCount());

        final Map<Integer, Rule> rules = rs.getAllRules();
        assertEquals(1, rules.size());
        final Rule rule = rules.get(0);
        assertEquals("Rule has unexpected identity", "user1", rule.getIdentity());
        assertEquals("Rule has unexpected operation", Operation.ACCESS, rule.getAction().getOperation());
        assertEquals("Rule has unexpected operation", ObjectType.VIRTUALHOST, rule.getAction().getObjectType());
        assertEquals("Rule has unexpected object properties", ObjectProperties.EMPTY, rule.getAction().getProperties());
    }

}
