/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package sample.certsecurity.custom.jsfbean;

import java.io.FileInputStream;
import java.io.Serializable;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXCertPathValidatorResult;
import java.security.cert.PKIXParameters;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.enterprise.context.SessionScoped;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;
import javax.naming.ldap.LdapName;
import javax.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.certsecurity.custom.ejb.LdapEjb;
import sample.certsecurity.custom.ejb.LdapEntry;
import sample.certsecurity.custom.util.FacesUtils;

/**
 *
 * JSF bean to handle the logged user. The bean saves the user information
 * (username and LDAP information) and manages the group membership. It is
 * session scoped.
 * 
 * @author ricky
 */
@Named("loginBean")
@SessionScoped
public class LoginBean implements Serializable {
    
    /**
     * logger for the class
     */
    static private final Logger logger = LoggerFactory.getLogger(LoginBean.class);
    
    @Inject
    private LdapEjb ldap;

    private String username = null;
    private LdapEntry userEntry = null;
    private String password = null;
    private Map<String,Boolean> groupsCache = null;
    //@ManagedProperty(value = "false")
    @Inject @ConfigProperty
    private boolean validatePKIX = false;
    @Inject @ConfigProperty
    private String cacertsFile = "/home/ricky/glassfish3/glassfish/domains/domain1/config/cacerts.jks";
    @Inject @ConfigProperty
    private String cacertsPassword = "changeit";
    private KeyStore cacerts = null;

    /**
     * Creates a new instance of LoginBean 
     */
    public LoginBean() {
        groupsCache = new HashMap<String,Boolean>();
    }

    //
    // GETTERS & SETTERS
    //
    
    /**
     * Getter for the password
     * @return the password
     */
    public String getPassword() {
        return password;
    }

    /**
     * Setter for the password
     * @param password The new password
     */
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * Getter for the username
     * @return The username
     */
    public String getUsername() {
        return username;
    }

    /**
     * Setter for the username
     * @param username The new username
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * Getter of the path to the file against we check the validity of 
     * certificates
     * @return Path to the cacerts file
     */
    public String getCacertsFile() {
        return cacertsFile;
    }

    /**
     * Setter for the cacerts file path. This file is used as the trusted CAs
     * used to validate user certificates when programmatic validation is used.
     * @param cacertsFile The new cacerts file
     */
    public void setCacertsFile(String cacertsFile) {
        this.cacertsFile = cacertsFile;
    }

    /**
     * Getter of the password for cacerts file
     * @return The password of the cacerts file
     */
    public String getCacertsPassword() {
        return cacertsPassword;
    }

    /**
     * Setter for the cacerts password.
     * @param cacertsPassword The new password
     */
    public void setCacertsPassword(String cacertsPassword) {
        this.cacertsPassword = cacertsPassword;
    }

    /**
     * Getter for custom or programmatic validation. If check the user certificate
     * is validate in the login bean.
     * @return the boolean value
     */
    public boolean isValidatePKIX() {
        return validatePKIX;
    }
    
    /**
     * Return if the user is logged in. Just check the username and
     * the ldap userEntry is filled.
     * @return true or false
     */
    public boolean isLoggedIn() {
        boolean res = username != null && userEntry != null;
        logger.debug("Exiting: {}", res);
        return res;
    }

    /**
     * Setter for custom or programmatic validation. If check the user certificate
     * is validate in the login bean.
     * @param validatePKIX The new boolean value
     */
    public void setValidatePKIX(boolean validatePKIX) {
        this.validatePKIX = validatePKIX;
    }

    /**
     * Method that loads the certificates in the cacerts file if 
     * programmatic validation has to be performed.
     */
    private synchronized void loadCaCerts() {
        if (validatePKIX && cacertsFile != null && cacertsPassword != null) {
            try {
                Security.setProperty("ocsp.enable", "true");
                System.setProperty("com.sun.security.enableCRLDP", "true");
                cacerts = KeyStore.getInstance(KeyStore.getDefaultType());
                cacerts.load(new FileInputStream(cacertsFile), cacertsPassword.toCharArray());
            } catch (Exception e) {
                logger.error("Error loading the certificates", e);
            }
        }
    }

