/* ====================================================================
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2000-2001 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Apache" and "Apache Software Foundation" and
 *     "Apache Jetspeed" must not be used to endorse or promote products
 *    derived from this software without prior written permission. For
 *    written permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache" or
 *    "Apache Jetspeed", nor may "Apache" appear in their name, without
 *    prior written permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package org.apache.jetspeed.services.security;

import java.util.HashMap;
import javax.servlet.ServletConfig;

// Jetspeed
import org.apache.jetspeed.om.security.JetspeedUser;
import org.apache.jetspeed.om.security.JetspeedUserFactory;
import org.apache.jetspeed.om.security.UserNamePrincipal;

import org.apache.jetspeed.portal.Portlet;

import org.apache.jetspeed.services.JetspeedSecurity;
import org.apache.jetspeed.services.JetspeedUserManagement;
import org.apache.jetspeed.services.JetspeedPortalAccessController;

// Turbine
import org.apache.turbine.services.TurbineServices;
import org.apache.turbine.services.TurbineBaseService;
import org.apache.turbine.services.InitializationException;
import org.apache.turbine.services.resources.ResourceService;

import org.apache.turbine.util.Log;
import org.apache.jetspeed.services.rundata.JetspeedRunData;


/**
 * <p>This is an implementation of the <code>JetspeedSecurityService</code> interface.
 *
 *
 * @author <a href="mailto:david@bluesunrise.com">David Sean Taylor</a>
 * @author <a href="mailto:sgala@hisitech.com">Santiago Gala</a>
 * @version $Id: JetspeedDBSecurityService.java,v 1.22 2003/03/04 00:05:10 sgala Exp $
 */

