/*
 * 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 flex.messaging.security;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.Principal;
import java.util.List;

import flex.messaging.FlexComponent;
import flex.messaging.FlexContext;
import flex.messaging.FlexSession;
import flex.messaging.HttpFlexSession;
import flex.messaging.config.ConfigMap;
import flex.messaging.config.ConfigurationException;
import flex.messaging.config.SecurityConstraint;
import flex.messaging.endpoints.Endpoint;
import flex.messaging.log.Log;
import flex.messaging.log.LogCategories;

/**
 * Much of this logic has been taken from the Flash Remoting Gateway.
 * <p>
 * Since each application server manages sessions, users and security
 * differently, a separate LoginCommand needs to be written for
 * each server.
 * </p>
 */
public class LoginManager implements FlexComponent {
    /**
     * Log category for LoginManager.
     */
    public static final String LOG_CATEGORY = LogCategories.SECURITY;

    private static final String NIOHTTP_FLEX_SESSION_TYPE = "flex.messaging.endpoints.NIOHTTPFlexSession";
    private static final String INVALIDATE_METHOD = "invalidate";

    // Exception/error message numbers.
    private static final int INVALID_LOGIN = 10050;
    private static final int LOGIN_REQ = 10051;
    private static final int NO_LOGIN_COMMAND = 10053;
    private static final int CANNOT_REAUTH = 10054;
    private static final int ACCESS_DENIED = 10055;
    private static final int LOGIN_REQ_FOR_AUTH = 10056;
    private static final int NO_BASIC_SECURITY = 10057;
    private static final int PER_CLIENT_ANT_APPSERVER = 10065;

    private LoginCommand loginCommand;
    private boolean perClientAuthentication;

    private boolean started;

    //--------------------------------------------------------------------------
    //
    // Constructor
    //
    //--------------------------------------------------------------------------

    /**
     * Creates a new <code>LoginManager</code> instance.
     */
    public LoginManager() {
        perClientAuthentication = false;
    }

    //--------------------------------------------------------------------------
    //
    // Initialize, validate, start, and stop methods.
    //
    //--------------------------------------------------------------------------

    /**
     * Implements FlexComponents.initialize.
     * This is no-op for LoginManager as it does not have an id and all
     * its properties are directly settable.
     *
     * @param id        The id of the component.
     * @param configMap The properties for configuring component.
     */
    public void initialize(String id, ConfigMap configMap) {
        // No-op
    }

    /**
     * Validates the LoginManager before it is started.
     */
    protected void validate() {
        if (perClientAuthentication && loginCommand instanceof AppServerLoginCommand) {
            // Cannot use application server authentication together with per client authentication.
            ConfigurationException configException = new ConfigurationException();
            configException.setMessage(PER_CLIENT_ANT_APPSERVER);
            throw configException;
        }
    }

    /**
     * Implements FlexComponent.start.
     * Starts the <code>LoginManager</code>.
     */
    public void start() {
        if (started)
            return;

        validate();

        if (loginCommand != null)
            loginCommand.start(FlexContext.getServletConfig());

        started = true;
    }

    /**
     * Implements FlexComponents.stop.
     * Stops the <code>LoginManager</code>.
     */
    public void stop() {
        if (!started)
            return;

        if (loginCommand != null)
            loginCommand.stop();

        started = false;
    }

    //--------------------------------------------------------------------------
    //
    // Public Methods
    //
    //--------------------------------------------------------------------------

    /**
     * Returns whether per client authentication is enabled or not.
     *
     * @return <code>true</code> if per client authentication is enabled;
     * otherwise <code>false</code>.
     */
    public boolean isPerClientAuthentication() {
        return perClientAuthentication;
    }

    /**
     * Sets whether per client authentication is enabled or not.
     *
     * @param perClientAuthentication <code>true</code> if per client authentication
     *                                is enabled; otherwise <code>false</code>.
     */
    public void setPerClientAuthentication(boolean perClientAuthentication) {
        this.perClientAuthentication = perClientAuthentication;
    }

    /**
     * Implements FlexComponent.isStarted.
     * Returns whether the LoginManager is started or not.
     *
     * @return <code>true</code> if the LoginManager is started; otherwise <code>false</code>.
     */
    public boolean isStarted() {
        return started;
    }

    /**
     * Returns the login command used.
     *
     * @return loginCommand The login command used.
     */
    public LoginCommand getLoginCommand() {
        return loginCommand;
    }

    /**
     * Sets the login command used.
     *
     * @param value The login command to set.
     */
    public void setLoginCommand(LoginCommand value) {
        if (loginCommand == value)
            return;

        loginCommand = value;
        if (started)
            loginCommand.start(FlexContext.getServletConfig());
    }

