/*
 *  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.users;

import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.Principal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;

import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

import org.apache.catalina.Group;
import org.apache.catalina.Role;
import org.apache.catalina.User;
import org.apache.catalina.realm.GenericPrincipal;
import org.apache.catalina.realm.TestMemoryRealm;
import org.apache.catalina.realm.UserDatabaseRealm;

public class TestMemoryUserDatabase {
    private static File TEST_FILE = new File(System.getProperty("java.io.tmpdir"), "tomcat-users.xml");

    private static MemoryUserDatabase db;

    @BeforeClass
    public static void createSampleDB()
        throws Exception {

        try(BufferedWriter out = new BufferedWriter(new FileWriter(TEST_FILE))) {
            out.write(TestMemoryRealm.CONFIG);
        }

        db = new MemoryUserDatabase();
        db.setPathname(TEST_FILE.toURI().toURL().toString());
        db.open();
    }

    @AfterClass
    public static void cleanup() {
        Assert.assertTrue(TEST_FILE.delete());
    }

    @Test
    public void testLoadUserDatabase()
        throws Exception {
        assertPrincipalNames(new String[] { "testrole", "otherrole"}, db.getRoles());
        assertPrincipalNames(new String[] { "testgroup", "othergroup"}, db.getGroups());

        Iterator<User> users = db.getUsers();

        Assert.assertTrue("No users found", users.hasNext());

        User user = users.next();

        Assert.assertEquals("admin", user.getName());
        Assert.assertNull(user.getFullName());
        Assert.assertEquals("sekr3t", user.getPassword());

        assertPrincipalNames(new String[] { "testrole", "otherrole"}, user.getRoles());
        assertPrincipalNames(new String[] { "testgroup", "othergroup"}, user.getGroups());
    }

    public void testReloadUserDatabase()
        throws Exception {
        // Change the database on the disk and reload

        try(BufferedWriter out = new BufferedWriter(new FileWriter(TEST_FILE))) {
            out.write("<?xml version=\"1.0\" ?>"
                    + "<tomcat-users xmlns=\"http://tomcat.apache.org/xml\""
                    + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
                    + " xsi:schemaLocation=\"http://tomcat.apache.org/xml/tomcat-users.xsd\""
                    + " version=\"1.0\">"
                    + "<role rolename=\"foo\" />"
                    + "<group groupname=\"bar\" />"
                    + "<user username=\"root\" password=\"sup3Rsekr3t\" roles=\"foo, bar\" groups=\"bar, foo\" />"
                    + "</tomcat-users>");

            db.open();
        }

        assertPrincipalNames(new String[] { "foo", "bar"}, db.getRoles());
        assertPrincipalNames(new String[] { "bar", "foo"}, db.getGroups());

        Iterator<User> users = db.getUsers();

        Assert.assertTrue("No users found", users.hasNext());

        User user = users.next();

        Assert.assertEquals("root", user.getName());
        Assert.assertNull(user.getFullName());
        Assert.assertEquals("sup3Rsekr3t", user.getPassword());

        assertPrincipalNames(new String[] { "foo", "bar"}, user.getRoles());
        assertPrincipalNames(new String[] { "bar", "foo"}, user.getGroups());
    }

    @Test
    public void testMultithreadedMutateUserDatabase()
        throws Exception {
        // Generate lots of concurrent load on the user database
        Runnable job = new Runnable() {
            @Override
            public void run() {
                for(int i=0; i<10; ++i) {
                  db.createUser("newUser-" + Thread.currentThread().getName() + "-" + i, "x", null);
                }
            }
        };

        int numThreads = 100;
        Thread[] threads = new Thread[numThreads + 1];
        for(int i=0; i<numThreads; ++i) {
          threads[i] = new Thread(job);
        }

        // Let's
        threads[numThreads] = new Thread(new Runnable() {
            @Override
            public void run() {
                try { db.open(); }
                catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        ++numThreads;

        for(int i=0; i<numThreads; ++i) {
          threads[i].start();
        }

        for(int i=0; i<numThreads; ++i) {
          threads[i].join();
        }

        // Remove all those extra users
        Iterator<User> users = db.getUsers();
        for(; users.hasNext();) {
            User user = users.next();
            if(user.getUsername().startsWith("newUser")) {
              db.removeUser(user);
            }
        }

        users = db.getUsers();

        Assert.assertTrue("No users found", users.hasNext());

        User user = users.next();

        Assert.assertEquals("admin", user.getName());
        Assert.assertNull(user.getFullName());
        Assert.assertEquals("sekr3t", user.getPassword());

        assertPrincipalNames(new String[] { "testrole", "otherrole"}, user.getRoles());
        assertPrincipalNames(new String[] { "testgroup", "othergroup"}, user.getGroups());
    }

    @Test
    public void testSerializePrincipal()
        throws Exception {
        User user = db.findUser("admin");
        GenericPrincipal gpIn = new UserDatabaseRealm.UserDatabasePrincipal(user, db);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(gpIn);

        byte[] data = bos.toByteArray();

        ByteArrayInputStream bis = new ByteArrayInputStream(data);
        ObjectInputStream ois = new ObjectInputStream(bis);
        GenericPrincipal gpOut =  (GenericPrincipal) ois.readObject();

        Assert.assertEquals("admin", gpOut.getName());
        assertPrincipalNames(gpOut.getRoles(), user.getRoles());
    }

    private void assertPrincipalNames(String[] expectedNames, Iterator<? extends Principal> i) {
        HashSet<String> names = new HashSet<>(Arrays.asList(expectedNames));

        int j=0;
        while(i.hasNext()) {
            Assert.assertTrue(names.contains(i.next().getName()));
            j++;
        }

        Assert.assertEquals(expectedNames.length, j);
    }

    @Test
    public void testDataEscaping() throws Exception {
        File file = File.createTempFile("tomcat-users", ".xml");
        file.deleteOnExit();

        MemoryUserDatabase mud = new MemoryUserDatabase();
        Role role = mud.createRole("role\"name", "descr&iption");
        Group group = mud.createGroup("grou<p", null);
        group.addRole(role);
        Role role2 = mud.createRole("role2", null);
        group.addRole(role2);
        User user = mud.createUser("xml<breaker", "\"bobby", "tab'les");
        user.addRole(role);
        user.addRole(role2);
        user.addGroup(group);
        mud.setPathname(file.getAbsolutePath());
        mud.setReadonly(false);
        mud.save();

        String xml;
        try(java.io.FileReader in = new java.io.FileReader(file)) {
            StringBuilder sb = new StringBuilder((int)file.length());
            char[] buffer = new char[4096];
            int c;
            while(-1 != (c = in.read(buffer))) {
                sb.append(buffer, 0, c);
            }
            xml = sb.toString();
        }

        Assert.assertTrue("Role is not properly-escaped",
                          xml.contains("<role rolename=\"role&quot;name\" description=\"descr&amp;iption\""));
        Assert.assertTrue("Group is not escaped properly",
                          xml.contains("<group groupname=\"grou&lt;p\" roles=\"role&quot;name,role2\""));
        Assert.assertTrue("User is not properly-escaped",
                          xml.contains("<user username=\"xml&lt;breaker\" password=\"&quot;bobby\" fullName=\"tab&apos;les\" groups=\"grou&lt;p\" roles=\"role&quot;name,role2\""));
    }
}
