/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package it.mice.voila.runtime.springmvc.security;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;

import it.mice.voila.runtime.security.AbstractVoter;
import it.mice.voila.runtime.security.OperationConfigAttribute;
import it.mice.voila.runtime.security.OperationPerProfileConfigAttribute;
import it.mice.voila.runtime.security.RenderSecurityBean;
import it.mice.voila.runtime.security.SecurityBean;
import it.mice.voila.runtime.security.UserProfileGrantedAuthorityImpl;
import it.mice.voila.runtime.springmvc.flashmap.FlashMap;
import it.mice.voila.runtime.springmvc.form.JfFormBean;
import it.mice.voila.runtime.springmvc.interceptors.AdvancedSecurityCheckInterceptor;
import it.mice.voila.runtime.springmvc.interceptors.OperationAuditInterceptor;
import it.mice.voila.runtime.springmvc.util.SpringMvcUtil;
import it.mice.voila.runtime.util.UserMessage;
import it.mice.voila.runtime.util.UserMessages;



/**
 * <p>Votes if any {@link org.acegisecurity.ConfigAttribute#getAttribute()} starts with a prefix indicating that it is a role. The
 * default prefix string is <Code>ROLE_</code>, but this may be overriden to any value. It may also be set to empty,
 * which means that essentially any attribute will be voted on. As described further below, the effect of an empty
 * prefix may not be quite desireable.</p>
 * <p>Abstains from voting if no configuration attribute commences with the  role prefix. Votes to grant access if
 * there is an exact matching {@link org.acegisecurity.GrantedAuthority} to a <code>ConfigAttribute</code> starting
 * with the role prefix. Votes to deny access if there is no exact matching <code>GrantedAuthority</code>  to a
 * <code>ConfigAttribute</code> starting with the role prefix.</p>
 * <p>An empty role prefix means that the voter will vote for every ConfigAttribute. When there are different
 * categories of ConfigAttributes used, this will not be optimal since the voter will be voting for attributes which
 * do not represent roles. However, this option may be of some use when using preexisting role names without a prefix,
 * and no ability exists to prefix them with a role prefix on reading them in, such as provided for example in  {@link
 * org.acegisecurity.userdetails.jdbc.JdbcDaoImpl}.</p>
 * <p>All comparisons and prefixes are case sensitive.</p>
 *
 * @author Ben Alex
 * @author colin sampaleanu
 * @version $Id: FilteredRoleAndOperationVoter.java,v 1.1 2007/02/27 00:18:05 zzy9v4 Exp $
 */
public class FilteredRoleAndOperationVoter extends AbstractVoter implements AccessDecisionVoter {
    private static final Log logger = LogFactory.getLog(FilteredRoleAndOperationVoter.class);
    //~ Instance fields ================================================================================================
    private String alwaysEnabledOperationCode = "Y";
    private String disabledOperationCode = "N";

    //~ Methods ========================================================================================================

    public String getAlwaysEnabledOperationCode() {
        return alwaysEnabledOperationCode;
    }

    public void setAlwaysEnabledOperationCode(String alwaysEnabledOperationCode) {
        this.alwaysEnabledOperationCode = alwaysEnabledOperationCode;
    }

    public String getDisabledOperationCode() {
        return disabledOperationCode;
    }

    public void setDisabledOperationCode(String disabledOperationCode) {
        this.disabledOperationCode = disabledOperationCode;
    }