    /**
     * Perform login with username and credentials.
     *
     * @param username    Username to use to login.
     * @param credentials Credentials to use to login.
     */
    public void login(String username, Object credentials) {
        if (getCurrentPrincipal() == null) {
            if (loginCommand == null) {
                // Client needs to be externally authenticated via Basic Authentication or some other method.
                SecurityException se = new SecurityException();
                se.setMessage(NO_LOGIN_COMMAND);
                se.setCode(SecurityException.SERVER_AUTHENTICATION_CODE);
                throw se;
            }

            if (username != null && credentials != null) {
                Principal authenticated = loginCommand.doAuthentication(username, credentials);

                if (authenticated == null) // Invalid login.
                {
                    SecurityException se = new SecurityException();
                    se.setMessage(INVALID_LOGIN);
                    se.setCode(SecurityException.CLIENT_AUTHENTICATION_CODE);
                    throw se;
                }
                setCurrentPrincipal(authenticated);
            } else {
                // Login is required but the client passed null principal and credentials.
                SecurityException se = new SecurityException();
                se.setMessage(LOGIN_REQ);
                se.setCode(SecurityException.CLIENT_AUTHENTICATION_CODE);
                throw se;
            }
        } else {
            // It is possible that the username passed in from the client and that stored in the
            // Principal on the session may be different. To facilitate this case a LoginCommand
            // must implement LoginCommandExt and the user stored in the Principal is retrieved
            // here for comparison.
            String comparisonUsername = loginCommand instanceof LoginCommandExt ?
                    ((LoginCommandExt) loginCommand).getPrincipalNameFromCredentials(username, credentials) : username;

            // If we have a username and a different existing principal then we must raise an exception
            // as we don't allow re-authentication for a given session.
            if (comparisonUsername != null && !comparisonUsername.equals(getCurrentPrincipal().getName())) {
                // Cannot re-authenticate in the same session.
                SecurityException se = new SecurityException();
                se.setMessage(CANNOT_REAUTH);
                se.setCode(SecurityException.CLIENT_AUTHENTICATION_CODE);
                throw se;
            }
        }
    }

    /**
     * Perform logout.
     */
    public void logout() {
        if (loginCommand == null) {
            FlexContext.getFlexSession().invalidate();

            // External login command required. Please check your security configuration.
            SecurityException se = new SecurityException();
            se.setMessage(NO_LOGIN_COMMAND);
            se.setCode(SecurityException.SERVER_AUTHORIZATION_CODE);
            throw se;
        }

        // Always invoke the command's logout hook.
        loginCommand.logout(getCurrentPrincipal());

        if (FlexContext.isPerClientAuthentication()) {
            FlexContext.setUserPrincipal(null);
        } else {
            FlexSession session = FlexContext.getFlexSession();
            session.invalidate();
        }
    }

    /**
     * Throws various <code>SecurityException</code>s depending on whether an authenticated user
     * is associated with the current session and if one is whether the passed constraint prohibits
     * this user from performing an operation.  If a valid user is found and passed the constraint,
     * no exceptions are thrown
     *
     * @param constraint Constraint against which the current user is authorized.
     */
    public void checkConstraint(SecurityConstraint constraint) {
        if (constraint == null)
            return;

        Principal currentPrincipal = getCurrentPrincipal();

        if (currentPrincipal != null) {
            List roles = constraint.getRoles();
            boolean authorized = roles == null || checkRoles(currentPrincipal, roles);

            if (!authorized) {
                // Access denied. User not authorized.
                SecurityException se = new SecurityException();
                se.setMessage(ACCESS_DENIED);
                se.setCode(SecurityException.CLIENT_AUTHORIZATION_CODE);
                throw se;
            }
        } else {
            if (!isCustomAuth(constraint)) {
                // Some endpoints (NIO) do not support HTTP Basic authentication.
                if (FlexContext.getHttpResponse() == null) {
                    Endpoint endpoint = FlexContext.getEndpoint();
                    String endpointId = (endpoint != null) ? endpoint.getId() : "unknown";
                    // A resource protected by a security constraint that specifies Basic security was accessed via the ''{0}''
                    // endpoint which does not support HTTP Basic security. Please use custom security or an alternate endpoint.
                    SecurityException se = new SecurityException();
                    se.setMessage(NO_BASIC_SECURITY, new Object[]{constraint.getId(), endpointId});
                    se.setCode(SecurityException.CLIENT_AUTHORIZATION_CODE);
                    throw se;
                }
                // What goes back will cause basic user dialog
                FlexContext.getHttpResponse().setStatus(401);
                FlexContext.getHttpResponse().addHeader("WWW-Authenticate", "Basic realm=\"default\"");
            }
            // Login required before authorization can proceed.
            SecurityException se = new SecurityException();
            se.setMessage(LOGIN_REQ_FOR_AUTH);
            se.setCode(SecurityException.CLIENT_AUTHENTICATION_CODE);
            throw se;
        }
    }

    /**
     * Returns true if the passed in principal belongs to at least one of the
     * roles in the passed in list of roles.
     *
     * @param principal Principal to check against roles
     * @param roles     list of roles
     * @return true if principal belongs to at least one of the roles in the list
     */
    public boolean checkRoles(Principal principal, List roles) {
        if (loginCommand == null) // This should not happen but just in case.
        {
            if (Log.isWarn())
                Log.getLogger(LOG_CATEGORY).warn
                        ("Login command is null. Please ensure that the login-command"
                                + " tag has the correct server attribute value"
                                + ", or use 'all' to use the login command regardless of the server.");
            return false;
        }
        return loginCommand.doAuthorization(principal, roles);
    }

    //--------------------------------------------------------------------------
    //
    // Protected and Private methods
    //
    //--------------------------------------------------------------------------

    private Principal getCurrentPrincipal() {
        return FlexContext.getUserPrincipal();
    }

    private void setCurrentPrincipal(Principal p) {
        FlexContext.setUserPrincipal(p);
    }

    private boolean isCustomAuth(SecurityConstraint constraint) {
        return SecurityConstraint.CUSTOM_AUTH_METHOD.equals(constraint.getMethod());
    }
}
