package org.apereo.cas.web.flow; import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.services.MultifactorAuthenticationProvider; import org.apereo.cas.web.support.WebUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.binding.convert.ConversionExecutor; import org.springframework.binding.convert.service.RuntimeBindingConversionExecutor; import org.springframework.binding.expression.Expression; import org.springframework.binding.expression.ExpressionParser; import org.springframework.binding.expression.ParserContext; import org.springframework.binding.expression.spel.SpringELExpressionParser; import org.springframework.binding.expression.support.FluentParserContext; import org.springframework.binding.expression.support.LiteralExpression; import org.springframework.binding.mapping.Mapper; import org.springframework.binding.mapping.impl.DefaultMapper; import org.springframework.binding.mapping.impl.DefaultMapping; import org.springframework.context.ApplicationContext; import org.springframework.context.expression.BeanExpressionContextAccessor; import org.springframework.context.expression.EnvironmentAccessor; import org.springframework.context.expression.MapAccessor; import org.springframework.expression.spel.SpelParserConfiguration; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.ReflectivePropertyAccessor; import org.springframework.util.ReflectionUtils; import org.springframework.webflow.action.EvaluateAction; import org.springframework.webflow.action.ExternalRedirectAction; import org.springframework.webflow.action.ViewFactoryActionAdapter; import org.springframework.webflow.config.FlowDefinitionRegistryBuilder; import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; import org.springframework.webflow.engine.ActionState; import org.springframework.webflow.engine.DecisionState; import org.springframework.webflow.engine.EndState; import org.springframework.webflow.engine.Flow; import org.springframework.webflow.engine.FlowVariable; import org.springframework.webflow.engine.SubflowAttributeMapper; import org.springframework.webflow.engine.SubflowState; import org.springframework.webflow.engine.Transition; import org.springframework.webflow.engine.TransitionCriteria; import org.springframework.webflow.engine.TransitionableState; import org.springframework.webflow.engine.ViewState; import org.springframework.webflow.engine.WildcardTransitionCriteria; import org.springframework.webflow.engine.builder.BinderConfiguration; import org.springframework.webflow.engine.builder.support.FlowBuilderServices; import org.springframework.webflow.engine.support.ActionExecutingViewFactory; import org.springframework.webflow.engine.support.BeanFactoryVariableValueFactory; import org.springframework.webflow.engine.support.DefaultTargetStateResolver; import org.springframework.webflow.engine.support.DefaultTransitionCriteria; import org.springframework.webflow.engine.support.GenericSubflowAttributeMapper; import org.springframework.webflow.execution.Action; import org.springframework.webflow.execution.ViewFactory; import org.springframework.webflow.expression.spel.ActionPropertyAccessor; import org.springframework.webflow.expression.spel.BeanFactoryPropertyAccessor; import org.springframework.webflow.expression.spel.FlowVariablePropertyAccessor; import org.springframework.webflow.expression.spel.MapAdaptablePropertyAccessor; import org.springframework.webflow.expression.spel.MessageSourcePropertyAccessor; import org.springframework.webflow.expression.spel.ScopeSearchingPropertyAccessor; import javax.annotation.PostConstruct; import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; /** * The {@link AbstractCasWebflowConfigurer} is responsible for * providing an entry point into the CAS webflow. * * @author Misagh Moayyed * @since 4.2 */ public abstract class AbstractCasWebflowConfigurer implements CasWebflowConfigurer { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractCasWebflowConfigurer.class); /** * The logout flow definition registry. */ protected FlowDefinitionRegistry logoutFlowDefinitionRegistry; /** * The Login flow definition registry. */ protected final FlowDefinitionRegistry loginFlowDefinitionRegistry; /** * Application context. */ @Autowired protected ApplicationContext applicationContext; /** * CAS Properties. */ @Autowired protected CasConfigurationProperties casProperties; /** * Flow builder services. */ protected final FlowBuilderServices flowBuilderServices; public AbstractCasWebflowConfigurer(final FlowBuilderServices flowBuilderServices, final FlowDefinitionRegistry loginFlowDefinitionRegistry) { this.flowBuilderServices = flowBuilderServices; this.loginFlowDefinitionRegistry = loginFlowDefinitionRegistry; } @PostConstruct @Override public void initialize() { try { LOGGER.debug("Initializing CAS webflow configuration..."); if (casProperties.getWebflow().isAutoconfigure()) { doInitialize(); } else { LOGGER.warn("Webflow auto-configuration is disabled. CAS will not modify the webflow via [{}]", getClass().getName()); } } catch (final Exception e) { LOGGER.error(e.getMessage(), e); } } /** * Handle the initialization of the webflow. * * @throws Exception the exception */ protected abstract void doInitialize() throws Exception; @Override public Flow buildFlow(final String location, final String id) { final FlowDefinitionRegistryBuilder builder = new FlowDefinitionRegistryBuilder(this.applicationContext, this.flowBuilderServices); builder.setParent(this.loginFlowDefinitionRegistry); builder.addFlowLocation(location, id); final FlowDefinitionRegistry registry = builder.build(); return (Flow) registry.getFlowDefinition(id); } @Override public Flow getLoginFlow() { if (this.loginFlowDefinitionRegistry == null) { LOGGER.error("Login flow registry is not configured correctly."); return null; } final boolean found = Arrays.stream(this.loginFlowDefinitionRegistry.getFlowDefinitionIds()).anyMatch(f -> f.equals(FLOW_ID_LOGIN)); if (found) { return (Flow) this.loginFlowDefinitionRegistry.getFlowDefinition(FLOW_ID_LOGIN); } LOGGER.error("Could not find flow definition [{}]. Available flow definition ids are [{}]", FLOW_ID_LOGIN, this.loginFlowDefinitionRegistry.getFlowDefinitionIds()); return null; } @Override public Flow getLogoutFlow() { if (this.logoutFlowDefinitionRegistry == null) { LOGGER.error("Logout flow registry is not configured correctly."); return null; } return (Flow) this.logoutFlowDefinitionRegistry.getFlowDefinition(FLOW_ID_LOGOUT); } @Override public TransitionableState getStartState(final Flow flow) { return TransitionableState.class.cast(flow.getStartState()); } @Override public ActionState createActionState(final Flow flow, final String name, final Action... actions) { if (containsFlowState(flow, name)) { LOGGER.debug("Flow [{}] already contains a definition for state id [{}]", flow.getId(), name); return (ActionState) flow.getTransitionableState(name); } final ActionState actionState = new ActionState(flow, name); LOGGER.debug("Created action state [{}]", actionState.getId()); actionState.getActionList().addAll(actions); LOGGER.debug("Added action to the action state [{}] list of actions: [{}]", actionState.getId(), actionState.getActionList()); return actionState; } @Override public DecisionState createDecisionState(final Flow flow, final String id, final String testExpression, final String thenStateId, final String elseStateId) { if (containsFlowState(flow, id)) { LOGGER.debug("Flow [{}] already contains a definition for state id [{}]", flow.getId(), id); return (DecisionState) flow.getTransitionableState(id); } final DecisionState decisionState = new DecisionState(flow, id); final Expression expression = createExpression(testExpression, Boolean.class); final Transition thenTransition = createTransition(expression, thenStateId); decisionState.getTransitionSet().add(thenTransition); final Transition elseTransition = createTransition("*", elseStateId); decisionState.getTransitionSet().add(elseTransition); return decisionState; } @Override public void setStartState(final Flow flow, final String state) { flow.setStartState(state); final TransitionableState startState = getStartState(flow); LOGGER.debug("Start state is now set to [{}]", startState.getId()); } @Override public void setStartState(final Flow flow, final TransitionableState state) { setStartState(flow, state.getId()); } @Override public EvaluateAction createEvaluateAction(final String expression) { if (this.flowBuilderServices == null) { LOGGER.error("Flow builder services is not configured correctly."); return null; } final ParserContext ctx = new FluentParserContext(); final Expression action = this.flowBuilderServices.getExpressionParser().parseExpression(expression, ctx); final EvaluateAction newAction = new EvaluateAction(action, null); LOGGER.debug("Created evaluate action for expression [{}]", action.getExpressionString()); return newAction; } /** * Add a default transition to a given state. * * @param state the state to include the default transition * @param targetState the id of the destination state to which the flow should transfer */ protected void createStateDefaultTransition(final TransitionableState state, final String targetState) { if (state == null) { LOGGER.debug("Cannot add default transition of [{}] to the given state is null and cannot be found in the flow.", targetState); return; } final Transition transition = createTransition(targetState); state.getTransitionSet().add(transition); } /** * Create transition for state transition. * * @param state the state * @param criteriaOutcome the criteria outcome * @param targetState the target state * @return the transition */ protected Transition createTransitionForState(final TransitionableState state, final String criteriaOutcome, final String targetState) { return createTransitionForState(state, criteriaOutcome, targetState, false); } /** * Add transition to action state. * * @param state the action state * @param criteriaOutcome the criteria outcome * @param targetState the target state * @param removeExisting the remove existing * @return the transition */ protected Transition createTransitionForState(final TransitionableState state, final String criteriaOutcome, final String targetState, final boolean removeExisting) { try { if (removeExisting) { final Transition success = (Transition) state.getTransition(criteriaOutcome); if (success != null) { state.getTransitionSet().remove(success); } } final Transition transition = createTransition(criteriaOutcome, targetState); state.getTransitionSet().add(transition); LOGGER.debug("Added transition [{}] to the state [{}]", transition.getId(), state.getId()); return transition; } catch (final Exception e) { LOGGER.error(e.getMessage(), e); } return null; } @Override public Transition createTransition(final String criteriaOutcome, final String targetState) { return createTransition(new LiteralExpression(criteriaOutcome), targetState); } @Override public Transition createTransition(final String criteriaOutcome, final TransitionableState targetState) { return createTransition(new LiteralExpression(criteriaOutcome), targetState.getId()); } @Override public Transition createTransition(final Expression criteriaOutcomeExpression, final String targetState) { final TransitionCriteria criteria; if (criteriaOutcomeExpression.toString().equals(WildcardTransitionCriteria.WILDCARD_EVENT_ID)) { criteria = WildcardTransitionCriteria.INSTANCE; } else { criteria = new DefaultTransitionCriteria(criteriaOutcomeExpression); } final DefaultTargetStateResolver resolver = new DefaultTargetStateResolver(targetState); final Transition t = new Transition(criteria, resolver); return t; } /** * Create expression expression. * * @param expression the expression * @param expectedType the expected type * @return the expression */ protected Expression createExpression(final String expression, final Class expectedType) { final ParserContext parserContext = new FluentParserContext() .expectResult(expectedType); return getSpringExpressionParser().parseExpression(expression, parserContext); } /** * Gets spring expression parser. * * @return the spring expression parser */ protected SpringELExpressionParser getSpringExpressionParser() { final SpelParserConfiguration configuration = new SpelParserConfiguration(); final SpelExpressionParser spelExpressionParser = new SpelExpressionParser(configuration); final SpringELExpressionParser parser = new SpringELExpressionParser(spelExpressionParser, this.flowBuilderServices.getConversionService()); parser.addPropertyAccessor(new ActionPropertyAccessor()); parser.addPropertyAccessor(new BeanFactoryPropertyAccessor()); parser.addPropertyAccessor(new FlowVariablePropertyAccessor()); parser.addPropertyAccessor(new MapAdaptablePropertyAccessor()); parser.addPropertyAccessor(new MessageSourcePropertyAccessor()); parser.addPropertyAccessor(new ScopeSearchingPropertyAccessor()); parser.addPropertyAccessor(new BeanExpressionContextAccessor()); parser.addPropertyAccessor(new MapAccessor()); parser.addPropertyAccessor(new MapAdaptablePropertyAccessor()); parser.addPropertyAccessor(new EnvironmentAccessor()); parser.addPropertyAccessor(new ReflectivePropertyAccessor()); return parser; } @Override public Transition createTransition(final String targetState) { final DefaultTargetStateResolver resolver = new DefaultTargetStateResolver(targetState); return new Transition(resolver); } @Override public EndState createEndState(final Flow flow, final String id) { return createEndState(flow, id, (ViewFactory) null); } @Override public EndState createEndState(final Flow flow, final String id, final String viewId) { return createEndState(flow, id, new LiteralExpression(viewId)); } @Override public EndState createEndState(final Flow flow, final String id, final Expression expression) { final ViewFactory viewFactory = this.flowBuilderServices.getViewFactoryCreator().createViewFactory( expression, this.flowBuilderServices.getExpressionParser(), this.flowBuilderServices.getConversionService(), null, this.flowBuilderServices.getValidator(), this.flowBuilderServices.getValidationHintResolver()); return createEndState(flow, id, viewFactory); } @Override public EndState createEndState(final Flow flow, final String id, final String viewId, final boolean redirect) { if (!redirect) { return createEndState(flow, id, viewId); } final Expression expression = createExpression(viewId, String.class); final ActionExecutingViewFactory viewFactory = new ActionExecutingViewFactory(new ExternalRedirectAction(expression)); return createEndState(flow, id, viewFactory); } @Override public EndState createEndState(final Flow flow, final String id, final ViewFactory viewFactory) { if (containsFlowState(flow, id)) { LOGGER.debug("Flow [{}] already contains a definition for state id [{}]", flow.getId(), id); return (EndState) flow.getStateInstance(id); } final EndState endState = new EndState(flow, id); if (viewFactory != null) { final Action finalResponseAction = new ViewFactoryActionAdapter(viewFactory); endState.setFinalResponseAction(finalResponseAction); LOGGER.debug("Created end state state [{}] on flow id [{}], backed by view factory [{}]", id, flow.getId(), viewFactory); } else { LOGGER.debug("Created end state state [{}] on flow id [{}]", id, flow.getId()); } return endState; } @Override public ViewState createViewState(final Flow flow, final String id, final Expression expression, final BinderConfiguration binder) { try { if (containsFlowState(flow, id)) { LOGGER.debug("Flow [{}] already contains a definition for state id [{}]", flow.getId(), id); return (ViewState) flow.getTransitionableState(id); } final ViewFactory viewFactory = this.flowBuilderServices.getViewFactoryCreator().createViewFactory( expression, this.flowBuilderServices.getExpressionParser(), this.flowBuilderServices.getConversionService(), binder, this.flowBuilderServices.getValidator(), this.flowBuilderServices.getValidationHintResolver()); final ViewState viewState = new ViewState(flow, id, viewFactory); LOGGER.debug("Added view state [{}]", viewState.getId()); return viewState; } catch (final Exception e) { LOGGER.error(e.getMessage(), e); } return null; } @Override public ViewState createViewState(final Flow flow, final String id, final String viewId) { return createViewState(flow, id, new LiteralExpression(viewId), null); } @Override public ViewState createViewState(final Flow flow, final String id, final String viewId, final BinderConfiguration binder) { return createViewState(flow, id, new LiteralExpression(viewId), binder); } @Override public SubflowState createSubflowState(final Flow flow, final String id, final String subflow, final Action entryAction) { if (containsFlowState(flow, id)) { LOGGER.debug("Flow [{}] already contains a definition for state id [{}]", flow.getId(), id); return (SubflowState) flow.getTransitionableState(id); } final SubflowState state = new SubflowState(flow, id, new BasicSubflowExpression(subflow, this.loginFlowDefinitionRegistry)); if (entryAction != null) { state.getEntryActionList().add(entryAction); } return state; } @Override public SubflowState createSubflowState(final Flow flow, final String id, final String subflow) { return createSubflowState(flow, id, subflow, null); } /** * Create mapper to subflow state. * * @param mappings the mappings * @return the mapper */ protected Mapper createMapperToSubflowState(final List<DefaultMapping> mappings) { final DefaultMapper inputMapper = new DefaultMapper(); mappings.forEach(inputMapper::addMapping); return inputMapper; } /** * Create mapping to subflow state. * * @param name the name * @param value the value * @param required the required * @param type the type * @return the default mapping */ protected DefaultMapping createMappingToSubflowState(final String name, final String value, final boolean required, final Class type) { final ExpressionParser parser = this.flowBuilderServices.getExpressionParser(); final Expression source = parser.parseExpression(value, new FluentParserContext()); final Expression target = parser.parseExpression(name, new FluentParserContext()); final DefaultMapping mapping = new DefaultMapping(source, target); mapping.setRequired(required); final ConversionExecutor typeConverter = new RuntimeBindingConversionExecutor(type, this.flowBuilderServices.getConversionService()); mapping.setTypeConverter(typeConverter); return mapping; } /** * Create subflow attribute mapper. * * @param inputMapper the input mapper * @param outputMapper the output mapper * @return the subflow attribute mapper */ protected SubflowAttributeMapper createSubflowAttributeMapper(final Mapper inputMapper, final Mapper outputMapper) { return new GenericSubflowAttributeMapper(inputMapper, outputMapper); } public void setLogoutFlowDefinitionRegistry(final FlowDefinitionRegistry logoutFlowDefinitionRegistry) { this.logoutFlowDefinitionRegistry = logoutFlowDefinitionRegistry; } /** * Contains flow state? * * @param flow the flow * @param stateId the state id * @return true if flow contains the state. */ protected boolean containsFlowState(final Flow flow, final String stateId) { if (flow == null) { LOGGER.error("Flow is not configured correctly and cannot be null."); return false; } return flow.containsState(stateId); } /** * Contains transition boolean. * * @param state the state * @param transition the transition * @return the boolean */ protected boolean containsTransition(final TransitionableState state, final String transition) { if (state == null) { LOGGER.error("State is not configured correctly and cannot be null."); return false; } return state.getTransition(transition) != null; } /** * Create flow variable flow variable. * * @param flow the flow * @param id the id * @param type the type * @return the flow variable */ protected FlowVariable createFlowVariable(final Flow flow, final String id, final Class type) { final Optional<FlowVariable> opt = Arrays.stream(flow.getVariables()).filter(v -> v.getName().equalsIgnoreCase(id)).findFirst(); if (opt.isPresent()) { return opt.get(); } final FlowVariable flowVar = new FlowVariable(id, new BeanFactoryVariableValueFactory(type, applicationContext.getAutowireCapableBeanFactory())); flow.addVariable(flowVar); return flowVar; } /** * Create state model bindings. * * @param properties the properties * @return the binder configuration */ protected BinderConfiguration createStateBinderConfiguration(final List<String> properties) { final BinderConfiguration binder = new BinderConfiguration(); properties.forEach(p -> binder.addBinding(new BinderConfiguration.Binding(p, null, true))); return binder; } /** * Create state model binding. * * @param state the state * @param modelName the model name * @param modelType the model type */ protected void createStateModelBinding(final TransitionableState state, final String modelName, final Class modelType) { state.getAttributes().put("model", createExpression(modelName, modelType)); } /** * Gets state binder configuration. * * @param state the state * @return the state binder configuration */ protected BinderConfiguration getViewStateBinderConfiguration(final ViewState state) { final Field field = ReflectionUtils.findField(state.getViewFactory().getClass(), "binderConfiguration"); ReflectionUtils.makeAccessible(field); return (BinderConfiguration) ReflectionUtils.getField(field, state.getViewFactory()); } /** * Gets expression string from action. * * @param act the act * @return the expression string from action */ protected Expression getExpressionStringFromAction(final EvaluateAction act) { final Field field = ReflectionUtils.findField(act.getClass(), "expression"); ReflectionUtils.makeAccessible(field); return (Expression) ReflectionUtils.getField(field, act); } /** * Register multifactor providers state transitions into webflow. * * @param state the state */ protected void registerMultifactorProvidersStateTransitionsIntoWebflow(final TransitionableState state) { final Map<String, MultifactorAuthenticationProvider> providerMap = WebUtils.getAvailableMultifactorAuthenticationProviders(this.applicationContext); providerMap.forEach((k, v) -> createTransitionForState(state, v.getId(), v.getId())); } }