/*
 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

import com.sun.security.auth.UnixPrincipal;

import javax.security.auth.Subject;
import javax.security.auth.callback.*;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/*
 * @test
 * @bug 8050460
 * @summary Test checks that proper methods associated with login/logout process
 * of LoginContext are called for different configurations and circumstances.
 * @modules jdk.security.auth
 *
 * @run main/othervm  LCTest EmptyModuleConfig false
 * @run main/othervm  LCTest IncorrectName false
 * @run main/othervm  LCTest AbortRequisite false abort
 * @run main/othervm  LCTest AbortSufficient false abort
 * @run main/othervm  LCTest AbortRequired false abort
 * @run main/othervm  LCTest LogoutRequisite false logout
 * @run main/othervm  LCTest LogoutSufficient true logout
 * @run main/othervm  LCTest LogoutRequired false logout
 * @run main/othervm  LCTest LoginRequisite false login
 * @run main/othervm  LCTest LoginSufficient true login
 * @run main/othervm  LCTest LoginRequired false login
 */

public class LCTest {

    private static final String USER_NAME = "testUser";
    private static final String PASSWORD = "testPassword";
    private static final List<String> loggedActions = new ArrayList<>();

    static {
        System.setProperty("java.security.auth.login.config",
                System.getProperty("test.src")
                        + System.getProperty("file.separator")
                        + "LCTest.jaas.config");
    }

    public static void main(String[] args) {
        if (args.length < 2) {
            throw new RuntimeException("Incorrect test params");
        }
        String nameOfContext = args[0];
        boolean isPositive = Boolean.parseBoolean(args[1]);
        String actionName = null;
        if (args.length == 3) {
            actionName = args[2];
        }
        try {
            LoginContext lc = new LoginContext(nameOfContext,
                    new MyCallbackHandler());
            lc.login();
            checkPrincipal(lc, true);
            lc.logout();
            checkPrincipal(lc, false);
            if (!isPositive) {
                throw new RuntimeException("Test failed. Exception expected.");
            }
        } catch (LoginException le) {
            if (isPositive) {
                throw new RuntimeException("Test failed. Unexpected " +
                        "exception", le);
            }
            System.out.println("Expected exception: "
                    + le.getMessage());
        }
        checkActions(actionName);
        System.out.println("Test passed.");
    }

    /*
     * Log action from login modules
     */
    private static void logAction(String actionName) {
        loggedActions.add(actionName);
    }

    /*
     * Check if logged actions are as expected. We always expected 3 actions
     * if any.
     */
    private static void checkActions(String actionName) {
        if (actionName == null) {
            if (loggedActions.size() != 0) {
                throw new RuntimeException("No logged actions expected");
            }
        } else {
            int loggedActionsFound = 0;
            System.out.println("Logged actions : " + loggedActions);
            for (String s : loggedActions) {
                if (s.equals(actionName)) {
                    loggedActionsFound++;
                }
            }
            if (loggedActionsFound != 3) {
                throw new RuntimeException("Incorrect number of actions " +
                        actionName + " : " + loggedActionsFound);
            }
        }
    }

    /*
     * Check context for principal of the test user.
     */
    private static void checkPrincipal(LoginContext loginContext, boolean
            principalShouldExist) {
        if (!principalShouldExist) {
            if (loginContext.getSubject().getPrincipals().size() != 0) {
                throw new RuntimeException("Test failed. Principal was not " +
                        "cleared.");
            }
        } else {
            for (Principal p : loginContext.getSubject().getPrincipals()) {
                if (p instanceof UnixPrincipal &&
                        USER_NAME.equals(p.getName())) {
                    //Proper principal was found, return.
                    return;
                }
            }
            throw new RuntimeException("Test failed. UnixPrincipal "
                    + USER_NAME + " expected.");
        }
    }

    private static class MyCallbackHandler implements CallbackHandler {

        @Override
        public void handle(Callback[] callbacks) throws IOException,
                UnsupportedCallbackException {
            for (Callback callback : callbacks) {
                if (callback instanceof NameCallback) {
                    ((NameCallback) callback).setName(USER_NAME);
                } else if (callback instanceof PasswordCallback) {
                    ((PasswordCallback) callback).setPassword(
                            PASSWORD.toCharArray());
                } else {
                    throw new UnsupportedCallbackException(callback);
                }
            }
        }
    }