    public boolean supports(ConfigAttribute attribute) {
        if ((attribute != null) && attribute instanceof OperationConfigAttribute) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * This implementation supports any type of class, because it does not query the presented secure object.
     *
     * @param clazz the secure object
     * @return always <code>true</code>
     */
    public boolean supports(Class clazz) {
        return true;
    }

    public int vote(Authentication authentication, Object object, Collection config) {
        int result = ACCESS_ABSTAIN;
        if (getAuthenticationTrustResolver().isAnonymous(authentication)) {
        	return result;
        }
        UserMessages msgs = new UserMessages();
        //for each config attribute defined for the current operation...
        Iterator<ConfigAttribute> iter = config.iterator();
        while (iter.hasNext()) {
            ConfigAttribute attribute = iter.next();
            //if the attribute is supported (generally if start with ROLE_ prefix...
            if (this.supports(attribute)) {
                OperationConfigAttribute operationConfigAttribute = (OperationConfigAttribute) attribute;
                if (object instanceof FilterInvocation) {
                    SpringMvcUtil.getRequest().setAttribute(OperationAuditInterceptor.OPERATION_CONFIG_ATTRIBUTE, operationConfigAttribute);
                }

                if (this.alwaysEnabledOperationCode.equals(operationConfigAttribute.getAllowDeniedFlag())) {
                    return ACCESS_GRANTED;
                }
                if (this.disabledOperationCode.equals(operationConfigAttribute.getAllowDeniedFlag())) {
                	//msgs.add(new UserMessage("authorization.disabledOperation"));
                    return finalizeMessageAndReturn(ACCESS_DENIED, msgs);
                }

                //assume no match found
                result = ACCESS_DENIED;
                //ActionMessages actionMessages = new ActionMessages();
	                UserProfileGrantedAuthorityImpl[] filteredGrantedAuthorities;
					try {
						filteredGrantedAuthorities = filterAuthenticationRoles(authentication);
					} catch (Exception e) {
						return result;
					}
	
	                // Attempt to find a matching granted authority
	                //Cycle thru the user profiles...
	                for (int i = 0; i < filteredGrantedAuthorities.length; i++) {
	                    //Cycle thru the current operation enabled profiles...
	                    for (int j = 0; j < operationConfigAttribute.getProfiles().length; j++) {
	                        //Check if two profiles match.
	                        if (operationConfigAttribute.getProfiles()[j].getProfileName().equals(filteredGrantedAuthorities[i].getJfAuthority().getProfileName())) {
	                            //match found !!!
	                            //Now check against operation rules...
	                            if (checkForOperation(
	                                    filteredGrantedAuthorities[i],
	                                    authentication, object,
	                                    operationConfigAttribute,
	                                    operationConfigAttribute.getProfiles()[j],
	                                    msgs)) {
	                                //Access granted !!!
	                                return ACCESS_GRANTED;
	                            }
	                        }
	                    }
	                }
	                //addActionMessageToExecutionContext(object, actionMessages);
            }
        }
        return finalizeMessageAndReturn(result, msgs);
    }

    private int finalizeMessageAndReturn(int result, UserMessages msgs) {
    	if (msgs.size() > 0) {
        	FlashMap.getMessages().addUserMessages(msgs);
    	}
		return result;
	}

	private boolean checkForOperation(
            UserProfileGrantedAuthorityImpl userProfileGrantedAuthorityImpl,
            Authentication authentication,
            Object object,
            OperationConfigAttribute operationConfigAttribute,
            OperationPerProfileConfigAttribute operationPerProfileConfigAttribute,
            UserMessages actionMessages) {

		boolean result = false;
        //Determine authorization rule to be used by looking in the chain...
        AuthorizationRule authorizationRule = determineAuthorizationRule(operationConfigAttribute, operationPerProfileConfigAttribute, object);
        if (authorizationRule != null) {
        	if (object instanceof FilterInvocation && authorizationRule.authorizationRuleString != null) {
        		//La richiesta di autorizzazione è sata fatta da spring security, ma in questo preciso istante
        		//il form non è stato completamente popolato, pertanto viene bypassato tale controllato, ma viene
        		//ricordato all'interceptor di spring di intervenire uccessivamente in tal senso.
        		SpringMvcUtil.getRequest().setAttribute(AdvancedSecurityCheckInterceptor.PERFORM_AUTH_RULE_SECURITY_CHECK, Boolean.TRUE);
        		result = true;
        	} else {
    			result = checkForAuthRule(authorizationRule, object,
    					userProfileGrantedAuthorityImpl, authentication,
    					operationConfigAttribute,
    					operationPerProfileConfigAttribute, actionMessages);
        	}
        } else {
            //No authorization rule found, access granted.
            if (logger.isDebugEnabled())
                logger.debug("Access granted without authorization rule evaluation!");
            result = true;
        }
        
        return result;
    }

	public boolean checkForAuthRule(
			AuthorizationRule authorizationRule,
			Object object,
			UserProfileGrantedAuthorityImpl userProfileGrantedAuthorityImpl,
			Authentication authentication,
			OperationConfigAttribute operationConfigAttribute,
			OperationPerProfileConfigAttribute operationPerProfileConfigAttribute,
			UserMessages actionMessages) {

		boolean result = false;
        //Authorization rule found, proceed...
    	Object ctx = extractContext(object);
    	if (ctx == null && 
    			authorizationRule.authorizationRuleString != null && 
    			authorizationRule.authorizationRuleString.indexOf("ctx.") > -1) 
    	{
    		//This case cover situation where check authorization from menu render tag and submit button render tag is performed.
    		//In fact, in this situation, there is not an action context.
    		//In any case we ensure that the authorization rule contain reference with ctx to assume that the check
    		//need to be by-passed (to support auth check for auth rules with no run-time reference formulas).
    		result = true;
    	} else {
            //build properties chain
            StringBuffer propertySetters = preparePropertySetters(userProfileGrantedAuthorityImpl, authentication, operationConfigAttribute, operationPerProfileConfigAttribute);

            if (logger.isDebugEnabled())
                logger.debug("Evaluating elementary operation execution using velocity engine");

            //evaluate authorization rule by using Velocity...
            String velocityResult = evaluateVelocityTemplate(ctx, authorizationRule.authorizationRuleString, propertySetters, authentication, userProfileGrantedAuthorityImpl, true);
            if (velocityResult == null || velocityResult.trim().length() == 0) {
                //Velocity don't give a feedback, abstaining...
                if (logger.isDebugEnabled())
                    logger.debug("Abstaining for " + object + ". Velocity content for Authorization Rule field template:" + authorizationRule + " on profile " + userProfileGrantedAuthorityImpl);
                result = true;
            } else {
                //Velocity give a feedback, check it...
                if (velocityResult.equalsIgnoreCase("true")) {
                    //Velocity said TRUE !!!
                    if (logger.isDebugEnabled())
                        logger.debug("Access granted after authorization rule evaluation!");
                    result = true;
                } else {
                	if (!(object instanceof RenderSecurityBean)) {
                        actionMessages.add(new UserMessage(authorizationRule.msgKey, authorizationRule.msgText));
                	}
                    result = false;
                }
            }
    	}
        return result;
	}

	public UserProfileGrantedAuthorityImpl[] filterAuthenticationRoles(Authentication authentication) {
    	Collection<GrantedAuthority> filteredAuth = new ArrayList<GrantedAuthority>();
        Collection<? extends GrantedAuthority> grantedAuthorities = authentication.getAuthorities();
        for (Iterator<? extends GrantedAuthority> iterator = grantedAuthorities.iterator(); iterator
				.hasNext();) {
			GrantedAuthority grantedAuthority = (GrantedAuthority) iterator.next();
            if (checkProfileAssociation(authentication, (UserProfileGrantedAuthorityImpl) grantedAuthority)) {
                filteredAuth.add(grantedAuthority);
            } else {
                if (logger.isInfoEnabled())
                    logger.info("User profile filtered! Details:" + grantedAuthority);
//                addActionMessage(actionMessages,
//                        "authorization.filteredProfile",
//                        null,
//                        new String[]{
//                                ((UserProfileGrantedAuthorityImpl) grantedAuthorities[i]).getProfileName()
//                        }
//                );
            }
        }
        if (filteredAuth.size() > 0) {
            return (UserProfileGrantedAuthorityImpl[]) filteredAuth.toArray(new UserProfileGrantedAuthorityImpl[]{});
        }
        return new UserProfileGrantedAuthorityImpl[]{};
    }

    private boolean checkProfileAssociation(Authentication authentication, UserProfileGrantedAuthorityImpl userProfileGrantedAuthority) {
        //decide wich profile association rule must be processed
        String profileAssociationRule = getQualifiedProfileAssociationRule(userProfileGrantedAuthority);
        if (profileAssociationRule == null) {
            if (logger.isTraceEnabled())
                logger.trace("No profile association rules found at any level for: " + userProfileGrantedAuthority + ", no check to perform.");
            return true;
        }
        //Profile association rule found, execute it to verify if the profile is supported.
        StringBuffer propertySetters = preparePropertySetters(userProfileGrantedAuthority, authentication, null, null);
        if (logger.isDebugEnabled())
            logger.debug("Evaluating profile association using velocity engine for: " + userProfileGrantedAuthority);

        JfFormBean form = SpringMvcUtil.getFormFromSession();
		if (form == null && profileAssociationRule.indexOf("ctx.form") > -1) {
			// This case cover situation where check authorization from menu
			// render tag and submit button render tag is performed.
			// In fact, in this situation, there is not an action context.
			// In any case we ensure that the authorization rule contain
			// reference with ctx to assume that the check
			// need to be by-passed (to support auth check for auth rules with
			// no run-time reference formulas).
	        if (logger.isDebugEnabled())
	            logger.debug("profileAssociationRule=" + profileAssociationRule + " contain 'ctx.form' expression but the current form is null. Evaluating always to 'true'!");
	        
			return true;
		}
		String prefixFormName = "=";
		if (form != null) {
			prefixFormName = form.getClass().getSimpleName() + "=";
		}
		String[] assocRuleParts = profileAssociationRule.split("\\n");
		String finalProfileAssociationRule = null;
    	for (String part : assocRuleParts) {
    		if (part.startsWith(prefixFormName)) {
    			finalProfileAssociationRule = part.substring(prefixFormName.length());
    			break;
    		}
    	}
    	if (finalProfileAssociationRule == null) {
    		return true;
    	}
        Map<String, Object> map = new HashMap<String, Object>();
		map.put("form", form);
        String velocityResult = evaluateVelocityTemplate(map, finalProfileAssociationRule, propertySetters, authentication, userProfileGrantedAuthority, false);
        if (velocityResult == null || velocityResult.trim().length() == 0) {
            return true;
        } else {
            return velocityResult.equalsIgnoreCase("true");
        }
    }

    private String getQualifiedProfileAssociationRule(UserProfileGrantedAuthorityImpl userProfileGrantedAuthority) {
        //If user/profile association level rule was found use this
        if (userProfileGrantedAuthority.getProfilePerUserAssociationRule() != null) {
            if (logger.isDebugEnabled())
                logger.debug("Check profile association using custom Profile Per User Association Rule:" + userProfileGrantedAuthority.getProfilePerUserAssociationRule());
            return userProfileGrantedAuthority.getProfilePerUserAssociationRule();
        }
        //If profile level rule was found use this
        if (userProfileGrantedAuthority.getProfileAssociationRule() != null) {
            if (logger.isDebugEnabled())
                logger.debug("Check profile association using custom Profile Association Rule:" + userProfileGrantedAuthority.getProfileAssociationRule());
            return userProfileGrantedAuthority.getProfileAssociationRule();
        }
        //If application level rule was found use this
        if (getApplicationHolder() != null && getApplicationHolder().getProfileAssociationRule() != null) {
            if (logger.isDebugEnabled())
                logger.debug("Check profile association using default Profile Association Rule at application level:" + getApplicationHolder().getProfileAssociationRule());
            return getApplicationHolder().getProfileAssociationRule();
        }

        //No profile association rules found at any level, no check to perform.
        return null;
    }

    private AuthorizationRule determineAuthorizationRule(OperationConfigAttribute operationConfigAttribute, OperationPerProfileConfigAttribute operationPerProfileConfigAttribute, Object object) {
        String authorizationRule = null;
        String msgKey = null;
        String msgText = null;
        if (object instanceof RenderSecurityBean) {
            if (operationPerProfileConfigAttribute.getAuthorizationRuleRender() != null) {
                authorizationRule = operationPerProfileConfigAttribute.getAuthorizationRuleRender();
                if (logger.isDebugEnabled())
                    logger.debug("Vote using custom operation per profile authorization rule:" + authorizationRule + " on security config:" + operationConfigAttribute);
            } else if (operationConfigAttribute.getAuthorizationRuleRender() != null) {
                authorizationRule = operationConfigAttribute.getAuthorizationRuleRender();
                if (logger.isDebugEnabled())
                    logger.debug("Vote using custom elementary operation authorization rule:" + authorizationRule + " on security config:" + operationConfigAttribute);
            } else if (getApplicationHolder() != null) {
                authorizationRule = getApplicationHolder().getAuthorizationRuleRender();
                if (logger.isDebugEnabled())
                    logger.debug("Vote using default application level authorization rule:" + authorizationRule + " on security config:" + operationConfigAttribute);
            }
        } else {
            if (operationPerProfileConfigAttribute.getAuthorizationRule() != null) {
                authorizationRule = operationPerProfileConfigAttribute.getAuthorizationRule();
                if (logger.isDebugEnabled())
                    logger.debug("Vote using custom operation per profile authorization rule:" + authorizationRule + " on security config:" + operationConfigAttribute);
            } else if (operationConfigAttribute.getAuthorizationRule() != null) {
                authorizationRule = operationConfigAttribute.getAuthorizationRule();
                if (logger.isDebugEnabled())
                    logger.debug("Vote using custom elementary operation authorization rule:" + authorizationRule + " on security config:" + operationConfigAttribute);
            } else if (getApplicationHolder() != null) {
                authorizationRule = getApplicationHolder().getAuthorizationRule();
                if (logger.isDebugEnabled())
                    logger.debug("Vote using default application level authorization rule:" + authorizationRule + " on security config:" + operationConfigAttribute);
            }
        }

        if (operationPerProfileConfigAttribute.getMsgKey() != null || operationPerProfileConfigAttribute.getMsgText() != null) {
            msgKey = operationPerProfileConfigAttribute.getMsgKey();
            msgText = operationPerProfileConfigAttribute.getMsgText();
        } else if (operationConfigAttribute.getMsgKey() != null || operationConfigAttribute.getMsgText() != null) {
            msgKey = operationConfigAttribute.getMsgKey();
            msgText = operationConfigAttribute.getMsgText();
        }
        return new AuthorizationRule(authorizationRule, msgKey, msgText);
    }

    private class AuthorizationRule {
        private String authorizationRuleString;
        private String msgKey;
        private String msgText;
        public AuthorizationRule(String authorizationRuleString, String msgKey, String msgText) {
            this.authorizationRuleString = authorizationRuleString;
            this.msgKey = msgKey;
            this.msgText = msgText;
        }
    }

    protected Object extractContext(Object object) {
    	if (object instanceof SkyGenSecurityContext) {
            return object;
    	}
    	if (object instanceof SecurityBean) {
            return new SkyGenSecurityContext(((SecurityBean)object).getContext());
    	}
        SkyGenSecurityContext ctx = new SkyGenSecurityContext();
    	return ctx;
    }
}
