/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2004-2008 Bull S.A.
 * Contact: jonas-team@objectweb.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.objectweb.jonas.security.realm.web.catalina55;

import java.io.IOException;
import java.security.Principal;
import java.security.acl.Group;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;

import javax.security.auth.Subject;
import javax.security.auth.login.AccountExpiredException;
import javax.security.auth.login.CredentialExpiredException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.catalina.Context;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.deploy.SecurityConstraint;
import org.apache.catalina.realm.Constants;
import org.apache.catalina.realm.GenericPrincipal;
import org.apache.catalina.realm.RealmBase;
import org.apache.catalina.util.StringManager;

import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.security.SecurityService;
import org.objectweb.jonas.security.auth.JSigned;
import org.objectweb.jonas.security.auth.callback.NoInputCallbackHandler;
import org.objectweb.jonas.security.realm.factory.JResource;
import org.objectweb.jonas.security.realm.factory.JResourceException;
import org.objectweb.jonas.security.realm.principals.User;
import org.objectweb.jonas.service.ServiceManager;
import org.objectweb.jonas.web.lib.PermissionManager;

import org.objectweb.security.context.SecurityContext;
import org.objectweb.security.context.SecurityCurrent;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

/**
 * <p>
 * Implementation of a Realm. (by a wrapper) Use any JOnAS realm by specifying
 * the resource name This implementation manages the security with JACC
 * specification It implements Cloneable to allow clones. Each context must have
 * its own Realm. A realm can not be shared across different contexts or an
 * engine. This is because each realm is associated to a permission manager
 *
 * Extends the Realmbase class of the Tomcat Server.
 *
 * @author Florent Benoit
 */
public class JACC extends RealmBase implements Cloneable {

    /**
     * Descriptive information about this Realm implementation.
     */
    private static final String NAME = "JACC_Catalina55";

    /**
     * Descriptive information about this Realm implementation.
     */
    private static final String INFO = "org.objectweb.jonas.security.realm.JRealmJACCCatalina50/1.0";

    /**
     * Name used in the JAAS config file
     */
    private static final String JAAS_CONFIG_NAME = "tomcat";

    /**
     * The string manager for this package.
     */
    private static StringManager sm = StringManager.getManager(Constants.Package);

    /**
     * The logger used in JOnAS
     */
    private static Logger logger = null;

    /**
     * The resource we will use to authenticate users and identify associated
     * roles.
     */
    private JResource jResource = null;

    /**
     * The name of the resource
     */
    private String resourceName = null;

    /**
     * Reference to the JOnAS security service
     */
    private SecurityService securityService = null;

    /**
     * Permission manager used by this realm for JACC permissions
     */
    private PermissionManager permissionManager = null;

    /**
     * Last request that has been send to hasUserDataPermission or
     * hasResourcePermission methods Used in hasRole to know the current servlet
     * name.
     * This request is store in a local thread.
     */
    private ThreadLocal lastRequestThread = new ThreadLocal();

    /**
     * Context (used to retrieve web.xml informations)
     */
    private Context context = null;

    /**
     * Name of this Realm for traces
     */
    private String realmName = NAME;


    /**
     * Return descriptive information about this Realm implementation and the
     * corresponding version number, in the format
     * <code>&lt;description&gt;/&lt;version&gt;</code>.
     *
     * @return the info.
     */
    public String getInfo() {
        return INFO;
    }

    /**
     * Return the resource name we will be using.
     *
     * @return the resource name.
     */
    public String getResourceName() {
        return resourceName;
    }

    /**
     * Set the resource name we will be using.
     *
     * @param resourceName The new resource name
     */
    public void setResourceName(String resourceName) {
        this.resourceName = resourceName;

    }

    /**
     * Set the permission manager used by this realm
     *
     * @param permissionManager the permission manager to use
     */
    public void setPermissionManager(PermissionManager permissionManager) {
        this.permissionManager = permissionManager;

    }

    /**
     * Return the SecurityConstraints configured to guard the request URI for
     * this request, or <code>null</code> if there is no such constraint.
     *
     * @param request Request we are processing
     * @param context Context the Request is mapped to
     * @return security constraints configured to guard the request URI
     */
    public SecurityConstraint[] findSecurityConstraints(Request request, Context context) {
        // Use super Method
        return super.findSecurityConstraints(request, context);
    }

