/*
 * Copyright 2015 the original author or authors.
 *
 * 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 org.springframework.statemachine.security;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.messaging.Message;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.access.vote.AbstractAccessDecisionManager;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.ConsensusBased;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.access.vote.UnanimousBased;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.support.StateMachineInterceptor;
import org.springframework.statemachine.support.StateMachineInterceptorAdapter;
import org.springframework.statemachine.transition.Transition;
import org.springframework.util.StringUtils;

/**
 * {@link StateMachineInterceptor} which can be registered into a {@link StateMachine}
 * order to intercept a various security related checks.
 *
 * @author Janne Valkealahti
 *
 * @param <S> the type of state
 * @param <E> the type of event
 */
public class StateMachineSecurityInterceptor<S, E> extends StateMachineInterceptorAdapter<S, E> {

	private AccessDecisionManager transitionAccessDecisionManager;
	private AccessDecisionManager eventAccessDecisionManager;
	private final ExpressionParser expressionParser = new SpelExpressionParser(new SpelParserConfiguration(SpelCompilerMode.OFF, null));
	private SecurityRule eventSecurityRule;

	/**
	 * Instantiates a new state machine security interceptor.
	 */
	public StateMachineSecurityInterceptor() {
		this(null, null);
	}

	/**
	 * Instantiates a new state machine security interceptor with
	 * a custom {@link AccessDecisionManager} for both transitions
	 * and events.
	 *
	 * @param transitionAccessDecisionManager the transition access decision manager
	 * @param eventAccessDecisionManager the event access decision manager
	 */
	public StateMachineSecurityInterceptor(AccessDecisionManager transitionAccessDecisionManager, AccessDecisionManager eventAccessDecisionManager) {
		this(transitionAccessDecisionManager, eventAccessDecisionManager, null);
	}

	/**
	 * Instantiates a new state machine security interceptor with
	 * a custom {@link AccessDecisionManager} for both transitions
	 * and events and a {@link SecurityRule} for events;
	 *
	 * @param transitionAccessDecisionManager the transition access decision manager
	 * @param eventAccessDecisionManager the event access decision manager
	 * @param eventSecurityRule the event security rule
	 */
	public StateMachineSecurityInterceptor(AccessDecisionManager transitionAccessDecisionManager,
			AccessDecisionManager eventAccessDecisionManager, SecurityRule eventSecurityRule) {
		this.transitionAccessDecisionManager = transitionAccessDecisionManager;
		this.eventAccessDecisionManager = eventAccessDecisionManager;
		this.eventSecurityRule = eventSecurityRule;
	}

	@Override
	public Message<E> preEvent(Message<E> message, StateMachine<S, E> stateMachine) {
		if (eventSecurityRule != null) {
			decide(eventSecurityRule, message);
		}
		return super.preEvent(message, stateMachine);
	}

	@Override
	public StateContext<S, E> preTransition(StateContext<S, E> stateContext) {
		Transition<S, E> transition = stateContext.getTransition();
		SecurityRule rule = transition.getSecurityRule();
		if (rule != null) {
			decide(rule, transition);
		}
		return super.preTransition(stateContext);
	}

	/**
	 * Sets the event access decision manager.
	 *
	 * @param eventAccessDecisionManager the new event access decision manager
	 */
	public void setEventAccessDecisionManager(AccessDecisionManager eventAccessDecisionManager) {
		this.eventAccessDecisionManager = eventAccessDecisionManager;
	}

	/**
	 * Sets the transition access decision manager.
	 *
	 * @param transitionAccessDecisionManager the new transition access decision manager
	 */
	public void setTransitionAccessDecisionManager(AccessDecisionManager transitionAccessDecisionManager) {
		this.transitionAccessDecisionManager = transitionAccessDecisionManager;
	}

	/**
	 * Sets the event security rule.
	 *
	 * @param eventSecurityRule the new event security rule
	 */
	public void setEventSecurityRule(SecurityRule eventSecurityRule) {
		this.eventSecurityRule = eventSecurityRule;
	}

