/**
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2006 Sun Microsystems Inc. All Rights Reserved
 *
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at
 * https://opensso.dev.java.net/public/CDDLv1.0.html or
 * opensso/legal/CDDLv1.0.txt
 * See the License for the specific language governing
 * permission and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at opensso/legal/CDDLv1.0.txt.
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * $Id: AuthenticatedSharedAgentsCondition.java,v 1.7 2009/09/09 23:52:28 veiming Exp $
 *
 */



package com.sun.identity.policy.plugins;

import java.security.AccessController;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import com.iplanet.sso.SSOException;
import com.iplanet.sso.SSOToken;
import com.sun.identity.policy.interfaces.Condition;
import com.sun.identity.policy.ConditionDecision;
import com.sun.identity.policy.PolicyException;
import com.sun.identity.policy.Syntax;
import com.sun.identity.policy.PolicyEvaluator;
import com.sun.identity.common.CaseInsensitiveHashSet;
import com.sun.identity.idm.AMIdentity;
import com.sun.identity.idm.IdConstants;
import com.sun.identity.idm.IdType;
import com.sun.identity.idm.IdRepoException;
import com.sun.identity.idm.IdUtils;
import com.sun.identity.security.AdminTokenAction;
import com.sun.identity.sm.SMSException;
import com.sun.identity.sm.ServiceConfig;
import com.sun.identity.sm.ServiceConfigManager;
import com.sun.identity.sm.ServiceListener;
import com.sun.identity.shared.debug.Debug;

/**
 * The class <code>AuthenticatedSharedAgentsCondition</code> checks
 * if the authenticated shared agent has the agent name it is trying to read.
 */