    /**
     * Perform access control based on the specified authorization constraint.
     *
     * @return <code>true</code> if this constraint is satisfied and
     *         processing should continue, or <code>false</code> otherwise.
     *
     * @param request Request we are processing
     * @param response Response we are creating
     * @param constraints Security constraint we are enforcing
     * @param context The Context to which client of this class is attached.
     *
     * @exception IOException if an input/output error occurs
     */
    public boolean hasResourcePermission(Request request, Response response, SecurityConstraint[] constraints,
            Context context) throws IOException {

        // Update request
        lastRequestThread.set(request);

        // --- Use code of RealmBase for the Login / Error pages

        // Specifically allow access to the form login and form error pages
        // and the "j_security_check" action
        LoginConfig config = context.getLoginConfig();
        if (config != null && Constants.FORM_METHOD.equals(config.getAuthMethod())) {
            String requestURI = request.getDecodedRequestURI();
            String loginPage = context.getPath() + config.getLoginPage();
            if (loginPage.equals(requestURI)) {
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, realmName + "Allow access to login page " + loginPage);
                }
                return true;
            }

            String errorPage = context.getPath() + config.getErrorPage();
            if (errorPage.equals(requestURI)) {
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, realmName + "Allow access to error page " + errorPage);
                }
                return true;
            }
            if (requestURI.endsWith(Constants.FORM_ACTION)) {
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, realmName + "Allow access to username/password submission");
                }
                return true;
            }
        }

        // Which user principal have we already authenticated?
        Principal principal = request.getUserPrincipal();

        // --- End of code from RealmBase class

        String[] roles = null;
        String principalName = null;
        if (principal instanceof GenericPrincipal) {
            roles = ((GenericPrincipal) principal).getRoles();
            principalName = principal.getName();
        }

        if (permissionManager == null) {
            logger.log(BasicLevel.ERROR, realmName + "No permission manager is set. It means that you are using this realm without using the JOnAS deployer but only Tomcat.");
            return false;
        }

        boolean hasResourcePermission = permissionManager.checkWebResourcePermission(request, principalName, roles);

        // Need to send HTTP status code as invoke() method of Authenticator
        // expect that it is
        // done by the realm.
        if (!hasResourcePermission) {
            // Return a "Forbidden" message denying access to this resource
            response.sendError(HttpServletResponse.SC_FORBIDDEN, sm
                    .getString("realmBase.forbidden"));
        }

        return hasResourcePermission;
    }

    /**
     * @return <code>true</code> if the specified Principal has the specified
     *         security role, within the context of this Realm; otherwise return
     *         <code>false</code>. This method can be overridden by Realm
     *         implementations, but the default is adequate when an instance of
     *         <code>GenericPrincipal</code> is used to represent
     *         authenticated Principals from this Realm.
     *
     * @param principal Principal for whom the role is to be checked
     * @param role Security role to be checked
     */
    public boolean hasRole(Principal principal, String role) {

        if ((principal == null) || (role == null) || !(principal instanceof GenericPrincipal)) {
            return false;
        }

        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, realmName + "Principal = " + principal);
            logger.log(BasicLevel.DEBUG, realmName + "Role = " + role);
        }

        if (context == null) {
            logger.log(BasicLevel.ERROR, realmName + "Cannot find a servlet name for isUserInRole() as no context was found");
            return false;
        }

        Request req = (Request) lastRequestThread.get();

        if (req == null) {
            logger.log(BasicLevel.ERROR, realmName + "Cannot find a servlet name for isUserInRole(). No previous request !");
            return false;
        }
        String servletName = findServletName(req);
        String[] roles = null;
        String principalName = null;

        if (principal instanceof GenericPrincipal) {
            roles = ((GenericPrincipal) principal).getRoles();
            principalName = principal.getName();
        }

        if (permissionManager == null) {
            logger.log(BasicLevel.ERROR, realmName + "No permission manager is set. It means that you are using this realm without using the JOnAS deployer but only Tomcat.");
            return false;
        }

        boolean hasRole = permissionManager.checkWebRoleRefPermission(req, servletName, principalName, roles, role);
        return hasRole;
    }

    /**
     * @return the name of the servlet (or "" if it's a JSP for example) This
     *         servlet name is used to build JACC permission for the hasRole
     *         method
     *
     * @param request the servlet request with which we have to find
     *        servlet name
     */
    private String findServletName(Request request) {

        // Pattern of the user (remove path)
        String userPattern = request.getRequestURI().substring(request.getContextPath().length());

        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, realmName + "User Pattern = " + userPattern);
        }

        String servletName = "";
        String[] patterns = context.findServletMappings();
        boolean foundServlet = false;
        String pattern = "";
        int i = 0;

        // Try to search servlet name
        while ((i < patterns.length) && !foundServlet) {
            pattern = patterns[i];
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, realmName + "Pattern found = " + pattern);
                logger.log(BasicLevel.DEBUG, realmName + "Servlet name for pattern = " + context.findServletMapping(pattern));
            }

            // Extension pattern and ends with extension
            if (pattern.startsWith("*.") && userPattern.endsWith(pattern.substring(1))) {
                foundServlet = true;
                continue;
            }

            // Exact Servlet name (ie pattern = /ServletName and userPattern =
            // /ServletName)
            if (pattern.equals(userPattern)) {
                foundServlet = true;
                continue;
            }

            i++;
        }

        if (foundServlet) {
            servletName = context.findServletMapping(pattern);
            // JSP case. servlet name must be empty as required by JACC
            // specification
            if (servletName.equals("jsp")) {
                servletName = "";
            }
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, realmName + "Found servlet name = " + servletName);
            }
        }
        return servletName;
    }

    /**
     * Enforce any user data constraint required by the security constraint
     * guarding this request URI.
     *
     * @return <code>true</code> if this constraint was not violated and
     *         processing should continue, or <code>false</code> if we have
     *         created a response already.
     *
     * @param request Request we are processing
     * @param response Response we are creating
     * @param constraints Security constraints being checked
     *
     * @exception IOException if an input/output error occurs
     */
    public boolean hasUserDataPermission(Request request, Response response, SecurityConstraint[] constraints)
            throws IOException {

        // Update request
        lastRequestThread.set(request);

        // ---- Start of copy from RealmBase class ---

        // Validate the request against the user data constraint
        if (request.getRequest().isSecure()) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, realmName + "User data constraint already satisfied");
            }
            return true;
        }

        // Which user principal have we already authenticated?
        Principal principal = ((HttpServletRequest) request).getUserPrincipal();

        // ---- End of copy from RealmBase class ---

        String[] roles = null;
        String principalName = null;
        if (principal instanceof GenericPrincipal) {
            roles = ((GenericPrincipal) principal).getRoles();
            principalName = principal.getName();
        }

        // ---- Start of copy from RealmBase class ---
        for (int i = 0; i < constraints.length; i++) {
            SecurityConstraint constraint = constraints[i];
            // Use redirect only if it the transport protocol is integral or
            // confidential
            String userConstraint = constraint.getUserConstraint();

            // Redirect only if the constraint is INTEGRAL or CONFIDENTIAL !
            if (userConstraint != null
                    && (userConstraint.equals(Constants.INTEGRAL_TRANSPORT) || userConstraint
                            .equals(Constants.CONFIDENTIAL_TRANSPORT))) {
                // Initialize variables we need to determine the appropriate action
                int redirectPort = request.getConnector().getRedirectPort();

                // Is redirecting disabled?
                if (redirectPort <= 0) {
                    if (logger.isLoggable(BasicLevel.DEBUG)) {
                        logger.log(BasicLevel.DEBUG, realmName + "SSL redirect is disabled");
                    }
                    response.sendError(HttpServletResponse.SC_FORBIDDEN, request.getRequestURI());
                    return false;
                }

                // Redirect to the corresponding SSL port
                StringBuffer file = new StringBuffer();
                String protocol = "https";
                String host = request.getServerName();
                // Protocol
                file.append(protocol).append("://");
                // Host with port
                file.append(host).append(":").append(redirectPort);
                // URI
                file.append(request.getRequestURI());
                String requestedSessionId = request.getRequestedSessionId();
                if ((requestedSessionId != null) && request.isRequestedSessionIdFromURL()) {
                    file.append(";jsessionid=");
                    file.append(requestedSessionId);
                }
                String queryString = request.getQueryString();
                if (queryString != null) {
                    file.append('?');
                    file.append(queryString);
                }

                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, realmName + "Redirecting to " + file.toString());
                }
                response.sendRedirect(file.toString());

                return false;
            }
        }

        // ---- End of copy from RealmBase ---
        if (permissionManager == null) {
            logger.log(BasicLevel.ERROR, realmName + "No permission manager is set. It means that you are using this realm without using the JOnAS deployer but only Tomcat.");
            return false;
        }

        // If Transport protocol is NONE :
        boolean hasUserDataPermission = permissionManager.checkWebUserDataPermission(request,  principalName, roles);
        return hasUserDataPermission;

    }

    /**
     * Return the Principal associated with the specified username and
     * credentials, if there is one; otherwise return <code>null</code>.
     *
     * @param username Username of the Principal to look up
     * @param credentials Password or other credentials to use in authenticating
     *        this username
     * @return the principal associated
     */
    public Principal authenticate(String username, String credentials) {
        // Use JOnAS resource if present
        if (jResource != null) {
            return authenticateResource(username, credentials);
        } else {
            // else use JAAS mechanism
            return authenticateJAAS(username, credentials);
        }
    }



    /**
     * Return the Principal associated with the specified username and
     * credentials, if there is one; otherwise return <code>null</code>.
     *
     * @param username Username of the Principal to look up
     * @param credentials Password or other credentials to use in authenticating
     *        this username
     * @return the principal associated
     */
    public Principal authenticateResource(String username, String credentials) {

        // No authentication can be made with a null username
        if (username == null) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, realmName + "No username so no authentication");
            }
            return null;
        }

        // Does a user with this username exist?
        User user = null;
        try {
            user = jResource.findUser(username);
        } catch (Exception jre) {
            // could not retrieve user
            logger.log(BasicLevel.ERROR, realmName + "Can not find the user : " + jre.getMessage());
            return null;
        }

        // User was not found
        if (user == null) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, realmName + "User " + username + " not found.");
            }
            return null;
        }

        boolean validated = jResource.isValidUser(user, credentials);
        if (!validated) {
            logger.log(BasicLevel.ERROR, realmName + "The password for the user " + username + " is not valid");
            return null;
        }

        ArrayList combinedRoles = null;
        try {
            combinedRoles = jResource.getArrayListCombinedRoles(user);
        } catch (JResourceException jre) {
            logger.log(BasicLevel.ERROR, realmName + jre.getMessage(), jre);
            return null;
        }

        GenericPrincipal principal = new GenericPrincipal(this, user.getName(), user.getPassword(), combinedRoles);
        SecurityContext ctx = new SecurityContext(principal.getName(), combinedRoles);
        SecurityCurrent current = SecurityCurrent.getCurrent();
        current.setSecurityContext(ctx);

        return principal;
    }


    /**
     * Return the Principal associated with the specified username and
     * credentials, if there is one; otherwise return <code>null</code>.
     *
     * @param username Username of the Principal to look up
     * @param credentials Password or other credentials to use in
     *                    authenticating this username
     * @return the principal associated
     */
    public Principal authenticateJAAS(String username, String credentials) {

        // No authentication can be made with a null username
        if (username == null) {
            log("No username so no authentication");
            return null;
        }
        // Establish a LoginContext to use for authentication
        LoginContext loginContext = null;
        try {
            loginContext = new LoginContext(JAAS_CONFIG_NAME, new NoInputCallbackHandler(username, credentials));
        } catch (LoginException e) {
            logger.log(BasicLevel.ERROR, "loginException for user '" + username + "'", e);
            return null;
        }
        // Negotiate a login via this LoginContext
        Subject subject = null;
        try {
            loginContext.login();
            subject = loginContext.getSubject();
            if (subject == null) {
                if (logger.isLoggable(BasicLevel.ERROR)) {
                    logger.log(BasicLevel.ERROR, "failedLoginlogin for user :" + username);
                }
                return null;
            }
        } catch (AccountExpiredException e) {
            if (logger.isLoggable(BasicLevel.ERROR)) {
                logger.log(BasicLevel.ERROR, "accountExpired for user :" + username, e);
            }
            return null;
        } catch (CredentialExpiredException e) {
            if (logger.isLoggable(BasicLevel.ERROR)) {
                logger.log(BasicLevel.ERROR, "credentialExpired for user :" + username, e);
            }
            return null;
        } catch (FailedLoginException e) {
            if (logger.isLoggable(BasicLevel.ERROR)) {
                logger.log(BasicLevel.ERROR, "failedLogin for user :" + username, e);
            }
            return null;
        } catch (LoginException e) {
            if (logger.isLoggable(BasicLevel.ERROR)) {
                logger.log(BasicLevel.ERROR, "loginException for user :" + username, e);
            }
            return null;
        }

        // Get credentials iterators from the subject
        Iterator credentialsIterator = subject.getPrivateCredentials().iterator();
        String credential = (String) credentialsIterator.next();

        // Retrieve first principal name found (without groups)
        Iterator iterator = subject.getPrincipals(Principal.class).iterator();
        String userName = null;
        while (iterator.hasNext() && (userName == null)) {
            Principal principal = (Principal) iterator.next();
            if (!(principal instanceof Group)) {
               userName = principal.getName();
            }
        }

        // No name --> error
        if (userName == null) {
            logger.log(BasicLevel.ERROR, "No Username found in the subject");
            return null;
        }

        // Signed object (if any)
        JSigned jSigned = null;

        // Retrieve all roles of the user (Roles are members of the Group.class)
        iterator = subject.getPrincipals(Group.class).iterator();
        ArrayList roles = new ArrayList();
        while (iterator.hasNext()) {
            Group group = (Group) iterator.next();

            // Signed group (empty group that contains a signature)?
            if (group instanceof JSigned) {
                jSigned = (JSigned) group;
                continue;
            }

            Enumeration e = group.members();
            while (e.hasMoreElements()) {
                Principal p = (Principal) e.nextElement();
                roles.add(p.getName());
            }
        }

        JOnASGenericPrincipal principal = new JOnASGenericPrincipal(this, userName, credential, roles);
        //instanciation of the security context
        SecurityContext ctx = new SecurityContext(userName, roles);
        // Signed ?
        if (jSigned != null) {
            ctx.setSignature(jSigned.getSignature());
            principal.setSignature(jSigned.getSignature());
        }

        SecurityCurrent current = SecurityCurrent.getCurrent();
        current.setSecurityContext(ctx);

        return principal;
    }



    /**
     * Return the Principal associated with the specified chain of X509 client
     * certificates. If there is none, return <code>null</code>.
     *
     * @param cert Array of client certificates, with the first one in the array
     *        being the certificate of the client itself.
     * @return the associated Principal
     */
    public Principal authenticate(X509Certificate[] cert) {
        String dn = cert[0].getSubjectDN().getName();
        return authenticate(dn, "tomcat");
    }

    /**
     * Return a short name for this Realm implementation.
     *
     * @return the name
     */
    protected String getName() {
        return NAME;
    }

    /**
     * Return the password associated with the given principal's user name.
     *
     * @param username the given principal's user name.
     * @return the password associated.
     */
    protected String getPassword(String username) {
        return null;
    }

    /**
     * Return the Principal associated with the given user name.
     *
     * @param username the given principal's user name.
     * @return the Principal associated
     */
    protected Principal getPrincipal(String username) {
        return null;
    }

    /**
     * Set the context of this Realm. This is used to retrieve xml information
     * of the web.xml file
     *
     * @param context Context for this realm
     */
    public void setContext(Context context) {
        this.context = context;
        StringBuffer sb = new StringBuffer();
        sb.append("[");
        sb.append(NAME);
        sb.append(":");
        sb.append(resourceName);
        sb.append(":");
        if (context != null) {
            sb.append(context.getName());
        }
        sb.append("] ");
        this.realmName = sb.toString();
    }

    /**
     * Prepare for active use of the public methods of this Component.
     *
     * @exception LifecycleException if this component detects a fatal error
     *            that prevents it from being started
     */
    public synchronized void start() throws LifecycleException {

        if (logger == null) {
            logger = Log.getLogger(Log.JONAS_SECURITY_PREFIX);
        }

        // Get the Security Service
        try {
            securityService = (SecurityService) ServiceManager.getInstance().getSecurityService();
        } catch (Exception e) {
            // Can't retrieve Security service
            throw new LifecycleException("can't retrieve Security service");
        }

        // Get the resource from the security service if working with resource mode
        if (resourceName != null) {
            jResource = securityService.getJResource(resourceName);
            if (jResource == null) {
                throw new LifecycleException("Can't retrieve resource '" + resourceName + "' from the security service");

            }
        }

        // Perform normal superclass initialization
        super.start();

    }

    /**
     * Gracefully shut down active use of the public methods of this Component.
     *
     * @exception LifecycleException if this component detects a fatal error
     *            that needs to be reported
     */
    public synchronized void stop() throws LifecycleException {
        // Perform normal superclass finalization
        super.stop();

        // Release reference to our resource
        jResource = null;
    }

    /**
     * Log a message on the Logger associated with our Container (if any)
     *
     * @param message Message to be logged
     */
    protected void log(String message) {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, message);
        }
    }

    /**
     * Creates and returns a copy of this object.
     *
     * @return copy of this object.
     * @throws CloneNotSupportedException if the copy fails
     */
    public Object clone() throws CloneNotSupportedException {
        JACC jRealm = new JACC();
        jRealm.setResourceName(resourceName);
        return jRealm;
    }

    /**
     * @return the permission manager used by this realm.
     */
    public PermissionManager getPermissionManager() {
        return permissionManager;
    }


}