/*
 * 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.karaf.jaas.modules.properties;

import java.io.File;
import java.io.IOException;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;

import javax.security.auth.Subject;
import javax.security.auth.callback.*;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;

import org.apache.felix.utils.properties.Properties;
import org.apache.karaf.jaas.boot.principal.GroupPrincipal;
import org.apache.karaf.jaas.boot.principal.RolePrincipal;
import org.apache.karaf.jaas.boot.principal.UserPrincipal;
import org.junit.Assert;
import org.junit.Test;

public class PropertiesLoginModuleTest {

    @Test
    public void testBasicLogin() throws Exception {
        File f = File.createTempFile(getClass().getName(), ".tmp");
        try {
            Properties p = new Properties(f);
            PropertiesBackingEngine pbe = new PropertiesBackingEngine(p);
            pbe.addUser("abc", "xyz");
            pbe.addRole("abc", "myrole");
            pbe.addUser("pqr", "abc");

            PropertiesLoginModule module = new PropertiesLoginModule();
            Map<String, String> options = new HashMap<String, String>();
            options.put(PropertiesLoginModule.USER_FILE, f.getAbsolutePath());
            CallbackHandler cb = new CallbackHandler() {
                @Override
                public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
                    for (Callback cb : callbacks) {
                        if (cb instanceof NameCallback) {
                            ((NameCallback) cb).setName("abc");
                        } else if (cb instanceof PasswordCallback) {
                            ((PasswordCallback) cb).setPassword("xyz".toCharArray());
                        }
                    }
                }
            };
            Subject subject = new Subject();
            module.initialize(subject, cb, null, options);

            Assert.assertEquals("Precondition", 0, subject.getPrincipals().size());
            Assert.assertTrue(module.login());
            Assert.assertTrue(module.commit());

            Assert.assertEquals(2, subject.getPrincipals().size());

            boolean foundUser = false;
            boolean foundRole = false;
            for (Principal pr : subject.getPrincipals()) {
                if (pr instanceof UserPrincipal) {
                    Assert.assertEquals("abc", pr.getName());
                    foundUser = true;
                } else if (pr instanceof RolePrincipal) {
                    Assert.assertEquals("myrole", pr.getName());
                    foundRole = true;
                }
            }
            Assert.assertTrue(foundUser);
            Assert.assertTrue(foundRole);

            Assert.assertTrue(module.logout());
            Assert.assertEquals("Principals should be gone as the user has logged out", 0, subject.getPrincipals().size());
        } finally {
            if (!f.delete()) {
                Assert.fail("Could not delete temporary file: " + f);
            }
        }
    }

    @Test
    public void testLoginIncorrectPassword() throws Exception {
        File f = File.createTempFile(getClass().getName(), ".tmp");
        try {
            Properties p = new Properties(f);
            PropertiesBackingEngine pbe = new PropertiesBackingEngine(p);
            pbe.addUser("abc", "xyz");
            pbe.addUser("pqr", "abc");

            PropertiesLoginModule module = new PropertiesLoginModule();
            Map<String, String> options = new HashMap<String, String>();
            options.put(PropertiesLoginModule.USER_FILE, f.getAbsolutePath());
            CallbackHandler cb = new CallbackHandler() {
                @Override
                public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
                    for (Callback cb : callbacks) {
                        if (cb instanceof NameCallback) {
                            ((NameCallback) cb).setName("abc");
                        } else if (cb instanceof PasswordCallback) {
                            ((PasswordCallback) cb).setPassword("abc".toCharArray());
                        }
                    }
                }
            };
            module.initialize(new Subject(), cb, null, options);
            try {
                module.login();
                Assert.fail("The login should have failed as the passwords didn't match");
            } catch (FailedLoginException fle) {
                // good
            }
        } finally {
            if (!f.delete()) {
                Assert.fail("Could not delete temporary file: " + f);
            }
        }
    }

    @Test
    public void testLoginWithGroups() throws Exception {
        File f = File.createTempFile(getClass().getName(), ".tmp");
        try {
            Properties p = new Properties(f);
            PropertiesBackingEngine pbe = new PropertiesBackingEngine(p);
            pbe.addUser("abc", "xyz");
            pbe.addRole("abc", "myrole");
            pbe.addUser("pqr", "abc");
            pbe.addGroup("pqr", "group1");
            pbe.addGroupRole("group1", "r1");

            PropertiesLoginModule module = new PropertiesLoginModule();
            Map<String, String> options = new HashMap<String, String>();
            options.put(PropertiesLoginModule.USER_FILE, f.getAbsolutePath());
            CallbackHandler cb = new CallbackHandler() {
                @Override
                public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
                    for (Callback cb : callbacks) {
                        if (cb instanceof NameCallback) {
                            ((NameCallback) cb).setName("pqr");
                        } else if (cb instanceof PasswordCallback) {
                            ((PasswordCallback) cb).setPassword("abc".toCharArray());
                        }
                    }
                }
            };
            Subject subject = new Subject();
            module.initialize(subject, cb, null, options);

            Assert.assertEquals("Precondition", 0, subject.getPrincipals().size());
            Assert.assertTrue(module.login());
            Assert.assertTrue(module.commit());

            Assert.assertEquals(3, subject.getPrincipals().size());
            boolean foundUser = false;
            boolean foundRole = false;
            boolean foundGroup = false;
            for (Principal pr : subject.getPrincipals()) {
                if (pr instanceof UserPrincipal) {
                    Assert.assertEquals("pqr", pr.getName());
                    foundUser = true;
                } else if (pr instanceof GroupPrincipal) {
                    Assert.assertEquals("group1", pr.getName());
                    foundGroup = true;
                } else if (pr instanceof RolePrincipal) {
                    Assert.assertEquals("r1", pr.getName());
                    foundRole = true;
                }
            }
            Assert.assertTrue(foundUser);
            Assert.assertTrue(foundGroup);
            Assert.assertTrue(foundRole);
        } finally {
            if (!f.delete()) {
                Assert.fail("Could not delete temporary file: " + f);
            }
        }
    }

    // This is a fairly important test that ensures that you cannot log in under the name of a
    // group directly.
    @Test
    public void testCannotLoginAsGroupDirectly() throws Exception {
        testCannotLoginAsGroupDirectly("group1");
        testCannotLoginAsGroupDirectly("_g_:group1");
        testCannotLoginAsGroupDirectly(PropertiesBackingEngine.GROUP_PREFIX + "group1");
    }

    private void testCannotLoginAsGroupDirectly(final String name) throws IOException, LoginException {
        File f = File.createTempFile(getClass().getName(), ".tmp");
        try {
            Properties p = new Properties(f);
            PropertiesBackingEngine pbe = new PropertiesBackingEngine(p);
            pbe.addUser("abc", "xyz");
            pbe.addRole("abc", "myrole");
            pbe.addUser("pqr", "abc");
            pbe.addGroup("pqr", "group1");
            pbe.addGroupRole("group1", "r1");

            PropertiesLoginModule module = new PropertiesLoginModule();
            Map<String, String> options = new HashMap<String, String>();
            options.put(PropertiesLoginModule.USER_FILE, f.getAbsolutePath());
            CallbackHandler cb = new CallbackHandler() {
                @Override
                public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
                    for (Callback cb : callbacks) {
                        if (cb instanceof NameCallback) {
                            ((NameCallback) cb).setName(name);
                        } else if (cb instanceof PasswordCallback) {
                            ((PasswordCallback) cb).setPassword("group".toCharArray());
                        }
                    }
                }
            };
            module.initialize(new Subject(), cb, null, options);
            try {
                module.login();
                Assert.fail("The login should have failed as you cannot log in under a group name directly");
            } catch (FailedLoginException fle) {
                // good
            }
        } finally {
            if (!f.delete()) {
                Assert.fail("Could not delete temporary file: " + f);
            }
        }
    }

    @Test
    public void testNullUsersFile() {
        try {
            testWithUsersFile(null);
            Assert.fail("LoginException expected");
        } catch (LoginException e) {
            Assert.assertEquals("The property users may not be null", e.getMessage());
        }
    }

    @Test
    public void testNonExistantPropertiesFile() throws LoginException, IOException, UnsupportedCallbackException {
        try {
            testWithUsersFile(File.separator + "test" + File.separator + "users.properties");
        } catch (LoginException e) {
            Assert.assertEquals("Users file not found at " + File.separator + "test" + File.separator + "users.properties", e.getMessage());
        }
    }

    @Test
    public void testNullPassword() throws Exception {
        PropertiesLoginModule module = new PropertiesLoginModule();
        Subject subject = new Subject();
        CallbackHandler handler = new NullHandler();
        Map<String, String> options = new HashMap<String, String>();
        options.put(PropertiesLoginModule.USER_FILE, getTestUsersFile());
        module.initialize(subject, handler, null, options);

        try {
            module.login();
            Assert.fail("LoginException expected");
        } catch (LoginException e) {
            Assert.assertEquals("Password can not be null", e.getMessage());
        }
    }

    private void testWithUsersFile(String usersFilePath) throws LoginException {
        PropertiesLoginModule module = new PropertiesLoginModule();
        Subject sub = new Subject();
        CallbackHandler handler = new NamePasswordHandler("test", "test");
        Map<String, String> options = new HashMap<String, String>();
        options.put(PropertiesLoginModule.USER_FILE, usersFilePath);
        module.initialize(sub, handler, null, options);
        module.login();
    }
    
    @Test
    public void testNullCallbackHandler() {
        PropertiesLoginModule module = new PropertiesLoginModule();
        Subject subject = new Subject();
        Map<String, String> options = new HashMap<String, String>();
        options.put(PropertiesLoginModule.USER_FILE, getTestUsersFile());
        module.initialize(subject, null, null, options );
        try {
            module.login();
            Assert.fail("LoginException expected");
        } catch (LoginException e) {
            Assert.assertEquals("Username can not be null", e.getMessage());
        }
    }

    private String getTestUsersFile() {
        return this.getClass().getClassLoader().getResource("org/apache/karaf/jaas/modules/properties/test.properties").getFile();
    }
}