    //
    // ACTIONS
    //
    
    /**
     * Normal login action. The user is checked against the ldap using
     * provided username and password. If ok the user is redirected to
     * the main page, if some error it continues in the login page.
     * @return "index" or "login"
     */
    public String login() {
        userEntry = ldap.findUser(username, password.toCharArray());
        groupsCache.clear();
        if (userEntry != null) {
            return "index";
        } else {
            FacesUtils.errorMessage("loginForm:username", "login_invalid_username_or_password");
            return "login";
        }
    }

    /**
     * <p>This method is used by the jsf phase listener to silently login the user
     * if the certificate is provided. The method do the following steps:</p>
     * <ul>
     * <li>The username is get from the certificate Subject. It is hardcoded to
     * get the first value of the RDN. CN=&gt;username&lt;,OU=...</li>
     * <li>Then if the username in the bean and the one in the certificate
     * are different the user is searched in the ldap and the silent login 
     * performed.</li>
     * <li>If local validation is checked the certificate is validated using
     * the cacerts file, CRLs and OCSP.</li>
     * <li>Finally if the certificate is valid the user is searched in the ldap.
     * If there is a coincidence the user is silently logged into the application.</li>
     * </ul>
     * 
     * @param cert The certficated provided by the user
     * @return The username found and validated or null
     */
    public void trustedLogin(X509Certificate cert) {
        try {
            logger.debug("entering trustedLogin");
            // only check if user not logged in
            if (!isLoggedIn()) {
            // get CN part => it's the last one
                LdapName name = new LdapName(cert.getSubjectDN().getName());
                String certName = name.getRdn(name.size() - 1).getValue().toString();
                logger.debug("certName: {}", certName);
                logger.debug("username: {}", username);
                // validate via PKIX if configured
                boolean certOK = !validatePKIX || validatePKIX(cert);
                if (certOK) {
                    // login without password
                    LdapEntry entry = ldap.findUser(certName, null);
                    logger.debug("Found: {}", entry.getDn());
                    if (entry != null) {
                        username = certName;
                        userEntry = entry;
                        groupsCache.clear();
                    }
                }
            }
        } catch (Exception e) {
            logger.error("Error performing the login", e);
        }
    }

    /**
     * Method that validate the certificate using the cacerts configured
     * (CRLs and OCSP are previously activated to be used).
     * @param cert The certificate to validate
     * @return true if ok false otherwise
     */
    public boolean validatePKIX(X509Certificate cert) {
        try {
            if (cacerts == null) {
                loadCaCerts();
            }
            logger.debug("entering validatePKIX");
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            List<X509Certificate> certs = new ArrayList<X509Certificate>();
            certs.add(cert);
            CertPath certPath = cf.generateCertPath(certs);
            CertPathValidator cpv = CertPathValidator.getInstance("PKIX");
            PKIXParameters params = new PKIXParameters(cacerts);
            PKIXCertPathValidatorResult cpv_result =
                    (PKIXCertPathValidatorResult) cpv.validate(certPath, params);
        } catch (Exception e) {
            logger.error("Exception doing validation => false", e);
            return false;
        }
        return true;
    }
    
    /**
     * Method that return if the logged user belongs to a group.
     * @param groupname The group name to check
     * @return true if belong false if not or error
     */
    public boolean isUserInGroup(String groupname) {
        // check cache
        logger.debug("entering isUserInGroup {}", username);
        String key = groupname.toLowerCase();
        if (groupsCache.containsKey(key)) {
            logger.debug("found in cache: {}", groupsCache.get(key));
            return groupsCache.get(key);
        } else {
            // read from ldap and set into cache
            boolean res = ldap.isUserInGroup(userEntry.getDn(), groupname);
            groupsCache.put(key, res);
            logger.debug("get from ldap: {}", groupsCache.get(key));
            return res;
        }
    }
    
    /**
     * Just invalidates the session.
     * @return  "logout" => go to logout page
     */
    public String logout() {
        HttpSession session = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(false);
        if (session != null) {
            session.invalidate();
        }
        return "logout";
    }
}

