Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@
<groupId>org.wso2.carbon.identity.organization.management.core</groupId>
<artifactId>org.wso2.carbon.identity.organization.management.service</artifactId>
</dependency>
<dependency>
<groupId>org.wso2.carbon.identity.framework</groupId>
<artifactId>org.wso2.carbon.identity.rule.evaluation</artifactId>
</dependency>
<dependency>
<groupId>org.wso2.carbon.identity.framework</groupId>
<artifactId>org.wso2.carbon.identity.rule.management</artifactId>
</dependency>
<dependency>
<groupId>org.wso2.carbon.identity.framework</groupId>
<artifactId>org.wso2.carbon.identity.role.v2.mgt.core</artifactId>
</dependency>
</dependencies>

<build>
Expand All @@ -124,7 +136,8 @@
<Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
<Bundle-Name>${project.artifactId}</Bundle-Name>
<Private-Package>
org.wso2.carbon.identity.flow.execution.engine.internal
org.wso2.carbon.identity.flow.execution.engine.internal,
org.wso2.carbon.identity.flow.execution.engine.rule
</Private-Package>
<Import-Package>
javax.xml.parsers; version="${javax.xml.parsers.import.pkg.version}",
Expand Down Expand Up @@ -160,6 +173,8 @@
version="${carbon.identity.package.import.version.range}",
org.wso2.carbon.identity.flow.mgt.exception;
version="${carbon.identity.package.import.version.range}",
org.wso2.carbon.identity.flow.mgt.utils;
version="${carbon.identity.package.import.version.range}",
org.wso2.carbon.identity.input.validation.mgt.exceptions;
version="${carbon.identity.package.import.version.range}",
org.wso2.carbon.identity.input.validation.mgt.model;
Expand Down Expand Up @@ -190,7 +205,13 @@
com.fasterxml.jackson.annotation.*;
version="${com.fasterxml.jackson.annotation.version.range}",
org.wso2.carbon.utils; version="${carbon.kernel.package.import.version.range}",
org.slf4j; version="${org.slf4j.imp.pkg.version.range}"
org.slf4j; version="${org.slf4j.imp.pkg.version.range}",
org.wso2.carbon.identity.rule.evaluation.api.*;
version="${carbon.identity.package.import.version.range}",
org.wso2.carbon.identity.role.v2.mgt.core.*;
version="${carbon.identity.package.import.version.range}",
org.wso2.carbon.identity.rule.management.api.*;
version="${carbon.identity.package.import.version.range}"
</Import-Package>
<Export-Package>
!org.wso2.carbon.identity.flow.execution.internal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,13 @@ public enum ErrorMessages {
ERROR_CODE_ORGANIZATION_VALIDATION_FAILURE("65035",
"Error while validating organization details.",
"Error occurred while validating the organization details of tenant: %s."),
ERROR_CODE_BRANCH_EVALUATION_FAILURE("65036",
"Error while evaluating a decision.",
"The branch %s of the decision step %s could not be evaluated in the %s flow of tenant: %s."),
ERROR_CODE_CONDITIONAL_BRANCHING_DISABLED("65037",
"Conditional branching is not enabled.",
"The decision step %s in the %s flow of tenant: %s cannot run because conditional branching is not "
+ "enabled on this server."),

// Client errors.
ERROR_CODE_INVALID_FLOW_ID("60001",
Expand Down Expand Up @@ -335,6 +342,8 @@ private FlowExecutionConfigs() {

public static final String FLOW_EXECUTION_PROPERTY = "FlowExecution";
public static final String DEFAULT_TTL_PROPERTY = "FlowExecution.DefaultTTL";
public static final String REGISTRATION_DEFAULT_USER_STORE_PROPERTY =
"FlowExecution.Registration.DefaultUserStore";
public static final String FLOW_TYPE_TTL_CONFIG_KEY_PREFIX = "FlowTypeTTLs";
public static final String FLOW_TYPE_TTL_CONFIG_KEY = "FlowTypeTTL";
public static final String FLOW_TYPE_ATTRIBUTE = "type";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.wso2.carbon.identity.flow.execution.engine.exception.FlowEngineException;
import org.wso2.carbon.identity.flow.execution.engine.exception.FlowEngineServerException;
import org.wso2.carbon.identity.flow.execution.engine.graph.PagePromptNode;
import org.wso2.carbon.identity.flow.execution.engine.graph.RuleEvaluationNode;
import org.wso2.carbon.identity.flow.execution.engine.graph.TaskExecutionNode;
import org.wso2.carbon.identity.flow.execution.engine.graph.UserChoiceDecisionNode;
import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext;
Expand Down Expand Up @@ -223,6 +224,8 @@ private NodeResponse triggerNode(NodeConfig nodeConfig, FlowExecutionContext con
return new TaskExecutionNode().execute(context, nodeConfig);
case Constants.NodeTypes.PROMPT_ONLY:
return new PagePromptNode().execute(context, nodeConfig);
case Constants.NodeTypes.RULE_EVALUATION:
return new RuleEvaluationNode().execute(context, nodeConfig);
default:
throw handleServerException(context.getFlowType(), ERROR_CODE_UNSUPPORTED_NODE, nodeConfig.getType(),
context.getFlowType(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*
* Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
*
* WSO2 LLC. licenses this file to you 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.wso2.carbon.identity.flow.execution.engine.graph;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.identity.flow.execution.engine.Constants;
import org.wso2.carbon.identity.flow.execution.engine.exception.FlowEngineException;
import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder;
import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext;
import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser;
import org.wso2.carbon.identity.flow.execution.engine.model.NodeResponse;
import org.wso2.carbon.identity.flow.execution.engine.rule.AbstractFlowRuleEvaluationDataProvider;
import org.wso2.carbon.identity.flow.execution.engine.util.RuleEvaluationDiagnosticLogger;
import org.wso2.carbon.identity.flow.mgt.model.BranchDTO;
import org.wso2.carbon.identity.flow.mgt.model.NodeConfig;
import org.wso2.carbon.identity.flow.mgt.model.StepDTO;
import org.wso2.carbon.identity.flow.mgt.utils.FlowMgtUtils;
import org.wso2.carbon.identity.rule.evaluation.api.exception.RuleEvaluationException;
import org.wso2.carbon.identity.rule.evaluation.api.model.FlowContext;
import org.wso2.carbon.identity.rule.evaluation.api.model.FlowType;
import org.wso2.carbon.identity.rule.evaluation.api.service.RuleEvaluationService;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.wso2.carbon.identity.flow.execution.engine.util.FlowExecutionEngineUtils.handleServerException;
import static org.wso2.carbon.identity.flow.mgt.Constants.NodeTypes.RULE_EVALUATION;

/**
* A step that decides where the flow goes by evaluating rules against the context.
*/
public class RuleEvaluationNode implements Node {

private static final Log LOG = LogFactory.getLog(RuleEvaluationNode.class);
private static final RuleEvaluationDiagnosticLogger DIAGNOSTIC_LOGGER = new RuleEvaluationDiagnosticLogger();

@Override
public String getName() {

return RULE_EVALUATION;
}

@Override
public NodeResponse execute(FlowExecutionContext context, NodeConfig nodeConfig) throws FlowEngineException {

if (!FlowMgtUtils.isConditionalBranchingEnabled()) {
DIAGNOSTIC_LOGGER.logConditionalBranchingDisabled(context.getFlowType(), nodeConfig.getId());
throw handleServerException(Constants.ErrorMessages.ERROR_CODE_CONDITIONAL_BRANCHING_DISABLED,
nodeConfig.getId(), context.getFlowType(), context.getTenantDomain());
}
BranchDTO selected = selectBranch(context, nodeConfig, branchesOf(context, nodeConfig));
if (selected == null) {
DIAGNOSTIC_LOGGER.logNoBranchSelected(context.getFlowType(), nodeConfig.getId());
throw handleServerException(Constants.ErrorMessages.ERROR_CODE_UNSUPPORTED_NODE,
nodeConfig.getType(), context.getFlowType(),
context.getGraphConfig().getId(), context.getTenantDomain());
}

nodeConfig.setNextNodeId(selected.getNextId());
if (LOG.isDebugEnabled()) {
LOG.debug("Rule evaluation " + nodeConfig.getId() + " selected branch " + selected.getId() + ".");
}
return new NodeResponse.Builder().status(Constants.STATUS_COMPLETE).build();
}

@Override
public NodeResponse rollback(FlowExecutionContext context, NodeConfig nodeConfig) throws FlowEngineException {

return new NodeResponse.Builder().status(Constants.STATUS_COMPLETE).build();
}

/**
* The first branch whose rule holds, or the default.
*
* @param context Flow execution context.
* @param nodeConfig The decision step.
* @param branches Branches of the decision, in evaluation order.
* @return The winning branch, or null when the decision has no default and nothing matched.
* @throws FlowEngineException If a branch cannot be evaluated.
*/
private BranchDTO selectBranch(FlowExecutionContext context, NodeConfig nodeConfig, List<BranchDTO> branches)
throws FlowEngineException {

FlowExecutionContext ruleContext = ruleEvaluationContext(context);
BranchDTO defaultBranch = null;
for (BranchDTO branch : branches) {
if (branch.isDefault()) {
defaultBranch = branch;
continue;
}
if (holds(branch, ruleContext, context, nodeConfig)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Branch '" + branch.getName() + "' holds.");
}
DIAGNOSTIC_LOGGER.logBranchSelected(context.getFlowType(), nodeConfig.getId(), branch.getId(),
branch.getName(), branch.getNextId());
return branch;
}
}
if (defaultBranch != null) {
DIAGNOSTIC_LOGGER.logDefaultBranchSelected(context.getFlowType(), nodeConfig.getId(),
defaultBranch.getId(), defaultBranch.getName());
}
return defaultBranch;
}

/**
* Whether a branch's rule holds. A branch that cannot be evaluated fails the flow rather than counting as not
* holding, so a failed lookup never sends the user down another branch.
*
* @param branch The branch.
* @param ruleContext The copy of the context handed to the rule component.
* @param context Flow execution context.
* @param nodeConfig The decision step.
* @return True if the branch's rule is satisfied.
* @throws FlowEngineException If the branch cannot be evaluated.
*/
private boolean holds(BranchDTO branch, FlowExecutionContext ruleContext, FlowExecutionContext context,
NodeConfig nodeConfig) throws FlowEngineException {

RuleEvaluationService evaluationService =
FlowExecutionEngineDataHolder.getInstance().getRuleEvaluationService();
try {
if (evaluationService == null) {
throw new IllegalStateException("Rule evaluation service is not available.");
}
Map<String, Object> contextData = new HashMap<>();
contextData.put(AbstractFlowRuleEvaluationDataProvider.FLOW_EXECUTION_CONTEXT, ruleContext);
FlowContext ruleFlowContext = new FlowContext(
FlowType.valueOf(context.getFlowType()), contextData);

return evaluationService.evaluate(branch.getRule(), ruleFlowContext, context.getTenantDomain())
.isRuleSatisfied();
} catch (RuleEvaluationException | RuntimeException e) {
DIAGNOSTIC_LOGGER.logBranchEvaluationFailed(context.getFlowType(), nodeConfig.getId(), branch.getName(),
e.getMessage());
throw handleServerException(Constants.ErrorMessages.ERROR_CODE_BRANCH_EVALUATION_FAILURE, e,
branch.getName(), nodeConfig.getId(), context.getFlowType(), context.getTenantDomain());
}
}

/**
* A copy of the context holding only what the rule fields read, to hand to the rule component. The flow's own
* context is never passed out, so evaluation cannot change it, and the user's credentials, inputs and the flow
* graph stay in the engine.
*
* @param context Flow execution context.
* @return The copy.
*/
private static FlowExecutionContext ruleEvaluationContext(FlowExecutionContext context) {

FlowExecutionContext copy = new FlowExecutionContext();
copy.setContextIdentifier(context.getContextIdentifier());
copy.setTenantDomain(context.getTenantDomain());
copy.setApplicationId(context.getApplicationId());
copy.setFlowType(context.getFlowType());

FlowUser user = context.getFlowUser();
if (user == null) {
copy.setFlowUser(null);
return copy;
}
// Copied field by field: FlowUser#getUsername makes up a username when none is given.
FlowUser userCopy = new FlowUser();
userCopy.setUserId(user.getUserId());
userCopy.setUserStoreDomain(user.getUserStoreDomain());
if (user.getClaims() != null) {
userCopy.addClaims(new HashMap<>(user.getClaims()));
}
copy.setFlowUser(userCopy);
return copy;
}

private List<BranchDTO> branchesOf(FlowExecutionContext context, NodeConfig nodeConfig)
throws FlowEngineException {

StepDTO step = context.getGraphConfig() == null
? null : context.getGraphConfig().getNodePageMappings().get(nodeConfig.getId());
if (step == null || step.getData() == null || step.getData().getAction() == null
|| step.getData().getAction().getBranches() == null) {
throw handleServerException(Constants.ErrorMessages.ERROR_CODE_UNSUPPORTED_NODE,
nodeConfig.getType(), context.getFlowType(),
context.getGraphConfig() == null ? null : context.getGraphConfig().getId(),
context.getTenantDomain());
}
return step.getData().getAction().getBranches();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import static org.wso2.carbon.identity.flow.execution.engine.Constants.STATUS_INCOMPLETE;
import static org.wso2.carbon.identity.flow.execution.engine.util.FlowExecutionEngineUtils.handleClientException;
import static org.wso2.carbon.identity.flow.execution.engine.util.FlowExecutionEngineUtils.handleServerException;
import static org.wso2.carbon.identity.flow.execution.engine.util.FlowExecutionEngineUtils.resolveUserStoreDomain;
import static org.wso2.carbon.identity.flow.mgt.Constants.NodeTypes.TASK_EXECUTION;
import static org.wso2.carbon.identity.flow.mgt.Constants.StepTypes.INTERNAL_PROMPT;
import static org.wso2.carbon.identity.flow.mgt.Constants.StepTypes.REDIRECTION;
Expand Down Expand Up @@ -208,6 +209,7 @@ private NodeResponse handleCompleteStatus(FlowExecutionContext context, Executor
if (response.getUpdatedUserClaims() != null) {
response.getUpdatedUserClaims()
.forEach((key, value) -> user.addUpdatedClaim(key, String.valueOf(value)));
resolveUserStoreDomain(context);
}
if (response.getUserCredentials() != null) {
user.getUserCredentials().putAll(response.getUserCredentials());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import org.wso2.carbon.identity.flow.mgt.FlowMgtService;
import org.wso2.carbon.identity.input.validation.mgt.services.InputValidationManagementService;
import org.wso2.carbon.identity.organization.management.service.OrganizationManager;
import org.wso2.carbon.identity.role.v2.mgt.core.RoleManagementService;
import org.wso2.carbon.identity.rule.evaluation.api.service.RuleEvaluationService;
import org.wso2.carbon.identity.user.profile.mgt.association.federation.FederatedAssociationManager;
import org.wso2.carbon.user.core.service.RealmService;

Expand All @@ -51,6 +53,8 @@ public class FlowExecutionEngineDataHolder {
private FederatedAssociationManager federatedAssociationManager;
private IdentityEventService identityEventService;
private OrganizationManager organizationManager;
private RuleEvaluationService ruleEvaluationService;
private RoleManagementService roleManagementService;
private List<FlowExecutionListener> flowExecutionListeners = new ArrayList<>();

private FlowExecutionEngineDataHolder() {
Expand Down Expand Up @@ -106,6 +110,36 @@ public void removeExecutor(Executor executor) {
executors.remove(name, executor);
}

/**
* Evaluates the rules guarding the branches of a rule decision.
*
* @return The rule evaluation service, or null when it is not yet available.
*/
public RuleEvaluationService getRuleEvaluationService() {

return ruleEvaluationService;
}

public void setRuleEvaluationService(RuleEvaluationService ruleEvaluationService) {

this.ruleEvaluationService = ruleEvaluationService;
}

/**
* Role management, used to read the roles a user is assigned when a flow condition names them.
*
* @return The role management service, or null when it is not yet available.
*/
public RoleManagementService getRoleManagementService() {

return roleManagementService;
}

public void setRoleManagementService(RoleManagementService roleManagementService) {

this.roleManagementService = roleManagementService;
}

/**
* Get the realm service.
*
Expand Down
Loading
Loading