	private void decide(SecurityRule rule, Message<E> object) {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		Collection<ConfigAttribute> configAttributes = getEentConfigAttributes(rule);
		if (eventAccessDecisionManager != null) {
			decide(eventAccessDecisionManager, authentication, object, configAttributes);
		} else {
			decide(createDefaultEventManager(rule), authentication, object, configAttributes);
		}
	}

	private void decide(SecurityRule rule, Transition<S, E> object) {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		Collection<ConfigAttribute> configAttributes = getTransitionConfigAttributes(rule);
		if (transitionAccessDecisionManager != null) {
			decide(transitionAccessDecisionManager, authentication, object, configAttributes);
		} else {
			decide(createDefaultTransitionManager(rule), authentication, object, configAttributes);
		}
	}

	private Collection<ConfigAttribute> getTransitionConfigAttributes(SecurityRule rule) {
		List<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
		if (rule.getAttributes() != null) {
			for (String attribute : rule.getAttributes()) {
				configAttributes.add(new SecurityConfig(attribute));
			}
		}
		if (StringUtils.hasText(rule.getExpression())) {
			configAttributes.add(new TransitionExpressionConfigAttribute(expressionParser.parseExpression(rule.getExpression())));
		}
		return configAttributes;
	}

	private Collection<ConfigAttribute> getEentConfigAttributes(SecurityRule rule) {
		List<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
		if (rule.getAttributes() != null) {
			for (String attribute : rule.getAttributes()) {
				configAttributes.add(new SecurityConfig(attribute));
			}
		}
		if (StringUtils.hasText(rule.getExpression())) {
			configAttributes.add(new EventExpressionConfigAttribute(expressionParser.parseExpression(rule.getExpression())));
		}
		return configAttributes;
	}

	private void decide(AccessDecisionManager manager, Authentication authentication, Transition<S, E> object,
			Collection<ConfigAttribute> configAttributes) {
		if (manager.supports(object.getClass())) {
			manager.decide(authentication, object, configAttributes);
		}
	}

	private void decide(AccessDecisionManager manager, Authentication authentication, Message<E> object,
			Collection<ConfigAttribute> configAttributes) {
		if (manager.supports(object.getClass())) {
			manager.decide(authentication, object, configAttributes);
		}
	}

	private AbstractAccessDecisionManager createDefaultTransitionManager(SecurityRule rule) {
		List<AccessDecisionVoter<? extends Object>> voters = new ArrayList<AccessDecisionVoter<? extends Object>>();
		voters.add(new TransitionExpressionVoter());
		voters.add(new TransitionVoter<Object, Object>());
		voters.add(new RoleVoter());
		if (rule.getComparisonType() == SecurityRule.ComparisonType.ANY) {
			return new AffirmativeBased(voters);
		} else if (rule.getComparisonType() == SecurityRule.ComparisonType.ALL) {
			return new UnanimousBased(voters);
		} else if (rule.getComparisonType() == SecurityRule.ComparisonType.MAJORITY) {
			return new ConsensusBased(voters);
		} else {
			throw new IllegalStateException("Unknown SecurityRule match type: " + rule.getComparisonType());
		}
	}

	private AbstractAccessDecisionManager createDefaultEventManager(SecurityRule rule) {
		List<AccessDecisionVoter<? extends Object>> voters = new ArrayList<AccessDecisionVoter<? extends Object>>();
		voters.add(new EventExpressionVoter<Object>());
		voters.add(new EventVoter<Object>());
		voters.add(new RoleVoter());
		if (rule.getComparisonType() == SecurityRule.ComparisonType.ANY) {
			return new AffirmativeBased(voters);
		} else if (rule.getComparisonType() == SecurityRule.ComparisonType.ALL) {
			return new UnanimousBased(voters);
		} else if (rule.getComparisonType() == SecurityRule.ComparisonType.MAJORITY) {
			return new ConsensusBased(voters);
		} else {
			throw new IllegalStateException("Unknown SecurityRule match type: " + rule.getComparisonType());
		}
	}

	@Override
	public String toString() {
		return "StateMachineSecurityInterceptor [transitionAccessDecisionManager=" + transitionAccessDecisionManager
				+ ", eventAccessDecisionManager=" + eventAccessDecisionManager + ", eventSecurityRule=" + eventSecurityRule + "]";
	}

}