public class JetspeedDBSecurityService extends TurbineBaseService
                                       implements JetspeedSecurityService
{

    private final static String CONFIG_CASEINSENSITIVE_USERNAME = "caseinsensitive.username";
    private final static String CONFIG_CASEINSENSITIVE_PASSWORD = "caseinsensitive.password";
    private final static String CONFIG_CASEINSENSITIVE_UPPER = "caseinsensitive.upper";
    private final static String CONFIG_LOGON_STRIKE_COUNT = "logon.strike.count";
    private final static String CONFIG_LOGON_STRIKE_MAX = "logon.strike.max";
    private final static String CONFIG_LOGON_STRIKE_INTERVAL = "logon.strike.interval";
    private final static String CONFIG_LOGON_AUTO_DISABLE = "logon.auto.disable";
    private final static String CONFIG_ACTIONS_ANON_DISABLE = "actions.anon.disable";
    private final static String CONFIG_ACTIONS_ALLUSERS_DISABLE = "actions.allusers.disable";

    private final static String CONFIG_NEWUSER_ROLES     = "newuser.roles";
    private final static String CONFIG_DEFAULT_PERMISSION_LOGGEDIN     = "permission.default.loggedin";
    private final static String CONFIG_DEFAULT_PERMISSION_ANONYMOUS     = "permission.default.anonymous";
    private final static String CONFIG_ANONYMOUS_USER = "user.anonymous";
    private final static String [] DEFAULT_PERMISSIONS = {""};
    private final static String [] DEFAULT_CONFIG_NEWUSER_ROLES = 
    { "user" };

    String roles[] = null;
    boolean caseInsensitiveUsername = false;
    boolean caseInsensitivePassword = false;
    boolean caseInsensitiveUpper = true;
    boolean actionsAnonDisable = true;
    boolean actionsAllUsersDisable = false;
    String anonymousUser = "anon";

    int strikeCount = 3;             // 3 within the interval
    int strikeMax = 20;              // 20 total failures 
    long strikeInterval = 300;  // five minutes

    boolean autoLogonDisable = false;

    private static HashMap users = new HashMap();

    private static Object sem = new Object();

    /**
     * This is the early initialization method called by the 
     * Turbine <code>Service</code> framework
     * @param conf The <code>ServletConfig</code>
     * @exception throws a <code>InitializationException</code> if the service
     * fails to initialize
     */
    public synchronized void init(ServletConfig conf) throws InitializationException 
    {
        // already initialized
        if (getInit()) return;

        super.init(conf);

        // get configuration parameters from Jetspeed Resources
        ResourceService serviceConf = ((TurbineServices)TurbineServices.getInstance())
                                                     .getResources(JetspeedSecurityService.SERVICE_NAME);
        
        try
        {
            roles = serviceConf.getStringArray(CONFIG_NEWUSER_ROLES);
        }
        catch (Exception e)
        {}
            
        if (null == roles || roles.length == 0)
        {
            roles = DEFAULT_CONFIG_NEWUSER_ROLES;
        }

        caseInsensitiveUsername = serviceConf.getBoolean(CONFIG_CASEINSENSITIVE_USERNAME, caseInsensitiveUsername);
        caseInsensitivePassword = serviceConf.getBoolean(CONFIG_CASEINSENSITIVE_PASSWORD, caseInsensitivePassword);
        caseInsensitiveUpper = serviceConf.getBoolean(CONFIG_CASEINSENSITIVE_UPPER, caseInsensitiveUpper);

        strikeCount = serviceConf.getInt(CONFIG_LOGON_STRIKE_COUNT, strikeCount);
        strikeInterval = serviceConf.getLong(CONFIG_LOGON_STRIKE_INTERVAL, strikeInterval);
        strikeMax = serviceConf.getInt(CONFIG_LOGON_STRIKE_MAX, strikeMax);

        autoLogonDisable = serviceConf.getBoolean(CONFIG_LOGON_AUTO_DISABLE, autoLogonDisable);
        actionsAnonDisable = serviceConf.getBoolean(CONFIG_ACTIONS_ANON_DISABLE, actionsAnonDisable);
        actionsAllUsersDisable = serviceConf.getBoolean(CONFIG_ACTIONS_ALLUSERS_DISABLE, actionsAllUsersDisable);

        anonymousUser = serviceConf.getString(CONFIG_ANONYMOUS_USER, anonymousUser);

        // initialization done
        setInit(true);
     }


    //////////////////////////////////////////////////////////////////////////
    //
    // Required JetspeedSecurity Functions
    //
    // Required Features provided by default JetspeedSecurity
    //
    //////////////////////////////////////////////////////////////////////////

    /*
     * Factory to create a new JetspeedUser, using JetspeedUserFactory.
     * The class that is created by the default JetspeedUserFactory is configured
     * in the JetspeedSecurity properties:
     *
     *    services.JetspeedSecurity.user.class=
     *        org.apache.jetspeed.om.security.BaseJetspeedUser
     *
     * @return JetspeedUser a newly created user that implements JetspeedUser.
     */
    public JetspeedUser getUserInstance()
    {
        try
        {
            return JetspeedUserFactory.getInstance();
        }
        catch (UserException e)
        {
            return null;
        }
    }

    //////////////////////////////////////////////////////////////////////////
    //
    // Optional JetspeedSecurity Features 
    //
    // Features are not required to be implemented by Security Provider
    //
    //////////////////////////////////////////////////////////////////////////

    /*
     * During logon, the username can be case sensitive or case insensitive.
     *
     * Given a username, converts the username to either lower or upper case.
     * This optional feature is configurable from the JetspeedSecurity.properties:
     *
     *     <code>services.JetspeedSecurity.caseinsensitive.username = true/false</code>
     *     <code>services.JetspeedSecurity.caseinsensitive.upper = true/false</code>
     *
     * If <code>caseinsensitive.username</code> is true,  
     * then conversion is enabled and the username will be converted before 
     * being sent to the Authentication provider.
     *
     * @param username The username to be converted depending on configuration.
     * @return The converted username.
     *
     */
    public String convertUserName(String username)
    {
        if (caseInsensitiveUsername)
        { 
            username = (caseInsensitiveUpper) ? username.toUpperCase() : username.toLowerCase(); 
        } 
        return username;
    }

    /*
     * During logon, the password can be case sensitive or case insensitive.
     *
     * Given a password, converts the password to either lower or upper case.
     * This optional feature is configurable from the JetspeedSecurity.properties:
     *
     *     <code>services.JetspeedSecurity.caseinsensitive.password = true/false</code>
     *     <code>services.JetspeedSecurity.caseinsensitive.upper = true/false</code>
     *
     * If <code>caseinsensitive.password</code> is true,  
     * then conversion is enabled and the password will be converted before 
     * being sent to the Authentication provider.
     *
     * @param password The password to be converted depending on configuration.
     * @return The converted password.
     *
     */
    public String convertPassword(String password)
    {
        if (caseInsensitivePassword)
        { 
            password = (caseInsensitiveUpper) ? password.toUpperCase() : password.toLowerCase(); 
        } 
        return password;
    }

    /*
     * Logon Failure / Account Disabling Feature
     *
     * Checks and tracks failed user-logon attempts.
     * If the user fails to logon after a configurable number of logon attempts,
     * then the user's account will be disabled.
     *
     * This optional feature is configurable from the JetspeedSecurity.properties:
     *
     *     <code>services.JetspeedSecurity.logon.auto.disable=false</code>
     *
     * The example setting below allows for 3 logon strikes per 300 seconds.
     * When the strike.count is exceeded over the strike.interval, the account
     * is disabled. The strike.max is the cumulative maximum.
     *
     *     <code>services.JetspeedSecurity.logon.strike.count=3</code>
     *     <code>services.JetspeedSecurity.logon.strike.interval=300</code>
     *     <code>services.JetspeedSecurity.logon.strike.max=10</code>
     *
     * These settings are not persisted, and in a distributed environment are 
     * only tracked per node.
     *
     * @param username The username to be checked.
     * @return True if the strike count reached the maximum threshold and the
     *         user's account was disabled, otherwise False.
     *
     */
    public boolean checkDisableAccount(String username)
    {
        username = convertUserName(username);
 
        // TODO: make this work across a cluster of servers
        UserLogonStats stat = (UserLogonStats)users.get(username);
        if (stat == null)
        {
            stat = new UserLogonStats(username);
            synchronized (sem)
            {
                users.put(username, stat);
            }
        }
        boolean disabled = stat.failCheck(strikeCount, strikeInterval, strikeMax);

        if (disabled)
        {
            try
            {
                // disable the account
                JetspeedUser user = (JetspeedUser)JetspeedSecurity.getUser(username);
                if (user != null)
                {
                    user.setDisabled(true);
                    JetspeedSecurity.saveUser(user);
                }
            }
            catch (Exception e)
            {
                 Log.error("Could not disable user: " + username + e);
            }
        }
        return disabled;
    }

    /*
     * Logon Failure / Account Disabling Feature
     *    
     * Returns state of the the logon failure / account disabling feature.
     * 
     * If the user fails to logon after a configurable number of logon attempts,
     * then the user's account will be disabled.
     *
     * @see JetspeedSecurityService#checkLogonFailures
     *
     * @return True if the feature is enabled, false if the feature is disabled.
     *
     */
    public boolean isDisableAccountCheckEnabled()
    {
        return autoLogonDisable;
    }

    
    /*
     * Logon Failure / Account Disabling Feature
     *    
     * Resets counters for the logon failure / account disabling feature.
     * 
     * If the user fails to logon after a configurable number of logon attempts,
     * then the user's account will be disabled.
     *
     * @see JetspeedSecurityService#checkLogonFailures
     *
     * @param username The username to reset the logon failure counters.
     *
     */
    public void resetDisableAccountCheck(String username)
    {
        // TODO: make this work across a cluster of servers
        username = convertUserName(username);
        UserLogonStats stat = (UserLogonStats)users.get(username);
        if (stat == null)           
        {
            stat = new UserLogonStats(username);
            synchronized (sem)
            {
                users.put(username, stat);
            }
        }
        stat.reset();
    }
    

    //////////////////////////////////////////////////////////////////////////
    //
    // Optional JetspeedSecurity Helpers
    //
    //////////////////////////////////////////////////////////////////////////

    /**
     * Helper to UserManagement.
     * Retrieves a <code>JetspeedUser</code> given the primary principle username.
     * The principal can be any valid Jetspeed Security Principal:
     *   <code>org.apache.jetspeed.om.security.UserNamePrincipal</code>
     *   <code>org.apache.jetspeed.om.security.UserIdPrincipal</code>
     *   
     * The security service may optionally check the current user context
     * to determine if the requestor has permission to perform this action.
     *
     * @param username The username principal.
     * @return a <code>JetspeedUser</code> associated to the principal identity.
     * @exception UserException when the security provider has a general failure retrieving a user.
     * @exception UnknownUserException when the security provider cannot match
     *            the principal identity to a user.
     * @exception InsufficientPrivilegeException when the requestor is denied due to insufficient privilege 
     */

    public JetspeedUser getUser(String username) 
        throws JetspeedSecurityException
    {
        return JetspeedUserManagement.getUser(new UserNamePrincipal(username));
    }


    /**
     * Helper to PortalAuthorization.
     * Gets a <code>JetspeedUser</code> from rundata, authorize user to perform the secured action on
     * the given <code>Portlet</code> resource. If the user does not have
     * sufficient privilege to perform the action on the resource, the check returns false,
     * otherwise when sufficient privilege is present, checkPermission returns true.
     *
     * @param rundata request that the user is taken from rundatas
     * @param action the secured action to be performed on the resource by the user.     
     * @param portlet the portlet resource.
     * @return boolean true if the user has sufficient privilege.
     */
    public boolean checkPermission(JetspeedRunData runData, String action, Portlet portlet)
    {
        return JetspeedPortalAccessController.checkPermission(runData.getJetspeedUser(),
                                                       portlet,
                                                       action);
    }

    /**
     * Helper to PortalAuthorization.
     * Gets a <code>JetspeedUser</code> from rundata, authorize user to perform the secured action on
     * the given <code>Entry</code> resource. If the user does not have
     * sufficient privilege to perform the action on the resource, the check returns false,
     * otherwise when sufficient privilege is present, checkPermission returns true.
     *
     * @param rundata request that the user is taken from rundatas
     * @param action the secured action to be performed on the resource by the user.     
     * @param entry the portal entry resource.
     * @return boolean true if the user has sufficient privilege.
    public boolean checkPermission(JetspeedRunData runData, String action, RegistryEntry entry)
    {
        return JetspeedPortalAccessController.checkPermission(runData.getJetspeedUser(),
                                                       entry,
                                                       action);
    }
     */

    /*
     * Security configuration setting to disable all action buttons for the Anon user
     * This setting is readonly and is edited in the JetspeedSecurity deployment
     *    
     *
     * @return True if the feature actions are disabled for the anon user
     *
     */
    public boolean areActionsDisabledForAnon()
    {
        return actionsAnonDisable;
    }

    /*
     * Security configuration setting to disable all action buttons for all users
     * This setting is readonly and is edited in the JetspeedSecurity deployment
     *    
     *
     * @return True if the feature actions are disabled for the all users
     *
     */
    public boolean areActionsDisabledForAllUsers()
    {
        return actionsAllUsersDisable;
    }

   /*
     * Gets the name of the anonymous user account if applicable
     *    
     *
     * @return String the name of the anonymous user account
     *
     */
    public String getAnonymousUserName()
    {
        return anonymousUser;
    }


}