public class AuthenticatedSharedAgentsCondition implements Condition, 
    ServiceListener {

    // Instance variables
    private Map properties;

    private String delimiter = "/";

    // Configuration property names
    private static List propertyNames = Collections.EMPTY_LIST;

    private static ServiceConfigManager scm = null;

    private static final String version = "1.0";

    private static final String attributeToRead = "AgentsAllowedToRead";

    private static final String agentserviceName = IdConstants.AGENT_SERVICE;

    private static Map sharedAgentsCache = new HashMap(2);

    private static Map realmCache = new HashMap(2);

    // Debug file
    private static Debug debug = Debug.getInstance("AuthAgents");

    //  Constants for constructing resource names
    static final String RESOURCE_PREFIX = "sms://";
    static final String SVC_RESOURCE_NAME =
        "/sunIdentityRepositoryService/1.0/application/";

    /** No argument constructor 
     */
    public AuthenticatedSharedAgentsCondition() {
        SSOToken adminToken = (SSOToken) AccessController.doPrivileged(
               AdminTokenAction.getInstance());
        try {
            if (scm == null) {
                scm = new ServiceConfigManager(adminToken, agentserviceName,
                    version);
            }
            if (scm != null) {
                scm.addListener(this);
            }
        } catch (SMSException smse) {
            if (debug.warningEnabled()) {
                debug.warning("AuthenticatedSharedAgentsCondition: "
                       + "Unable to init scm due to " + smse);
            }
        } catch (SSOException ssoe) {
            if (debug.warningEnabled()) {
                debug.warning("AuthenticatedSharedAgentsCondition: "
                       + "Unable to init scm due to " + ssoe);
            }
        }
    }

    /**
     * Returns a set of property names for the condition.
     *
     * @return set of property names
     */
    public List getPropertyNames() 
    {
        return (propertyNames);
    }
 
    /**
     * Returns the syntax for a property name
     * @see com.sun.identity.policy.Syntax
     *
     * @param property <code>String</code> representing property name
     *
     * @return <code>Syntax<code> for the property name
     */
     public Syntax getPropertySyntax(String property)
     {
         return (Syntax.ANY);
     }
      
    /**
     * Gets the display name for the property name.
     * The <code>locale</code> variable could be used by the plugin to
     * customize the display name for the given locale.
     * The <code>locale</code> variable could be <code>null</code>, in which
     * case the plugin must use the default locale.
     *
     * @param property property name.
     * @param locale locale for which the property name must be customized.
     * @return display name for the property name.
     * @throws PolicyException if unable to get display name
     */
     public String getDisplayName(String property, Locale locale) 
       throws PolicyException
     {
         return property;
     }
 
     /**
      * Returns a set of valid values given the property name. This method
      * is called if the property Syntax is either the SINGLE_CHOICE or 
      * MULTIPLE_CHOICE.
      *
      * @param property <code>String</code> representing property name
      * @return Set of valid values for the property.
      * @exception PolicyException if unable to get the Syntax.
      */
     public Set getValidValues(String property) throws PolicyException
     {
         return (Collections.EMPTY_SET);
     }


    /** 
     *  Sets the properties of the condition.
     *  Evaluation of ConditionDecision is influenced by these properties.
     *  @param properties of the condition that governs
     *         whether a policy applies. The only defined property
     *         is <code>attributes</code>
     */
    public void setProperties(Map properties) throws PolicyException {

        this.properties = properties;
    }

    /** Gets the properties of the condition.  
     *  @return  map view of properties that govern the 
     *           evaluation of  the condition decision
     *  @see #setProperties(Map)
     */
    public Map getProperties() {
       return (properties == null)
                ? null : Collections.unmodifiableMap(properties);
    }

    /**
     * Gets the decision computed by this condition object.
     *
     * @param token single sign on token of the user
     *
     * @param env request specific environment map of key/value pairs.
     *
     * @return the condition decision. The condition decision 
     *         encapsulates whether a policy applies for the request. 
     *
     * Policy framework continues evaluating a policy only if it 
     * applies to the request as indicated by the CondtionDecision. 
     * Otherwise, further evaluation of the policy is skipped. 
     *
     * @throws SSOException if the token is invalid
     */
    public ConditionDecision getConditionDecision(SSOToken token, Map env) 
            throws PolicyException, SSOException {
        boolean allowed = false;
        if (debug.messageEnabled()) {
            debug.message("AuthenticatedSharedAgentsCondition."+
                "getConditionDecision: " +
                "called with Token: " + token.getPrincipal().getName() +
                ", requestedResourcename: "
                + env.get(PolicyEvaluator.SUN_AM_ORIGINAL_REQUESTED_RESOURCE));
        }
        String realmName = null;
        String sharedAgentName = null;
        String sharedAgentUnivId = null;
        try {
            AMIdentity id = IdUtils.getIdentity(token);
            realmName = id.getRealm();
            sharedAgentName = id.getName();
            sharedAgentUnivId = id.getUniversalId();
        } catch (SSOException ssoe) {
            // Debug it and throe error message.
            if (debug.messageEnabled()) {
                debug.message("AuthenticatedSharedAgentsCondition."
                    +"getConditionDecision: invalid sso token: " 
                    + ssoe.getMessage());
            }
            throw ssoe;
        } catch (IdRepoException ide) {
            // Debug it and throw converted policy exception.
            if (debug.messageEnabled()) {
                debug.message("AuthenticatedSharedAgentsCondition."
                    +"getConditionDecision IdRepo exception: ", ide);
            }
            throw new PolicyException(ide);
        }
        // Get the resource name from the env
        Object o = env.get(PolicyEvaluator.SUN_AM_ORIGINAL_REQUESTED_RESOURCE);
        if (debug.messageEnabled()) {
            debug.message("AuthenticatedSharedAgentsCondition."
                +"getConditionDecision:"
                +" name: " + sharedAgentName + " resource: " + o);
        }
        if (o != null) {
            String resourceName = null;
            if (o instanceof String ) {
                resourceName = (String) o;
            } else if (o instanceof Set) {
                if (!((Set) o).isEmpty()) {
                    resourceName = (String) ((Set) o).iterator().next();
                }
            } else if (debug.warningEnabled()) {
                resourceName = "";
                debug.warning("AuthenticatedSharedAgentsCondition."
                    +"getConditionDecision: Unable to get resource name");
            }

            // Get the agents from the shared list of the tokenID/shared
            // agent.
            // Iterate and compare the agents from the shared list with 
            // the ones under resource from env and if equal assign true.
            // resource from env might be a single element Set.

            try {
                Set agentsFromEnv = new HashSet();
                String agentTypeName = IdType.AGENT.getName();
                String agentOnlyTypeName = IdType.AGENTONLY.getName();
                SSOToken adminToken = (SSOToken) AccessController.doPrivileged(
                    AdminTokenAction.getInstance());
                ServiceConfig orgConfig = getOrgConfig(adminToken, realmName);

                String[] retVal = split(resourceName);
                         
                if ((retVal[0].equalsIgnoreCase(agentTypeName) &&
                    retVal[1].equalsIgnoreCase(agentTypeName)) ||
                    (retVal[0].equalsIgnoreCase(agentOnlyTypeName) &&
                     retVal[1].equalsIgnoreCase(agentOnlyTypeName))) {
                    agentsFromEnv.add(retVal[0]);
                }

                if ((!retVal[0].equalsIgnoreCase(agentTypeName)) &&
                    (!retVal[0].equalsIgnoreCase(agentOnlyTypeName))) {

                    retVal[0] = getAgentNameFromEnv(resourceName);

                    if (retVal[0] == null) {
                        return new ConditionDecision(false);
                    }
                    if (retVal[0].equalsIgnoreCase(sharedAgentName)) {
                        Map envMap = getAttributes(orgConfig, retVal[0]);
                        agentsFromEnv = (Set) envMap.get(attributeToRead);
                    } else {
                        agentsFromEnv.add(retVal[0]);
                    }
                    if (debug.messageEnabled()) {
                        debug.message("AuthenticatedSharedAgentsCondition." +
                            "getConditionDecision: agentsFromEnv: " +
                            agentsFromEnv + "retVal[0] "+retVal[0]);
                    }
                }
                // Check in cache
                if ((sharedAgentsCache != null) &&
                    (sharedAgentsCache.containsKey(sharedAgentUnivId))) {
                    Set agentsfromCache = 
                        (Set) sharedAgentsCache.get(sharedAgentUnivId);

                    if (agentsfromCache != null && 
                        !agentsfromCache.isEmpty()) {
                        allowed = getPermission(agentsFromEnv, 
                            agentsfromCache); 
                    }
                    return new ConditionDecision(allowed);
                }

                // If not in cache.
                // Return the attributes for the given agent under
                // default group.
                Map agentsAttrMap = getAttributes(orgConfig, sharedAgentName);
                Set agentsToRead = (Set) agentsAttrMap.get(attributeToRead);
                if (debug.messageEnabled()) {
                    debug.message("AuthenticatedSharedAgentsCondition." +
                        "getConditionDecision: agentsToRead: " +
                            agentsToRead);
                }

                if (agentsToRead != null && !agentsToRead.isEmpty()) {
                    allowed = getPermission(agentsFromEnv, agentsToRead);
                }
                // Update the cache.
                updateCache(sharedAgentUnivId, agentsToRead);
            } catch (IdRepoException idpe) {
                debug.error("AuthenticatedSharedAgentsCondition."+
                    "getConditionDecision(): Unable to read agent"+
                    " attributes for " + sharedAgentName, idpe);
                throw new PolicyException(idpe);
            }
        }
        return new ConditionDecision(allowed);
    }

    private String getAgentNameFromEnv(String resName) {
        String agentName = null;
        /*
         * This check is for the resource name like this is constructed 
         * from the delegation service while getting the permission
         * requestedResourcename:
         * [sms://dc=openam,dc=forgerock,dc=org/sunIdentityRepositoryService
         * /1.0/application/agentonly/http://quasar.red.iplanet.com:7001/
         * StockService/StockService]
         */
        String rn = resName.toLowerCase();
        // Get the index after the realm name
        int ndx = rn.indexOf(SVC_RESOURCE_NAME.toLowerCase());
        if (ndx != -1) {
            // Get the substring after the realm, server name, version and 
            // application.
            rn = rn.substring(ndx + SVC_RESOURCE_NAME.length());
            // Find the next index of "/" to bypass the agent type
            ndx = rn.indexOf('/');
            if (ndx != -1) {
                // Get the agent name
                agentName = rn.substring(ndx + 1);
            }
        }
        return (agentName);
    }

    /**
     * Returns a copy of this object.
     *
     * @return a copy of this object
     */
    public Object clone() {
        AuthenticatedSharedAgentsCondition theClone = null;
        try {
            theClone = (AuthenticatedSharedAgentsCondition) super.clone();
            theClone.properties = Collections.unmodifiableMap(
                com.sun.identity.sm.SMSUtils.copyAttributes(properties));
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }
        return theClone;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.sun.identity.sm.ServiceListener#globalConfigChanged(
     *      java.lang.String,
     *      java.lang.String, java.lang.String, java.lang.String, int)
     */
    public void globalConfigChanged(String serviceName, String version,
        String groupName, String serviceComponent, int type) {
        if (debug.messageEnabled()) {
            debug.message("AuthenticatedSharedAgentsCondition."+
                "globalConfigChanged..");
        }
        clearCache();
    }

    /*
     * (non-Javadoc)
     *
     * @see com.sun.identity.sm.ServiceListener#organizationConfigChanged(
     *      java.lang.String,
     *      java.lang.String, java.lang.String, java.lang.String,
     *      java.lang.String, int)
     */
    public void organizationConfigChanged(String serviceName, String version,
        String orgName, String groupName, String serviceComponent, int type)

    {
        if (debug.messageEnabled()) {
            debug.message("AuthenticatedSharedAgentsCondition."+
                "organizationConfigChanged..");
        }
        clearCache();
    }

    /*
     * (non-Javadoc)
     *
     * @see com.sun.identity.sm.ServiceListener#schemaChanged(java.lang.String,
     *      java.lang.String)
     */
    public void schemaChanged(String serviceName, String version) {
        if (debug.messageEnabled()) {
            debug.message("AuthenticatedSharedAgentsCondition.schemaChanged..");
        }
        clearCache();
    }

    // Returns the attributes from the agent's profile.
    private Map getAttributes(ServiceConfig svcConfig, String agentName)
        throws IdRepoException, SSOException, PolicyException {

        if (debug.messageEnabled()) {
            debug.message("AuthenticatedSharedAgentsCondition."+
                "getAttributes() called:AgentName- " + agentName);
        }
        Map answer = new HashMap(2);
        try {
            // Get the agent's config and then it's attributes.
            ServiceConfig aCfg = svcConfig.getSubConfig(agentName);
            if (aCfg != null) {
                answer = aCfg.getAttributesForRead();
            }
        } catch (SMSException sme) {
            debug.error("AuthenticatedSharedAgentsCondition.getAttributes(): "
                + "Error occurred while getting " + agentName, sme);
            throw new PolicyException(sme.getMessage());
        }
        return (answer);
    }

    // Returns the organization configuration of the 'default' group
    // from AgentService.
    private ServiceConfig getOrgConfig(SSOToken token, String realmName) {

        if (debug.messageEnabled()) {
            debug.message("AuthenticatedSharedAgentsCondition."+
                "getOrgConfig() called. ");
        }
        ServiceConfig orgConfigCache = null;
        try {
            // Check in cache first
            if ((realmCache != null) && (!realmCache.isEmpty()) &&
                (realmCache.containsKey(realmName))) {
                orgConfigCache = (ServiceConfig) realmCache.get(realmName);
                if (orgConfigCache.isValid()) {
                    debug.message("AuthenticatedSharedAgentsCondition.getOrgConfig() found in cache.");
                    return (orgConfigCache);
                }
            }
            if (scm == null) {
                scm = new ServiceConfigManager(token, agentserviceName,
                    version);
            }
            orgConfigCache = scm.getOrganizationConfig(realmName, null);
            // Update the realm cache.
            updateRealmCache(realmName, orgConfigCache);
        } catch (SMSException smse) {
            if (debug.warningEnabled()) {
                debug.warning("AuthenticatedSharedAgentsCondition."+
                    "getOrgConfig(): Unable to get organization config "+
                     "due to " + smse);
            }
        } catch (SSOException ssoe) {
            if (debug.warningEnabled()) {
                debug.warning("AuthenticatedSharedAgentsCondition."+
                    "getOrgConfig(): Unable to get organization config "+
                     "due to " + ssoe);
            }
        }
        return (orgConfigCache);
    }

    // Cache to store the agents configured for the "sharedAgentName".
    private static void updateCache(String sharedAgentUnivId, 
        Set agentsToRead) {
        Map nmap = new HashMap(2);
        nmap.putAll(sharedAgentsCache);
        nmap.put(sharedAgentUnivId, agentsToRead);
        sharedAgentsCache = nmap;
    }

    // Clears the cache where shared agent name and it's list of
    // agents to be read are stored.
    // Clears the cache where realm name and the service configuration
    // for the realm are stored.
    static void clearCache() {
        sharedAgentsCache = new HashMap(2);
        realmCache = new HashMap(2);
    }

    // Cache to store the realm name and the organization config.
    private static void updateRealmCache(String realmName, 
        ServiceConfig orgConfig) {
        if (debug.messageEnabled()) {
            debug.message("AuthenticatedSharedAgents.updateRealmCache: " +
                "update cache for realm " + realmName);
        }
        Map rmap = new HashMap(2);
        rmap.putAll(realmCache);
        rmap.put(realmName, orgConfig);
        realmCache = rmap;
    }

    /*
     * Returns the permission for the shared agent to read the profile
     * other agents by comparing the agent from resource from env 
     * parameter and from the token's list.
     */
    private boolean getPermission(Set agentsFromEnv, Set agentsToRead) { 
       
        boolean allowed = false;

        if (agentsFromEnv != null && !agentsFromEnv.isEmpty()) {
            Set envValues = new CaseInsensitiveHashSet(agentsFromEnv);
            if (agentsToRead != null && !agentsToRead.isEmpty()) {
                for (Iterator itr = agentsToRead.iterator();itr.hasNext();) {
                    String avName = (String) itr.next();
                    if ((envValues != null) && (envValues.contains(avName))) {
                        allowed = true;
                        if (debug.messageEnabled()) {
                            debug.message("AuthenticatedSharedAgentsCondition."
                                + "getPermission(): returning true.");
                        }
                        break;
                    }
                }
            }
        }
        if (!allowed) {
            if (debug.messageEnabled()) {
                debug.message("AuthenticatedSharedAgentsCondition."
                    + "getPermission(): returning false.");
            }
        }
        return (allowed);
    }

    /* Splits the given resource name
     * @param res the resource name to be split
     * @return an array of (String) split resource names
     */
    public String[] split(String res) {
        StringTokenizer st = new StringTokenizer(res, delimiter);
        int n = st.countTokens();
        String[] retVal = new String[n];
        for (int i = 0; i < n; i++) {
            retVal[n-i-1] = st.nextToken();
        }
        return retVal;
    }
}
