/**
 * 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.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 org.apache.catalina.LifecycleException;
import org.apache.catalina.realm.RealmBase;
import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.security.auth.JSigned;
import org.objectweb.jonas.security.auth.callback.NoInputCallbackHandler;
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 JAAS Realm. (by a wrapper)
 * It uses the entry JAAS_CONFIG_NAME from the JAAS config file
 * @author Florent Benoit
 * @author Alexandre Thaveau (JAAS support with JOnAS)
 * @author Marc-Antoine Bourgeot (JAAS support with JOnAS)
 */
public class JAAS extends RealmBase {

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

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

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

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

    /**
     * User Certificate
     */
    private X509Certificate cert = null;

    /**
     * User will be transformed in uppercase for the authentication
     */
    private String useUpperCaseUser = null;


    /**
     * 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 Principal associated with the specified X.509 certificate Distinguished Name
     * if there is one; otherwise return <code>null</code>.
     * @param cert X.509 Certificate of the user
     * @return the principal associated
     */
    public Principal authenticate(X509Certificate[] cert) {
        String headerCertificate = "##DN##";
        //concat the header certificate and replace the caracters
        String dn = headerCertificate.concat(cert[0].getSubjectDN().getName().replace('=', '#').replace(',', '%').replace(' ', '$'));
        this.cert = cert[0];
        return authenticate(dn, "");
    }

    /**
     * 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) {

        // Transform username in uppercase if option is enabled
        if (useUpperCaseUser != null && useUpperCaseUser.equals("true")) {
            username = username.toUpperCase();
        }

        // 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, this.cert));
        } 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 = null;
        if (credentialsIterator.hasNext()) {
            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 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;
    }


    /**
     * 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);
        }

        // 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();
   }


    /**
     * 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);
        }
    }


    /**
     * Redirect to setUseUpperCaseUserName() method.
     * @param useUpperCaseUser boolean value : "true" or "false"
     */
    public void setUseUpperCaseUser(String useUpperCaseUser) {
        setUseUpperCaseUserName(useUpperCaseUser);
    }

    /**
     * Authentication will be made with a username in uppercase
     * @param useUpperCaseUser boolean value : "true" or "false"
     */
    public void setUseUpperCaseUserName(String useUpperCaseUser) {
        this.useUpperCaseUser = useUpperCaseUser;
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "Option setUseUpperCaseUserName set to " + useUpperCaseUser);
        }
    }


}