    /* -------------------------------------------------------------------------
     * Test login modules
     * -------------------------------------------------------------------------
     */

    /*
     * Login module that should pass through all phases.
     */
    public static class LoginModuleAllPass extends LoginModuleBase {

    }

    /*
     * Login module that throws Exception in abort method.
     */
    public static class LoginModuleWithAbortException extends LoginModuleBase {

        @Override
        public boolean abort() throws LoginException {
            super.abort();
            throw new LoginException("Abort failed!");
        }
    }

    /*
     * Login module that throws Exception in login method.
     */
    public static class LoginModuleWithLoginException extends LoginModuleBase {

        @Override
        public boolean login() throws LoginException {
            super.login();
            throw new FailedLoginException("Login failed!");
        }
    }

    /*
     * Login module that throws Exception in logout method.
     */
    public static class LoginModuleWithLogoutException extends LoginModuleBase {

        @Override
        public boolean logout() throws LoginException {
            super.logout();
            throw new FailedLoginException("Logout failed!");
        }
    }

    /*
     * Base class for login modules
     */
    public static abstract class LoginModuleBase implements LoginModule {
        // initial state
        private Subject subject;
        private CallbackHandler callbackHandler;
        private Map sharedState;
        private Map options;
        private UnixPrincipal userPrincipal;

        // username and password
        private String username;
        private String password;

        // the authentication status
        private boolean succeeded = false;
        private boolean commitSucceeded = false;

        @Override
        public void initialize(Subject subject, CallbackHandler callbackHandler,
                               Map<String, ?> sharedState, Map<String, ?> options) {

            this.subject = subject;
            this.callbackHandler = callbackHandler;
            this.sharedState = sharedState;
            this.options = options;
            System.out.println("Login module initialized.");
        }

        /*
         * Authenticate the user by prompting for a username and password.
         */
        @Override
        public boolean login() throws LoginException {
            LCTest.logAction("login");
            if (callbackHandler == null) {
                throw new LoginException("No CallbackHandler available");
            }

            Callback[] callbacks = new Callback[2];
            callbacks[0] = new NameCallback("Username: ");
            callbacks[1] = new PasswordCallback("Password: ", false);

            try {
                callbackHandler.handle(callbacks);
                username = ((NameCallback) callbacks[0]).getName();
                password = new String(((PasswordCallback) callbacks[1])
                        .getPassword());
                if (username.equals(LCTest.USER_NAME) &&
                        password.equals(LCTest.PASSWORD)) {
                    succeeded = true;
                    return true;
                }
                throw new FailedLoginException("Incorrect username/password!");
            } catch (IOException | UnsupportedCallbackException e) {
                throw new LoginException("Login failed: " + e.getMessage());
            }
        }

        @Override
        public boolean commit() throws LoginException {
            LCTest.logAction("commit");
            if (succeeded == false) {
                return false;
            }
            userPrincipal = new UnixPrincipal(username);
            final Subject s = subject;
            final UnixPrincipal up = userPrincipal;
            java.security.AccessController.doPrivileged
                    ((java.security.PrivilegedAction) () -> {
                        if (!s.getPrincipals().contains(up)) {
                            s.getPrincipals().add(up);
                        }
                        return null;
                    });
            password = null;
            commitSucceeded = true;
            return true;
        }

        @Override
        public boolean abort() throws LoginException {
            LCTest.logAction("abort");
            if (succeeded == false) {
                return false;
            }
            clearState();
            return true;
        }

        @Override
        public boolean logout() throws LoginException {
            LCTest.logAction("logout");
            clearState();
            return true;
        }

        private void clearState() {
            if (commitSucceeded) {
                final Subject s = subject;
                final UnixPrincipal up = userPrincipal;
                java.security.AccessController.doPrivileged
                        ((java.security.PrivilegedAction) () -> {
                            s.getPrincipals().remove(up);
                            return null;
                        });
            }
            username = null;
            password = null;
            userPrincipal = null;
        }
    }

}