From 256f3c7b69e754e73ef1c1bfcc195a922c1a8bff Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Thu, 5 Feb 2026 17:26:13 +0530 Subject: [PATCH 01/41] Add In-Flow Extension Action Execution Support - Introduced InFlowExtensionExecutor to handle execution of In-Flow Extension actions during flow execution. - Created InFlowExtensionRequestBuilder to build action execution requests from FlowContext. - Implemented InFlowExtensionResponseProcessor to process responses from In-Flow Extension actions, handling context updates for properties, user claims, and user inputs. - Added OperationExecutionResult class to encapsulate the result of operation executions. - Updated ActionExecutorConfig to include configuration for In-Flow Extension actions. - Enhanced Action model to support In-Flow Extension action type. - Modified ActionManagementConfig to manage versions and properties for In-Flow Extension actions. --- .../execution/api/model/ActionType.java | 3 +- .../ActionExecutionServiceComponent.java | 23 +- ...ActionExecutionServiceComponentHolder.java | 22 + .../executor/InFlowExtensionEvent.java | 182 +++++++ .../executor/InFlowExtensionExecutor.java | 333 +++++++++++++ .../InFlowExtensionRequestBuilder.java | 279 +++++++++++ .../InFlowExtensionResponseProcessor.java | 453 ++++++++++++++++++ .../executor/OperationExecutionResult.java | 62 +++ .../internal/util/ActionExecutorConfig.java | 12 +- .../action/management/api/model/Action.java | 8 +- .../internal/util/ActionManagementConfig.java | 11 +- 11 files changed, 1382 insertions(+), 6 deletions(-) create mode 100644 components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionEvent.java create mode 100644 components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java create mode 100644 components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java create mode 100644 components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java create mode 100644 components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/OperationExecutionResult.java diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/ActionType.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/ActionType.java index a7ff01fc0297..203edb9ae14d 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/ActionType.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/ActionType.java @@ -29,7 +29,8 @@ public enum ActionType { PRE_UPDATE_PASSWORD, PRE_UPDATE_PROFILE, AUTHENTICATION, - PRE_ISSUE_ID_TOKEN; + PRE_ISSUE_ID_TOKEN, + IN_FLOW_EXTENSION; public String getDisplayName() { diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java index 185114e5da7c..253b8118c540 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java @@ -33,12 +33,16 @@ import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.action.execution.api.service.ActionInvocationResponseClassProvider; import org.wso2.carbon.identity.action.execution.api.service.ActionVersioningHandler; +import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionExecutor; +import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionRequestBuilder; +import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionResponseProcessor; import org.wso2.carbon.identity.action.execution.internal.service.impl.ActionExecutionRequestBuilderFactory; import org.wso2.carbon.identity.action.execution.internal.service.impl.ActionExecutionResponseProcessorFactory; import org.wso2.carbon.identity.action.execution.internal.service.impl.ActionExecutorServiceImpl; import org.wso2.carbon.identity.action.execution.internal.service.impl.ActionInvocationResponseClassFactory; import org.wso2.carbon.identity.action.execution.internal.service.impl.ActionVersioningHandlerFactory; import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; +import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; import org.wso2.carbon.identity.rule.evaluation.api.service.RuleEvaluationService; import org.wso2.carbon.identity.secret.mgt.core.SecretManager; import org.wso2.carbon.identity.secret.mgt.core.SecretResolveManager; @@ -60,8 +64,23 @@ protected void activate(ComponentContext context) { try { BundleContext bundleCtx = context.getBundleContext(); - bundleCtx.registerService(ActionExecutorService.class.getName(), ActionExecutorServiceImpl.getInstance(), - null); + + // Register ActionExecutorService + ActionExecutorService actionExecutorService = ActionExecutorServiceImpl.getInstance(); + bundleCtx.registerService(ActionExecutorService.class.getName(), actionExecutorService, null); + + // Store reference for internal use + ActionExecutionServiceComponentHolder.getInstance().setActionExecutorService(actionExecutorService); + + // Register In-Flow Extension services + bundleCtx.registerService(ActionExecutionRequestBuilder.class.getName(), + new InFlowExtensionRequestBuilder(), null); + bundleCtx.registerService(ActionExecutionResponseProcessor.class.getName(), + new InFlowExtensionResponseProcessor(), null); + + // Register InFlowExtensionExecutor for the flow engine + bundleCtx.registerService(Executor.class.getName(), new InFlowExtensionExecutor(), null); + LOG.debug("Action execution bundle is activated."); } catch (Throwable e) { LOG.error("Error while initializing Action execution service component.", e); diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java index d5f90106b0d0..05f2708d7fc9 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java @@ -18,6 +18,7 @@ package org.wso2.carbon.identity.action.execution.internal.component; +import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; import org.wso2.carbon.identity.rule.evaluation.api.service.RuleEvaluationService; import org.wso2.carbon.identity.secret.mgt.core.SecretManager; @@ -36,6 +37,7 @@ public class ActionExecutionServiceComponentHolder { private SecretManager secretManager; private SecretResolveManager secretResolveManager; private RealmService realmService; + private ActionExecutorService actionExecutorService; private ActionExecutionServiceComponentHolder() { @@ -115,4 +117,24 @@ public void setRealmService(RealmService realmService) { this.realmService = realmService; } + + /** + * Get the ActionExecutorService instance. + * + * @return ActionExecutorService instance. + */ + public ActionExecutorService getActionExecutorService() { + + return actionExecutorService; + } + + /** + * Set the ActionExecutorService instance. + * + * @param actionExecutorService ActionExecutorService instance. + */ + public void setActionExecutorService(ActionExecutorService actionExecutorService) { + + this.actionExecutorService = actionExecutorService; + } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionEvent.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionEvent.java new file mode 100644 index 000000000000..ee6598cb897e --- /dev/null +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionEvent.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2025, 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.action.execution.internal.executor; + +import org.wso2.carbon.identity.action.execution.api.model.Application; +import org.wso2.carbon.identity.action.execution.api.model.Event; +import org.wso2.carbon.identity.action.execution.api.model.Organization; +import org.wso2.carbon.identity.action.execution.api.model.Request; +import org.wso2.carbon.identity.action.execution.api.model.Tenant; +import org.wso2.carbon.identity.action.execution.api.model.User; +import org.wso2.carbon.identity.action.execution.api.model.UserStore; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * This class models the In-Flow Extension Event. + * It represents the event sent to the In-Flow Extension action over the Action Execution Request. + * Contains flow-specific context such as flow type, current node, user inputs, and flow properties. + */ +public class InFlowExtensionEvent extends Event { + + private final String flowType; + private final String currentNodeId; + private final Map userInputs; + private final Map flowProperties; + + private InFlowExtensionEvent(Builder builder) { + + this.request = builder.request; + this.tenant = builder.tenant; + this.organization = builder.organization; + this.user = builder.user; + this.userStore = builder.userStore; + this.application = builder.application; + this.flowType = builder.flowType; + this.currentNodeId = builder.currentNodeId; + this.userInputs = builder.userInputs != null ? + Collections.unmodifiableMap(new HashMap<>(builder.userInputs)) : Collections.emptyMap(); + this.flowProperties = builder.flowProperties != null ? + Collections.unmodifiableMap(new HashMap<>(builder.flowProperties)) : Collections.emptyMap(); + } + + /** + * Get the flow type. + * + * @return The flow type (e.g., "REGISTRATION", "PASSWORD_RESET"). + */ + public String getFlowType() { + + return flowType; + } + + /** + * Get the current node ID in the flow. + * + * @return The current node ID. + */ + public String getCurrentNodeId() { + + return currentNodeId; + } + + /** + * Get the user inputs collected during the flow. + * + * @return Unmodifiable map of user inputs. + */ + public Map getUserInputs() { + + return userInputs; + } + + /** + * Get the flow properties/context data. + * + * @return Unmodifiable map of flow properties. + */ + public Map getFlowProperties() { + + return flowProperties; + } + + /** + * Builder for the InFlowExtensionEvent. + */ + public static class Builder { + + private Request request; + private Tenant tenant; + private Organization organization; + private User user; + private UserStore userStore; + private Application application; + private String flowType; + private String currentNodeId; + private Map userInputs; + private Map flowProperties; + + public Builder request(Request request) { + + this.request = request; + return this; + } + + public Builder tenant(Tenant tenant) { + + this.tenant = tenant; + return this; + } + + public Builder organization(Organization organization) { + + this.organization = organization; + return this; + } + + public Builder user(User user) { + + this.user = user; + return this; + } + + public Builder userStore(UserStore userStore) { + + this.userStore = userStore; + return this; + } + + public Builder application(Application application) { + + this.application = application; + return this; + } + + public Builder flowType(String flowType) { + + this.flowType = flowType; + return this; + } + + public Builder currentNodeId(String currentNodeId) { + + this.currentNodeId = currentNodeId; + return this; + } + + public Builder userInputs(Map userInputs) { + + this.userInputs = userInputs; + return this; + } + + public Builder flowProperties(Map flowProperties) { + + this.flowProperties = flowProperties; + return this; + } + + public InFlowExtensionEvent build() { + + return new InFlowExtensionEvent(this); + } + } +} diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java new file mode 100644 index 000000000000..678211b3b46b --- /dev/null +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2025, 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.action.execution.internal.executor; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionException; +import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionStatus; +import org.wso2.carbon.identity.action.execution.api.model.ActionType; +import org.wso2.carbon.identity.action.execution.api.model.Error; +import org.wso2.carbon.identity.action.execution.api.model.Failure; +import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; +import org.wso2.carbon.identity.action.execution.internal.component.ActionExecutionServiceComponentHolder; +import org.wso2.carbon.identity.flow.execution.engine.exception.FlowEngineException; +import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; +import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; +import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; +import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; +import org.wso2.carbon.identity.flow.mgt.model.NodeConfig; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * This class is responsible for executing In-Flow Extension actions during flow execution. + * It integrates with the ActionExecutorService to call external services and process their responses. + */ +public class InFlowExtensionExecutor implements Executor { + + private static final Log LOG = LogFactory.getLog(InFlowExtensionExecutor.class); + private static final String EXECUTOR_NAME = "ExtensionExecutor"; +// private static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; + protected static final String CORRELATION_ID_KEY = "correlationId"; + protected static final String FLOW_USER_INPUT_DATA_KEY = "userInputData"; + protected static final String FLOW_USER_KEY = "flowUser"; + protected static final String FLOW_PROPERTIES_KEY = "flowProperties"; + protected static final String FLOW_CONTEXT_UPDATES_KEY = "contextUpdates"; + protected static final String ACTION_ID_METADATA_KEY = "actionId"; + protected static final String ALLOWED_OPERATIONS_METADATA_KEY = "allowedOperations"; + protected static final String ALLOWED_OPERATIONS_KEY = "allowedOperations"; + protected static final String TENET_DOMAIN_KEY = "tenetDomain"; + protected static final String APPLICATION_ID_KEY = "applicationId"; + protected static final String FLOW_TYPE_KEY = "flowType"; + protected static final String CURRENT_NODE_KEY = "currentNode"; + + @Override + public String getName() { + + return EXECUTOR_NAME; + } + + @Override + public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineException { + + ExecutorResponse response = new ExecutorResponse(); + + try { + // Get the action ID from the executor metadata configuration + String actionId = getActionIdFromMetadata(context); + if (actionId == null || actionId.isEmpty()) { + LOG.warn("No action ID configured for In-Flow Extension executor. Skipping execution."); + response.setResult(ExecutorResult.COMPLETE.name()); + return response; + } + + LOG.debug("Executing In-Flow Extension action with ID: " + actionId); + + // Build the flow context for ActionExecutorService + FlowContext flowContext = buildFlowContext(context); // passing the whole flow execution context as an object for now + + // Get the ActionExecutorService + ActionExecutorService actionExecutorService = getActionExecutorService(); + if (actionExecutorService == null) { + throw new FlowEngineException("ActionExecutorService is not available."); + } + + // Check if execution is enabled for this action type + if (!actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)) { + LOG.debug("In-Flow Extension action execution is disabled. Skipping."); + response.setResult(ExecutorResult.COMPLETE.name()); + return response; + } + + // Execute the action + ActionExecutionStatus executionStatus = actionExecutorService.execute( + ActionType.IN_FLOW_EXTENSION, + actionId, + flowContext, + context.getTenantDomain()); + + // Process the result + return processExecutionStatus(executionStatus, context); + + } catch (ActionExecutionException e) { + LOG.error("Error executing In-Flow Extension action.", e); + response.setResult(ExecutorResult.ERROR.name()); + response.setErrorMessage("Action execution failed"); + response.setErrorDescription(e.getMessage()); + response.setThrowable(e); + return response; + } + } + + @Override + public List getInitiationData() { + + return Collections.emptyList(); + } + + @Override + public ExecutorResponse rollback(FlowExecutionContext context) throws FlowEngineException { + + ExecutorResponse response = new ExecutorResponse(); + response.setResult(ExecutorResult.COMPLETE.name()); + return response; + } + + /** + * Build the FlowContext for ActionExecutorService from FlowExecutionContext. + * + * @param context The FlowExecutionContext. + * @return The FlowContext for action execution. + */ + private FlowContext buildFlowContext(FlowExecutionContext context) { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(CORRELATION_ID_KEY, context.getCorrelationId()); + flowContext.add(FLOW_USER_KEY, context.getFlowUser()); + flowContext.add(FLOW_USER_INPUT_DATA_KEY, context.getUserInputData()); + flowContext.add(FLOW_PROPERTIES_KEY, context.getProperties()); + flowContext.add(TENET_DOMAIN_KEY, context.getTenantDomain()); + flowContext.add(APPLICATION_ID_KEY, context.getApplicationId()); + flowContext.add(FLOW_TYPE_KEY, context.getFlowType()); + flowContext.add(CURRENT_NODE_KEY, context.getCurrentNode()); + + // Add allowed operations from executor metadata + String allowedOperationsJson = getAllowedOperationsFromMetadata(context); + if (allowedOperationsJson != null) { + flowContext.add(ALLOWED_OPERATIONS_KEY, allowedOperationsJson); + } + + return flowContext; + } + + /** + * Process the ActionExecutionStatus and map it to ExecutorResponse. + * + * @param executionStatus The status returned by ActionExecutorService. + * @param context The FlowExecutionContext to update with response data. + * @return The ExecutorResponse. + */ + @SuppressWarnings("unchecked") + private ExecutorResponse processExecutionStatus(ActionExecutionStatus executionStatus, + FlowExecutionContext context) { + + ExecutorResponse response = new ExecutorResponse(); + + if (executionStatus == null) { + response.setResult(ExecutorResult.USER_INPUT_REQUIRED.name()); + return response; + } + + switch (executionStatus.getStatus()) { + case SUCCESS: + response.setResult(ExecutorResult.COMPLETE.name()); + // Extract context updates from response and apply them + applyContextUpdates(executionStatus.getResponseContext(), context, response); + break; + + case FAILED: + response.setResult(ExecutorResult.USER_ERROR.name()); + Failure failure = (Failure) executionStatus.getResponse(); + if (failure != null) { + response.setErrorMessage(failure.getFailureReason()); + response.setErrorDescription(failure.getFailureDescription()); + } + break; + + case ERROR: + response.setResult(ExecutorResult.ERROR.name()); + Error error = (Error) executionStatus.getResponse(); + if (error != null) { + response.setErrorMessage(error.getErrorMessage()); + response.setErrorDescription(error.getErrorDescription()); + } + break; + + case INCOMPLETE: + // INCOMPLETE status indicates the flow should wait for external input + response.setResult(ExecutorResult.USER_INPUT_REQUIRED.name()); + break; + + default: + LOG.warn("Unknown execution status: " + executionStatus.getStatus()); + response.setResult(ExecutorResult.COMPLETE.name()); + } + + return response; + } + + /** + * Apply context updates from the action response to the FlowExecutionContext. + * + * @param responseContext The response context from action execution. + * @param flowContext The FlowExecutionContext to update. + * @param response The ExecutorResponse to populate with context properties. + */ + @SuppressWarnings("unchecked") + private void applyContextUpdates(Map responseContext, + FlowExecutionContext flowContext, + ExecutorResponse response) { + + if (responseContext == null || responseContext.isEmpty()) { + return; + } + + // Extract context updates from the response + Object contextUpdatesObj = responseContext.get(FLOW_CONTEXT_UPDATES_KEY); + if (contextUpdatesObj instanceof Map) { + Map contextUpdates = (Map) contextUpdatesObj; + + // Apply updates to FlowExecutionContext properties + for (Map.Entry entry : contextUpdates.entrySet()) { + flowContext.setProperty(entry.getKey(), entry.getValue()); + if (LOG.isDebugEnabled()) { + LOG.debug("Applied context update: " + entry.getKey()); + } + } + + // Also set the context properties in the executor response + response.setContextProperty(contextUpdates); + } + } + + /** + * Get the ActionExecutorService instance. + * + * @return The ActionExecutorService. + */ + private ActionExecutorService getActionExecutorService() { + + return ActionExecutionServiceComponentHolder.getInstance().getActionExecutorService(); + } + + /** + * Extract the action ID from the executor metadata configuration. + * The action ID should be configured in the flow JSON as part of the executor's meta object. + * + * @param context The FlowExecutionContext containing the current node configuration. + * @return The action ID if configured, null otherwise. + */ + private String getActionIdFromMetadata(FlowExecutionContext context) { + + NodeConfig currentNode = context.getCurrentNode(); + if (currentNode == null) { + LOG.debug("Current node is null, cannot extract action ID from metadata."); + return null; + } + + ExecutorDTO executorConfig = currentNode.getExecutorConfig(); + if (executorConfig == null) { + LOG.debug("Executor config is null, cannot extract action ID from metadata."); + return null; + } + + Map metadata = executorConfig.getMetadata(); + if (metadata == null || metadata.isEmpty()) { + LOG.debug("Executor metadata is null or empty, cannot extract action ID."); + return null; + } + + return metadata.get(ACTION_ID_METADATA_KEY); + } + + /** + * Extract the allowed operations configuration from the executor metadata. + * The allowed operations should be configured in the flow JSON as part of the executor's meta object. + * + * @param context The FlowExecutionContext containing the current node configuration. + * @return The allowed operations JSON string if configured, null otherwise. + */ + private String getAllowedOperationsFromMetadata(FlowExecutionContext context) { + + NodeConfig currentNode = context.getCurrentNode(); + if (currentNode == null) { + return null; + } + + ExecutorDTO executorConfig = currentNode.getExecutorConfig(); + if (executorConfig == null) { + return null; + } + + Map metadata = executorConfig.getMetadata(); + if (metadata == null || metadata.isEmpty()) { + return null; + } + + return metadata.get(ALLOWED_OPERATIONS_METADATA_KEY); + } + + /** + * Enum representing the possible results of executor execution. + * These values must match the expected status values in the flow execution engine. + * @see org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus + */ + public enum ExecutorResult { + COMPLETE, // Executor completed successfully + ERROR, // Server-side error occurred + USER_ERROR, // User-related error (e.g., validation failure) + USER_INPUT_REQUIRED,// Additional user input needed + EXTERNAL_REDIRECTION,// Redirect to external service + RETRY // Retry the current step + } +} diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java new file mode 100644 index 000000000000..8a3cb9d3a961 --- /dev/null +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2025, 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.action.execution.internal.executor; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionRequestBuilderException; +import org.wso2.carbon.identity.action.execution.api.model.*; +import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionRequestBuilder; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; +import org.wso2.carbon.identity.flow.mgt.model.NodeConfig; + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * This class is responsible for building the Action Execution Request for In-Flow Extension actions. + * It converts the FlowExecutionContext into the standard ActionExecutionRequest format. + */ +public class InFlowExtensionRequestBuilder implements ActionExecutionRequestBuilder { + + private static final Log LOG = LogFactory.getLog(InFlowExtensionRequestBuilder.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final TypeReference>> OPERATION_LIST_TYPE_REF = + new TypeReference>>() { }; + + @Override + public ActionType getSupportedActionType() { + + return ActionType.IN_FLOW_EXTENSION; + } + + @Override + public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContext, + ActionExecutionRequestContext actionExecutionContext) + throws ActionExecutionRequestBuilderException { + +// FlowExecutionContext flowExecutionContext = flowContext.getValue(FLOW_EXECUTION_CONTEXT_KEY, +// FlowExecutionContext.class); + +// if (flowExecutionContext == null) { +// throw new ActionExecutionRequestBuilderException("FlowExecutionContext not found in flow context."); +// } + + InFlowExtensionEvent event = buildEvent(flowContext); + + // Build allowed operations from metadata configuration + List allowedOperations = buildAllowedOperations(flowContext); + + return new ActionExecutionRequest.Builder() + .actionType(ActionType.IN_FLOW_EXTENSION) + .flowId(flowContext.getValue(InFlowExtensionExecutor.CORRELATION_ID_KEY, String.class)) + .event(event) + .allowedOperations(allowedOperations) + .build(); + } + + /** + * Build allowed operations from the flow context. + * Parses the allowedOperations JSON from executor metadata. + * + * @param flowContext The flow context containing allowed operations configuration. + * @return List of AllowedOperation objects. + */ + private List buildAllowedOperations(FlowContext flowContext) { + + String allowedOperationsJson = flowContext.getValue( + InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, String.class); + + if (allowedOperationsJson == null || allowedOperationsJson.isEmpty()) { + LOG.debug("No allowed operations configured in executor metadata. Using empty list."); + return Collections.emptyList(); + } + + try { + List> operationConfigs = objectMapper.readValue( + allowedOperationsJson, OPERATION_LIST_TYPE_REF); + + List allowedOperations = new ArrayList<>(); + for (Map config : operationConfigs) { + AllowedOperation operation = createAllowedOperationFromConfig(config); + if (operation != null) { + allowedOperations.add(operation); + } + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Built " + allowedOperations.size() + " allowed operations from executor metadata."); + } + + return allowedOperations; + } catch (JsonProcessingException e) { + LOG.error("Failed to parse allowed operations from executor metadata: " + e.getMessage(), e); + return Collections.emptyList(); + } + } + + /** + * Create an AllowedOperation from a configuration map. + * + * @param config The configuration map containing 'op' and 'paths'. + * @return AllowedOperation object or null if invalid. + */ + @SuppressWarnings("unchecked") + private AllowedOperation createAllowedOperationFromConfig(Map config) { + + String opString = (String) config.get("op"); + Object pathsObj = config.get("paths"); + + if (opString == null || pathsObj == null) { + LOG.warn("Invalid allowed operation config: missing 'op' or 'paths'."); + return null; + } + + Operation operation; + try { + operation = Operation.valueOf(opString.toUpperCase(Locale.ENGLISH)); + } catch (IllegalArgumentException e) { + LOG.warn("Unknown operation type: " + opString); + return null; + } + + List paths; + if (pathsObj instanceof List) { + paths = (List) pathsObj; + } else { + LOG.warn("Invalid 'paths' type in allowed operation config."); + return null; + } + + AllowedOperation allowedOperation = new AllowedOperation(); + allowedOperation.setOp(operation); + allowedOperation.setPaths(new ArrayList<>(paths)); + return allowedOperation; + } + + /** + * Build the InFlowExtensionEvent from FlowExecutionContext. + * + * @param context The FlowExecutionContext. + * @return The InFlowExtensionEvent. + */ + private InFlowExtensionEvent buildEvent(FlowContext context) { + + InFlowExtensionEvent.Builder eventBuilder = new InFlowExtensionEvent.Builder(); + + // Set tenant information + String tenantDomain = context.getValue(InFlowExtensionExecutor.TENET_DOMAIN_KEY, String.class); + if (tenantDomain != null) { + int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); + eventBuilder.tenant(new Tenant(String.valueOf(tenantId), tenantDomain)); + } + + // Set application information if available + String applicationId = context.getValue(InFlowExtensionExecutor.APPLICATION_ID_KEY, String.class); + if (applicationId != null) { + eventBuilder.application(new Application(applicationId, null)); + } + + // Set user information from FlowUser + FlowUser flowUser = context.getValue(InFlowExtensionExecutor.FLOW_USER_KEY, FlowUser.class); + if (flowUser != null) { + eventBuilder.user(buildUser(flowUser)); + + // Set user store if available + String userStoreDomain = flowUser.getUserStoreDomain(); + if (userStoreDomain != null) { + eventBuilder.userStore(new UserStore(userStoreDomain)); + } + } + + // Set flow-specific information + eventBuilder.flowType(context.getValue(InFlowExtensionExecutor.FLOW_TYPE_KEY, String.class)); + + // Set current node ID if available + NodeConfig currentNode = context.getValue(InFlowExtensionExecutor.CURRENT_NODE_KEY, NodeConfig.class); + if (currentNode != null) { + eventBuilder.currentNodeId(currentNode.getId()); + } + + // Set user inputs + eventBuilder.userInputs(context.getValue(InFlowExtensionExecutor.FLOW_USER_INPUT_DATA_KEY, Map.class)); + + // Set flow properties (filtering out sensitive data) + eventBuilder.flowProperties(filterFlowProperties(context.getValue(InFlowExtensionExecutor.FLOW_PROPERTIES_KEY, Map.class))); + + return eventBuilder.build(); + } + + /** + * Build the User model from FlowUser. + * + * @param flowUser The FlowUser from flow context. + * @return The User model for the event. + */ + private User buildUser(FlowUser flowUser) { + + User.Builder userBuilder = new User.Builder(flowUser.getUserId()); + + // Convert FlowUser claims to UserClaims + Map claims = flowUser.getClaims(); + if (claims != null && !claims.isEmpty()) { + List userClaims = new ArrayList<>(); + for (Map.Entry claim : claims.entrySet()) { + userClaims.add(new UserClaim(claim.getKey(), claim.getValue())); + } + userBuilder.claims(userClaims); + } + + return userBuilder.build(); + } + + /** + * Filter flow properties to remove sensitive data before sending to external service. + * + * @param properties The original flow properties. + * @return Filtered properties map. + */ + private Map filterFlowProperties(Map properties) { + + if (properties == null) { + return new HashMap<>(); + } + + Map filtered = new HashMap<>(); + for (Map.Entry entry : properties.entrySet()) { + String key = entry.getKey(); + // Filter out sensitive keys + if (!isSensitiveKey(key)) { + filtered.put(key, entry.getValue()); + } + } + return filtered; + } + + /** + * Check if a property key is sensitive and should be filtered out. + * + * @param key The property key. + * @return true if the key is sensitive, false otherwise. + */ + private boolean isSensitiveKey(String key) { + + if (key == null) { + return false; + } + String lowerKey = key.toLowerCase(Locale.ENGLISH); + return lowerKey.contains("password") || + lowerKey.contains("secret") || + lowerKey.contains("credential") || + lowerKey.contains("token") || + lowerKey.contains("key"); + } +} diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java new file mode 100644 index 000000000000..11de53df8ee8 --- /dev/null +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java @@ -0,0 +1,453 @@ +/* + * Copyright (c) 2025, 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.action.execution.internal.executor; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.action.execution.api.constant.ActionExecutionLogConstants; +import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionResponseProcessorException; +import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionResponseContext; +import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionStatus; +import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationErrorResponse; +import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationFailureResponse; +import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationSuccessResponse; +import org.wso2.carbon.identity.action.execution.api.model.ActionType; +import org.wso2.carbon.identity.action.execution.api.model.Error; +import org.wso2.carbon.identity.action.execution.api.model.ErrorStatus; +import org.wso2.carbon.identity.action.execution.api.model.FailedStatus; +import org.wso2.carbon.identity.action.execution.api.model.Failure; +import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; +import org.wso2.carbon.identity.action.execution.api.model.Success; +import org.wso2.carbon.identity.action.execution.api.model.SuccessStatus; +import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionResponseProcessor; +import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; +import org.wso2.carbon.utils.DiagnosticLog; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * This class is responsible for processing the response from In-Flow Extension actions. + * It processes operations (ADD, REMOVE, REPLACE) on flow context properties and user claims, + * with dynamic path validation based on the allowed operations configured in the request. + */ +public class InFlowExtensionResponseProcessor implements ActionExecutionResponseProcessor { + + private static final Log LOG = LogFactory.getLog(InFlowExtensionResponseProcessor.class); + private static final String CONTEXT_UPDATES_KEY = "contextUpdates"; + + // Path prefixes for In-Flow Extension context + private static final String PROPERTIES_PATH_PREFIX = "/properties/"; + private static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; + private static final String USER_INPUTS_PATH_PREFIX = "/userInputs/"; + + private static final char PATH_SEPARATOR = '/'; + private static final String LAST_ELEMENT_CHARACTER = "-"; + + @Override + public ActionType getSupportedActionType() { + + return ActionType.IN_FLOW_EXTENSION; + } + + @Override + public ActionExecutionStatus processSuccessResponse(FlowContext flowContext, + ActionExecutionResponseContext responseContext) + throws ActionExecutionResponseProcessorException { + + Map responseContextMap = new HashMap<>(); + Map contextUpdates = new HashMap<>(); + + // Initialize sub-maps for different context areas + Map propertiesUpdates = new HashMap<>(); + Map userClaimsUpdates = new HashMap<>(); + Map userInputsUpdates = new HashMap<>(); + + List operationExecutionResultList = new ArrayList<>(); + + // Get operations from the response (already filtered by ActionExecutorServiceImpl) + List operationsToPerform = + responseContext.getActionInvocationResponse().getOperations(); + + if (operationsToPerform != null && !operationsToPerform.isEmpty()) { + for (PerformableOperation operation : operationsToPerform) { + OperationExecutionResult result = processOperation(operation, + propertiesUpdates, userClaimsUpdates, userInputsUpdates); + operationExecutionResultList.add(result); + } + } + + // Log operation execution results + logOperationExecutionResults(operationExecutionResultList); + + // Build the context updates map from processed operations + // Add properties directly to contextUpdates (not nested under "properties") + for (Map.Entry entry : propertiesUpdates.entrySet()) { + Object value = entry.getValue(); + // If value is OperationValue, extract the actual value + if (value instanceof OperationValue) { + OperationValue opValue = (OperationValue) value; + if (!"REMOVE".equals(opValue.getOperation())) { + contextUpdates.put(entry.getKey(), opValue.getValue()); + } + // For REMOVE, we don't add to contextUpdates (removal handled by executor) + } else { + contextUpdates.put(entry.getKey(), value); + } + } + + // User claims and inputs are kept as separate maps for the executor to handle + if (!userClaimsUpdates.isEmpty()) { + contextUpdates.put("userClaims", userClaimsUpdates); + } + if (!userInputsUpdates.isEmpty()) { + contextUpdates.put("userInputs", userInputsUpdates); + } + + if (!contextUpdates.isEmpty()) { + responseContextMap.put(CONTEXT_UPDATES_KEY, contextUpdates); + if (LOG.isDebugEnabled()) { + LOG.debug("Processed " + operationExecutionResultList.size() + + " operations from In-Flow Extension response."); + } + } + + return new SuccessStatus.Builder() + .setSuccess(new InFlowExtensionSuccess()) + .setResponseContext(responseContextMap) + .build(); + } + + /** + * Process a single operation and apply it to the appropriate context map. + * + * @param operation The operation to process. + * @param propertiesUpdates Map to store property updates. + * @param userClaimsUpdates Map to store user claim updates. + * @param userInputsUpdates Map to store user input updates. + * @return The result of the operation execution. + */ + private OperationExecutionResult processOperation(PerformableOperation operation, + Map propertiesUpdates, + Map userClaimsUpdates, + Map userInputsUpdates) { + + String path = operation.getPath(); + + // Route to appropriate handler based on path prefix + if (path.startsWith(PROPERTIES_PATH_PREFIX)) { + return handlePropertyOperation(operation, propertiesUpdates); + } else if (path.startsWith(USER_CLAIMS_PATH_PREFIX)) { + return handleUserClaimOperation(operation, userClaimsUpdates); + } else if (path.startsWith(USER_INPUTS_PATH_PREFIX)) { + return handleUserInputOperation(operation, userInputsUpdates); + } + + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Unknown path prefix. Supported prefixes: " + PROPERTIES_PATH_PREFIX + + ", " + USER_CLAIMS_PATH_PREFIX + ", " + USER_INPUTS_PATH_PREFIX); + } + + /** + * Handle operations on flow properties. + * + * @param operation The operation to perform. + * @param propertiesUpdates Map to store property updates. + * @return The result of the operation execution. + */ + private OperationExecutionResult handlePropertyOperation(PerformableOperation operation, + Map propertiesUpdates) { + + String propertyName = extractNameFromPath(operation.getPath(), PROPERTIES_PATH_PREFIX); + + if (propertyName == null || propertyName.isEmpty()) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Invalid property path. Property name is required."); + } + + switch (operation.getOp()) { + case ADD: + case REPLACE: + if (operation.getValue() == null) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Value is required for " + operation.getOp() + " operation."); + } + // For ADD and REPLACE, store the value with operation type for later processing + propertiesUpdates.put(propertyName, new OperationValue(operation.getOp().name(), + operation.getValue())); + return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, + "Property " + operation.getOp().name().toLowerCase(Locale.ENGLISH) + " operation queued."); + + case REMOVE: + // Mark property for removal + propertiesUpdates.put(propertyName, new OperationValue("REMOVE", null)); + return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, + "Property remove operation queued."); + + default: + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Unsupported operation: " + operation.getOp()); + } + } + + /** + * Handle operations on user claims. + * + * @param operation The operation to perform. + * @param userClaimsUpdates Map to store user claim updates. + * @return The result of the operation execution. + */ + private OperationExecutionResult handleUserClaimOperation(PerformableOperation operation, + Map userClaimsUpdates) { + + String claimUri = extractNameFromPath(operation.getPath(), USER_CLAIMS_PATH_PREFIX); + + if (claimUri == null || claimUri.isEmpty()) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Invalid claim path. Claim URI is required."); + } + + switch (operation.getOp()) { + case ADD: + case REPLACE: + if (operation.getValue() == null) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Value is required for " + operation.getOp() + " operation."); + } + userClaimsUpdates.put(claimUri, new OperationValue(operation.getOp().name(), + operation.getValue())); + return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, + "User claim " + operation.getOp().name().toLowerCase(Locale.ENGLISH) + " operation queued."); + + case REMOVE: + userClaimsUpdates.put(claimUri, new OperationValue("REMOVE", null)); + return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, + "User claim remove operation queued."); + + default: + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Unsupported operation: " + operation.getOp()); + } + } + + /** + * Handle operations on user inputs. + * + * @param operation The operation to perform. + * @param userInputsUpdates Map to store user input updates. + * @return The result of the operation execution. + */ + private OperationExecutionResult handleUserInputOperation(PerformableOperation operation, + Map userInputsUpdates) { + + String inputName = extractNameFromPath(operation.getPath(), USER_INPUTS_PATH_PREFIX); + + if (inputName == null || inputName.isEmpty()) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Invalid user input path. Input name is required."); + } + + switch (operation.getOp()) { + case ADD: + case REPLACE: + if (operation.getValue() == null) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Value is required for " + operation.getOp() + " operation."); + } + userInputsUpdates.put(inputName, new OperationValue(operation.getOp().name(), + operation.getValue())); + return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, + "User input " + operation.getOp().name().toLowerCase(Locale.ENGLISH) + " operation queued."); + + case REMOVE: + userInputsUpdates.put(inputName, new OperationValue("REMOVE", null)); + return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, + "User input remove operation queued."); + + default: + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Unsupported operation: " + operation.getOp()); + } + } + + /** + * Extract the name/key from the operation path after the prefix. + * + * @param path The full operation path. + * @param prefix The path prefix to remove. + * @return The extracted name, or null if invalid. + */ + private String extractNameFromPath(String path, String prefix) { + + if (path == null || !path.startsWith(prefix)) { + return null; + } + + String remaining = path.substring(prefix.length()); + + // Handle trailing slash + if (remaining.endsWith("/")) { + remaining = remaining.substring(0, remaining.length() - 1); + } + + // Handle array index notation (e.g., /properties/items/0) + int lastSlash = remaining.lastIndexOf(PATH_SEPARATOR); + if (lastSlash > 0) { + String possibleIndex = remaining.substring(lastSlash + 1); + if (LAST_ELEMENT_CHARACTER.equals(possibleIndex) || isNumeric(possibleIndex)) { + // Return path up to the index + return remaining.substring(0, lastSlash); + } + } + + return remaining; + } + + /** + * Check if a string is numeric. + */ + private boolean isNumeric(String str) { + + try { + Integer.parseInt(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Log operation execution results for diagnostics and debugging. + * + * @param operationExecutionResultList List of operation execution results. + */ + private void logOperationExecutionResults(List operationExecutionResultList) { + + if (operationExecutionResultList.isEmpty()) { + return; + } + + if (LoggerUtils.isDiagnosticLogsEnabled()) { + List> operationDetailsList = new ArrayList<>(); + operationExecutionResultList.forEach(result -> { + Map details = new HashMap<>(); + details.put("operation", result.getOperation().getOp() + " path: " + + result.getOperation().getPath()); + details.put("status", result.getStatus().toString()); + details.put("message", result.getMessage()); + operationDetailsList.add(details); + }); + + DiagnosticLog.DiagnosticLogBuilder diagLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( + ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, + ActionExecutionLogConstants.ActionIDs.PROCESS_ACTION_RESPONSE); + diagLogBuilder + .inputParam("executedOperations", operationDetailsList) + .resultMessage("Processed operations for " + getSupportedActionType().getDisplayName() + + " action.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.SUCCESS) + .build(); + LoggerUtils.triggerDiagnosticLogEvent(diagLogBuilder); + } + + if (LOG.isDebugEnabled()) { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + try { + String executionSummary = objectMapper.writeValueAsString(operationExecutionResultList); + LOG.debug(String.format("Processed response for action type: %s. Results of operations: %s", + getSupportedActionType(), executionSummary)); + } catch (JsonProcessingException e) { + LOG.debug("Error occurred while logging operation execution results.", e); + } + } + } + + @Override + public ActionExecutionStatus processErrorResponse(FlowContext flowContext, + ActionExecutionResponseContext responseContext) + throws ActionExecutionResponseProcessorException { + + String errorMessage = responseContext.getActionInvocationResponse().getErrorMessage(); + String errorDescription = responseContext.getActionInvocationResponse().getErrorDescription(); + + if (LOG.isDebugEnabled()) { + LOG.debug("Processing error response from In-Flow Extension. Error: " + errorMessage + + ", Description: " + errorDescription); + } + + return new ErrorStatus(new Error(errorMessage, errorDescription)); + } + + @Override + public ActionExecutionStatus processFailureResponse(FlowContext flowContext, + ActionExecutionResponseContext responseContext) + throws ActionExecutionResponseProcessorException { + + String failureReason = responseContext.getActionInvocationResponse().getFailureReason(); + String failureDescription = responseContext.getActionInvocationResponse().getFailureDescription(); + + if (LOG.isDebugEnabled()) { + LOG.debug("Processing failure response from In-Flow Extension. Reason: " + failureReason + + ", Description: " + failureDescription); + } + + return new FailedStatus(new Failure(failureReason, failureDescription)); + } + + /** + * Inner class representing a successful In-Flow Extension execution result. + */ + public static class InFlowExtensionSuccess implements Success { + + // This class can be extended to include additional success metadata if needed + } + + /** + * Inner class to wrap operation type and value for context updates. + * This allows the executor to know what operation was performed on each field. + */ + public static class OperationValue { + + private final String operation; + private final Object value; + + public OperationValue(String operation, Object value) { + this.operation = operation; + this.value = value; + } + + public String getOperation() { + return operation; + } + + public Object getValue() { + return value; + } + } +} diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/OperationExecutionResult.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/OperationExecutionResult.java new file mode 100644 index 000000000000..2b42c4f6af25 --- /dev/null +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/OperationExecutionResult.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025, 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.action.execution.internal.executor; + +import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; + +/** + * This class represents the result of the execution of an operation. + * It contains the operation that was executed, the status of the execution and a message. + * This is used to summarize the operations performed based on action response. + */ +public class OperationExecutionResult { + + private final PerformableOperation operation; + private final Status status; + private final String message; + + public OperationExecutionResult(PerformableOperation operation, Status status, String message) { + + this.operation = operation; + this.status = status; + this.message = message; + } + + public PerformableOperation getOperation() { + + return operation; + } + + public Status getStatus() { + + return status; + } + + public String getMessage() { + + return message; + } + + /** + * Enum to represent the status of the operation execution. + */ + public enum Status { + SUCCESS, FAILURE + } +} diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutorConfig.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutorConfig.java index 2e215b8f4186..c93967db4de3 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutorConfig.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutorConfig.java @@ -87,6 +87,8 @@ public boolean isExecutionForActionTypeEnabled(ActionType actionType) { return isActionTypeEnabled(ActionTypeConfig.PRE_UPDATE_PROFILE.getActionTypeEnableProperty()); case PRE_ISSUE_ID_TOKEN: return isActionTypeEnabled(ActionTypeConfig.PRE_ISSUE_ID_TOKEN.getActionTypeEnableProperty()); + case IN_FLOW_EXTENSION: + return isActionTypeEnabled(ActionTypeConfig.IN_FLOW_EXTENSION.getActionTypeEnableProperty()); default: return false; } @@ -333,6 +335,8 @@ public String getRetiredUpToVersion(ActionType actionType) { return getVersion(ActionTypeConfig.PRE_UPDATE_PROFILE.getRetiredUpToVersionProperty()); case PRE_ISSUE_ID_TOKEN: return getVersion(ActionTypeConfig.PRE_ISSUE_ID_TOKEN.getRetiredUpToVersionProperty()); + case IN_FLOW_EXTENSION: + return getVersion(ActionTypeConfig.IN_FLOW_EXTENSION.getRetiredUpToVersionProperty()); default: return null; } @@ -417,7 +421,13 @@ private enum ActionTypeConfig { "Actions.Types.PreIssueIdToken.ActionRequest.ExcludedParameters.Parameter", "Actions.Types.PreIssueIdToken.ActionRequest.AllowedHeaders.Header", "Actions.Types.PreIssueIdToken.ActionRequest.AllowedParameters.Parameter", - "Actions.Types.PreIssueIdToken.Version.RetiredUpTo"); + "Actions.Types.PreIssueIdToken.Version.RetiredUpTo"), + IN_FLOW_EXTENSION("Actions.Types.InFlowExtension.Enable", + "Actions.Types.InFlowExtension.ActionRequest.ExcludedHeaders.Header", + "Actions.Types.InFlowExtension.ActionRequest.ExcludedParameters.Parameter", + "Actions.Types.InFlowExtension.ActionRequest.AllowedHeaders.Header", + "Actions.Types.InFlowExtension.ActionRequest.AllowedParameters.Parameter", + "Actions.Types.InFlowExtension.Version.RetiredUpTo"); private final String actionTypeEnableProperty; private final String excludedHeadersProperty; diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java index 6ff616ae12c4..2dfacceff73b 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java @@ -75,7 +75,13 @@ public enum ActionTypes { "PRE_ISSUE_ID_TOKEN", "Pre Issue ID Token", "Configure an extension point for modifying ID token via a custom service.", - Category.PRE_POST); + Category.PRE_POST), + IN_FLOW_EXTENSION( + "inFlowExtension", + "IN_FLOW_EXTENSION", + "In-Flow Extension", + "Configure an extension point within an any flow via a custom service.", + Category.IN_FLOW); private final String pathParam; private final String actionType; diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/util/ActionManagementConfig.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/util/ActionManagementConfig.java index f670ce106e7e..fd4acd913c41 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/util/ActionManagementConfig.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/util/ActionManagementConfig.java @@ -94,7 +94,11 @@ public String getLatestVersion(ActionTypes actionType) throws ActionMgtServerExc return getVersion( ActionTypeConfig.PRE_UPDATE_PROFILE.getLatestVersionProperty(), actionType); case PRE_ISSUE_ID_TOKEN: - return getVersion(ActionTypeConfig.PRE_ISSUE_ID_TOKEN.getLatestVersionProperty(), actionType); + return getVersion( + ActionTypeConfig.PRE_ISSUE_ID_TOKEN.getLatestVersionProperty(), actionType); + case IN_FLOW_EXTENSION: + return getVersion( + ActionTypeConfig.IN_FLOW_EXTENSION.getLatestVersionProperty(), actionType); default: throw new ActionMgtServerException("Unsupported action type: " + actionType); } @@ -140,6 +144,11 @@ public enum ActionTypeConfig { "Actions.Types.PreIssueIdToken.ActionRequest.ExcludedHeaders.Header", "Actions.Types.PreIssueIdToken.ActionRequest.ExcludedParameters.Parameter", "Actions.Types.PreIssueIdToken.Version.Latest" + ), + IN_FLOW_EXTENSION( + "Actions.Types.InFlowExtension.ActionRequest.ExcludedHeaders.Header", + "Actions.Types.InFlowExtension.ActionRequest.ExcludedParameters.Parameter", + "Actions.Types.InFlowExtension.Version.Latest" ); private final String excludedHeadersProperty; From 3abe160c448d2d5999137c4d3d86f0578b4f57cc Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Tue, 17 Feb 2026 08:18:20 +0530 Subject: [PATCH 02/41] Refactor component responsibilities in in-flow extensions. --- .../pom.xml | 14 + .../action/execution/api/model/User.java | 18 +- .../ActionExecutionServiceComponent.java | 24 + ...ActionExecutionServiceComponentHolder.java | 22 + .../executor/HierarchicalPrefixMatcher.java | 340 +++++++++ .../executor/InFlowExtensionEvent.java | 1 - .../executor/InFlowExtensionExecutor.java | 228 ++---- .../InFlowExtensionRequestBuilder.java | 320 +++++--- .../InFlowExtensionResponseProcessor.java | 716 ++++++++++++------ .../pom.xml | 8 +- 10 files changed, 1208 insertions(+), 483 deletions(-) create mode 100644 components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/HierarchicalPrefixMatcher.java diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/pom.xml b/components/action-mgt/org.wso2.carbon.identity.action.execution/pom.xml index 0e2c448fc24c..e2fb17bade38 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/pom.xml +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/pom.xml @@ -49,6 +49,14 @@ org.wso2.carbon.identity.framework org.wso2.carbon.identity.central.log.mgt + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.flow.execution.engine + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.claim.metadata.mgt + com.fasterxml.jackson.core jackson-databind @@ -105,6 +113,12 @@ version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.rule.evaluation.api.*; version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.flow.execution.engine.*; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.flow.mgt.*; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.claim.metadata.mgt.*; + version="${carbon.identity.package.import.version.range}", org.apache.commons.lang; version="${commons-lang.wso2.osgi.version.range}", org.apache.commons.logging; version="${import.package.version.commons.logging}", org.apache.commons.collections; version="${commons-collections.wso2.osgi.version.range}", diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/User.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/User.java index f26378fe0d4e..a365ec13d3fa 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/User.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/User.java @@ -18,9 +18,7 @@ package org.wso2.carbon.identity.action.execution.api.model; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; /** * This class models the User. @@ -30,6 +28,7 @@ public class User { private final String id; private final List claims = new ArrayList<>(); + private final Map userCredentials = new HashMap<>(); private final List groups = new ArrayList<>(); private final List roles = new ArrayList<>(); private Organization organization; @@ -65,6 +64,7 @@ public User(Builder builder) { this.id = builder.id; this.claims.addAll(builder.claims); + this.userCredentials.putAll(builder.userCredentials); this.groups.addAll(builder.groups); this.roles.addAll(builder.roles); this.organization = builder.organization; @@ -84,6 +84,11 @@ public List getClaims() { return Collections.unmodifiableList(claims); } + public Map getUserCredentials() { + + return Collections.unmodifiableMap(userCredentials); + } + public List getGroups() { return Collections.unmodifiableList(groups); @@ -126,6 +131,7 @@ public static class Builder { private final String id; private final List claims = new ArrayList<>(); + private final Map userCredentials = new HashMap<>(); private final List groups = new ArrayList<>(); private final List roles = new ArrayList<>(); private Organization organization; @@ -187,6 +193,12 @@ public Builder accessingOrganization(Organization accessingOrganization) { return this; } + public Builder userCredentials(Map userCredentials) { + + this.userCredentials.putAll(userCredentials); + return this; + } + public User build() { return new User(this); diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java index 253b8118c540..45879b425d0b 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java @@ -42,6 +42,7 @@ import org.wso2.carbon.identity.action.execution.internal.service.impl.ActionInvocationResponseClassFactory; import org.wso2.carbon.identity.action.execution.internal.service.impl.ActionVersioningHandlerFactory; import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; +import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; import org.wso2.carbon.identity.rule.evaluation.api.service.RuleEvaluationService; import org.wso2.carbon.identity.secret.mgt.core.SecretManager; @@ -295,4 +296,27 @@ protected void unsetRealmService(RealmService realmService) { LOG.debug("RealmService unset in ActionExecutionServiceComponentHolder bundle."); } } + + @Reference( + name = "claim.metadata.management.service", + service = ClaimMetadataManagementService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetClaimMetadataManagementService" + ) + protected void setClaimMetadataManagementService(ClaimMetadataManagementService claimMetadataManagementService) { + + LOG.debug("Registering a reference for ClaimMetadataManagementService in the ActionExecutionServiceComponent."); + ActionExecutionServiceComponentHolder.getInstance() + .setClaimMetadataManagementService(claimMetadataManagementService); + } + + protected void unsetClaimMetadataManagementService(ClaimMetadataManagementService claimMetadataManagementService) { + + LOG.debug("Unregistering reference for ClaimMetadataManagementService in the ActionExecutionServiceComponent."); + if (ActionExecutionServiceComponentHolder.getInstance().getClaimMetadataManagementService() + .equals(claimMetadataManagementService)) { + ActionExecutionServiceComponentHolder.getInstance().setClaimMetadataManagementService(null); + } + } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java index 05f2708d7fc9..3ff81c81179b 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java @@ -20,6 +20,7 @@ import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; +import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.rule.evaluation.api.service.RuleEvaluationService; import org.wso2.carbon.identity.secret.mgt.core.SecretManager; import org.wso2.carbon.identity.secret.mgt.core.SecretResolveManager; @@ -38,6 +39,7 @@ public class ActionExecutionServiceComponentHolder { private SecretResolveManager secretResolveManager; private RealmService realmService; private ActionExecutorService actionExecutorService; + private ClaimMetadataManagementService claimMetadataManagementService; private ActionExecutionServiceComponentHolder() { @@ -137,4 +139,24 @@ public void setActionExecutorService(ActionExecutorService actionExecutorService this.actionExecutorService = actionExecutorService; } + + /** + * Get the ClaimMetadataManagementService instance. + * + * @return ClaimMetadataManagementService instance. + */ + public ClaimMetadataManagementService getClaimMetadataManagementService() { + + return claimMetadataManagementService; + } + + /** + * Set the ClaimMetadataManagementService instance. + * + * @param claimMetadataManagementService ClaimMetadataManagementService instance. + */ + public void setClaimMetadataManagementService(ClaimMetadataManagementService claimMetadataManagementService) { + + this.claimMetadataManagementService = claimMetadataManagementService; + } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/HierarchicalPrefixMatcher.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/HierarchicalPrefixMatcher.java new file mode 100644 index 000000000000..fe08afc0fae8 --- /dev/null +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/HierarchicalPrefixMatcher.java @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2025, 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.action.execution.internal.executor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Utility class for hierarchical prefix-based path matching and context area identification. + * + * This class provides utilities for working with the unified prefix hierarchy used in + * In-Flow Extensions for context access control. + * + * Prefix Hierarchy Structure: + *
+ * /user/                           - User context
+ *   /user/claims/{claimURI}        - User claims (keys are system-configured claim URIs)
+ *   /user/userId                   - User's unique identifier
+ *   /user/username                 - Resolved username
+ *   /user/userStoreDomain          - User store domain
+ *   /user/credentials/             - User credentials (system-configured types)
+ *   /user/federatedAssociations/   - Federated IDP associations
+ * 
+ * /properties/{key}                - Flow properties (fully extensible)
+ * 
+ * /input/{key}                     - User input data (runtime extensible)
+ * 
+ * /flow/                           - Flow metadata (READ-ONLY)
+ *   /flow/tenantDomain             - Tenant domain
+ *   /flow/applicationId            - Application ID
+ *   /flow/flowType                 - Flow type (REGISTRATION, etc.)
+ *   /flow/contextIdentifier        - Flow context identifier
+ * 
+ * /graph/                          - Graph state (READ-ONLY)
+ *   /graph/currentNode/            - Current node info
+ *     /graph/currentNode/id        - Current node ID
+ *     /graph/currentNode/type      - Current node type
+ * 
+ */ +public final class HierarchicalPrefixMatcher { + + // Context area prefix constants + public static final String USER_PREFIX = "/user/"; + public static final String USER_CLAIMS_PREFIX = "/user/claims/"; + public static final String USER_CREDENTIALS_PREFIX = "/user/credentials/"; + public static final String USER_FEDERATED_PREFIX = "/user/federatedAssociations/"; + public static final String USER_ID_PATH = "/user/userId"; + public static final String USER_NAME_PATH = "/user/username"; + public static final String USER_STORE_DOMAIN_PATH = "/user/userStoreDomain"; + + public static final String PROPERTIES_PREFIX = "/properties/"; + public static final String INPUT_PREFIX = "/input/"; + + public static final String FLOW_PREFIX = "/flow/"; + public static final String FLOW_TENANT_PATH = "/flow/tenantDomain"; + public static final String FLOW_APP_ID_PATH = "/flow/applicationId"; + public static final String FLOW_TYPE_PATH = "/flow/flowType"; + + public static final String GRAPH_PREFIX = "/graph/"; + public static final String GRAPH_CURRENT_NODE_PREFIX = "/graph/currentNode/"; + + /** + * Default expose configuration — all context areas are exposed. + * Used when no explicit expose configuration is provided by the executor metadata. + */ + public static final List DEFAULT_EXPOSE = Collections.unmodifiableList( + Arrays.asList(USER_PREFIX, PROPERTIES_PREFIX, INPUT_PREFIX, FLOW_PREFIX, GRAPH_PREFIX)); + + // Context area enum for categorization + public enum ContextArea { + USER_CLAIMS(USER_CLAIMS_PREFIX, true), // System-configured keys (claim URIs) + USER_CREDENTIALS(USER_CREDENTIALS_PREFIX, true), // System-configured keys + USER_FEDERATED(USER_FEDERATED_PREFIX, true), // System-configured keys (IDP names) + USER_SCALAR(USER_PREFIX, false), // Scalar user fields (userId, username, etc.) + PROPERTIES(PROPERTIES_PREFIX, false), // Fully extensible + INPUT(INPUT_PREFIX, false), // Runtime extensible + FLOW(FLOW_PREFIX, false), // Read-only scalar values + GRAPH(GRAPH_PREFIX, false); // Read-only graph state + + private final String prefix; + private final boolean hasSystemConfiguredKeys; + + ContextArea(String prefix, boolean hasSystemConfiguredKeys) { + + this.prefix = prefix; + this.hasSystemConfiguredKeys = hasSystemConfiguredKeys; + } + + public String getPrefix() { + + return prefix; + } + + /** + * Whether this context area has system-configured keys that must be validated. + * - USER_CLAIMS: Keys are claim URIs configured in claim dialect + * - USER_CREDENTIALS: Keys are credential types (e.g., "password") + * - USER_FEDERATED: Keys are IDP names + */ + public boolean hasSystemConfiguredKeys() { + + return hasSystemConfiguredKeys; + } + } + + private HierarchicalPrefixMatcher() { + + // Utility class, no instantiation + } + + /** + * Identify the context area for a given path. + * + * @param path The full path (e.g., "/user/claims/http://wso2.org/claims/email") + * @return The ContextArea or null if path doesn't match any known area + */ + public static ContextArea identifyContextArea(String path) { + + if (path == null || path.isEmpty()) { + return null; + } + + // Check specific prefixes first (more specific before less specific) + if (path.startsWith(USER_CLAIMS_PREFIX)) { + return ContextArea.USER_CLAIMS; + } + if (path.startsWith(USER_CREDENTIALS_PREFIX)) { + return ContextArea.USER_CREDENTIALS; + } + if (path.startsWith(USER_FEDERATED_PREFIX)) { + return ContextArea.USER_FEDERATED; + } + if (path.startsWith(USER_PREFIX)) { + return ContextArea.USER_SCALAR; + } + if (path.startsWith(PROPERTIES_PREFIX)) { + return ContextArea.PROPERTIES; + } + if (path.startsWith(INPUT_PREFIX)) { + return ContextArea.INPUT; + } + if (path.startsWith(FLOW_PREFIX)) { + return ContextArea.FLOW; + } + if (path.startsWith(GRAPH_PREFIX)) { + return ContextArea.GRAPH; + } + + return null; + } + + /** + * Extract the key from a path within a context area. + * For map-based areas (claims, properties, input), this extracts the map key. + * + * @param path The full path + * @param prefix The prefix to remove + * @return The extracted key or null if invalid + */ + public static String extractKey(String path, String prefix) { + + if (path == null || prefix == null || !path.startsWith(prefix)) { + return null; + } + + String key = path.substring(prefix.length()); + + // Handle trailing slash + if (key.endsWith("/")) { + key = key.substring(0, key.length() - 1); + } + + // Handle nested paths (e.g., /properties/nested/field -> return "nested") + int slashIndex = key.indexOf('/'); + if (slashIndex > 0) { + // For simple use, return only the first level key + // The full remaining path is available via getSubPath() + key = key.substring(0, slashIndex); + } + + return key.isEmpty() ? null : key; + } + + /** + * Extract the full remaining path after the prefix. + * Unlike extractKey, this returns the complete sub-path including nested paths. + * + * @param path The full path + * @param prefix The prefix to remove + * @return The full remaining path or null if invalid + */ + public static String getSubPath(String path, String prefix) { + + if (path == null || prefix == null || !path.startsWith(prefix)) { + return null; + } + + String subPath = path.substring(prefix.length()); + + // Handle trailing slash + if (subPath.endsWith("/")) { + subPath = subPath.substring(0, subPath.length() - 1); + } + + return subPath.isEmpty() ? null : subPath; + } + + /** + * Build a full path from a prefix and key. + * + * @param prefix The context area prefix + * @param key The key within that area + * @return The full path + */ + public static String buildPath(String prefix, String key) { + + if (prefix == null || key == null) { + return null; + } + + // Ensure prefix ends with / + String normalizedPrefix = prefix.endsWith("/") ? prefix : prefix + "/"; + return normalizedPrefix + key; + } + + /** + * Check if a path is read-only (in /flow/ or /graph/ areas). + * + * @param path The path to check + * @return true if the path is in a read-only area + */ + public static boolean isReadOnly(String path) { + + if (path == null) { + return false; + } + return path.startsWith(FLOW_PREFIX) || path.startsWith(GRAPH_PREFIX); + } + + /** + * Check if a path requires system-configured key validation. + * + * @param path The path to check + * @return true if the path's keys must be validated against system configuration + */ + public static boolean requiresSystemKeyValidation(String path) { + + ContextArea area = identifyContextArea(path); + return area != null && area.hasSystemConfiguredKeys(); + } + + /** + * Check if a path matches any of the given expose prefixes using bidirectional prefix matching. + * A match occurs if the path starts with a prefix (prefix covers path) OR the prefix starts + * with the path (path is a parent of prefix). + * + *

Examples:

+ *
    + *
  • {@code /user/claims/email} matches prefix {@code /user/} → prefix covers path
  • + *
  • {@code /user/} matches prefix {@code /user/claims/email} → path is parent of prefix
  • + *
+ * + * @param path The path to check. + * @param exposePrefixes The list of expose prefixes. + * @return {@code true} if the path matches any expose prefix. + */ + public static boolean matchesAnyExpose(String path, List exposePrefixes) { + + if (path == null || exposePrefixes == null || exposePrefixes.isEmpty()) { + return false; + } + for (String prefix : exposePrefixes) { + if (path.startsWith(prefix) || prefix.startsWith(path)) { + return true; + } + } + return false; + } + + /** + * Map legacy path prefixes to unified hierarchy prefixes. + * This provides backward compatibility with existing configurations. + * + * Legacy mapping: + * - /userInputs/ -> /input/ + * - /user/claims/ -> /user/claims/ (unchanged) + * - /properties/ -> /properties/ (unchanged) + */ + + // TODO: This is a simple mapping for demonstration. In a real implementation, this could be loaded from configuration OR use the all context names unchanged. + private static final Map LEGACY_PREFIX_MAPPING = createLegacyMapping(); + + private static Map createLegacyMapping() { + + Map mapping = new HashMap<>(); + mapping.put("/userInputs/", INPUT_PREFIX); + // Add more legacy mappings as needed + return mapping; + } + + /** + * Normalize a path by converting legacy prefixes to unified prefixes. + * + * @param path The path to normalize + * @return The normalized path using unified prefixes + */ + public static String normalizePath(String path) { + + if (path == null) { + return null; + } + + for (Map.Entry entry : LEGACY_PREFIX_MAPPING.entrySet()) { + if (path.startsWith(entry.getKey())) { + return entry.getValue() + path.substring(entry.getKey().length()); + } + } + + return path; + } +} diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionEvent.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionEvent.java index ee6598cb897e..18582e2e4a19 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionEvent.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionEvent.java @@ -33,7 +33,6 @@ /** * This class models the In-Flow Extension Event. * It represents the event sent to the In-Flow Extension action over the Action Execution Request. - * Contains flow-specific context such as flow type, current node, user inputs, and flow properties. */ public class InFlowExtensionEvent extends Event { diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java index 678211b3b46b..6f4104d08547 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java @@ -18,6 +18,9 @@ package org.wso2.carbon.identity.action.execution.internal.executor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionException; @@ -35,31 +38,46 @@ import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; import org.wso2.carbon.identity.flow.mgt.model.NodeConfig; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; /** * This class is responsible for executing In-Flow Extension actions during flow execution. - * It integrates with the ActionExecutorService to call external services and process their responses. + * It integrates with the {@link ActionExecutorService} to call external services and process + * their responses. + * + * + *

Execution lifecycle:

+ *
    + *
  1. Extract executor metadata: {@code actionId}, {@code expose}, {@code allowedOperations}.
  2. + *
  3. Build the final expose list
  4. + *
  5. Build a minimal {@link FlowContext} containing only three entries: + * the full {@link FlowExecutionContext}, the expose list, and the allowed operations + * JSON. The request builder will use these to construct the filtered request.
  6. + *
  7. Invoke the external service via {@link ActionExecutorService}.
  8. + *
  9. Map the {@link ActionExecutionStatus} to an {@link ExecutorResponse}. + * Context updates are already applied directly to the {@link FlowExecutionContext} + * by the response processor.
  10. + *
*/ public class InFlowExtensionExecutor implements Executor { private static final Log LOG = LogFactory.getLog(InFlowExtensionExecutor.class); private static final String EXECUTOR_NAME = "ExtensionExecutor"; -// private static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; - protected static final String CORRELATION_ID_KEY = "correlationId"; - protected static final String FLOW_USER_INPUT_DATA_KEY = "userInputData"; - protected static final String FLOW_USER_KEY = "flowUser"; - protected static final String FLOW_PROPERTIES_KEY = "flowProperties"; - protected static final String FLOW_CONTEXT_UPDATES_KEY = "contextUpdates"; - protected static final String ACTION_ID_METADATA_KEY = "actionId"; - protected static final String ALLOWED_OPERATIONS_METADATA_KEY = "allowedOperations"; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private static final TypeReference> STRING_LIST_TYPE_REF = + new TypeReference>() { }; + + protected static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; + protected static final String EXPOSE_KEY = "expose"; protected static final String ALLOWED_OPERATIONS_KEY = "allowedOperations"; - protected static final String TENET_DOMAIN_KEY = "tenetDomain"; - protected static final String APPLICATION_ID_KEY = "applicationId"; - protected static final String FLOW_TYPE_KEY = "flowType"; - protected static final String CURRENT_NODE_KEY = "currentNode"; + protected static final String PATH_TYPE_ANNOTATIONS_KEY = "pathTypeAnnotations"; + private static final String ACTION_ID_METADATA_KEY = "actionId"; + private static final String ALLOWED_OPERATIONS_METADATA_KEY = "allowedOperations"; + private static final String EXPOSE_METADATA_KEY = "expose"; @Override public String getName() { @@ -73,41 +91,43 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE ExecutorResponse response = new ExecutorResponse(); try { - // Get the action ID from the executor metadata configuration - String actionId = getActionIdFromMetadata(context); + String actionId = getMetadataValue(context, ACTION_ID_METADATA_KEY); if (actionId == null || actionId.isEmpty()) { LOG.warn("No action ID configured for In-Flow Extension executor. Skipping execution."); response.setResult(ExecutorResult.COMPLETE.name()); return response; } - LOG.debug("Executing In-Flow Extension action with ID: " + actionId); + if (LOG.isDebugEnabled()) { + LOG.debug("Executing In-Flow Extension action with ID: " + actionId); + } + + String exposeJson = getMetadataValue(context, EXPOSE_METADATA_KEY); + List expose = buildExposeList(exposeJson); + + FlowContext flowContext = FlowContext.create() + .add(FLOW_EXECUTION_CONTEXT_KEY, context) + .add(EXPOSE_KEY, expose); - // Build the flow context for ActionExecutorService - FlowContext flowContext = buildFlowContext(context); // passing the whole flow execution context as an object for now + String allowedOpsJson = getMetadataValue(context, ALLOWED_OPERATIONS_METADATA_KEY); + if (allowedOpsJson != null) { + flowContext.add(ALLOWED_OPERATIONS_KEY, allowedOpsJson); + } - // Get the ActionExecutorService ActionExecutorService actionExecutorService = getActionExecutorService(); if (actionExecutorService == null) { throw new FlowEngineException("ActionExecutorService is not available."); } - - // Check if execution is enabled for this action type if (!actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)) { LOG.debug("In-Flow Extension action execution is disabled. Skipping."); response.setResult(ExecutorResult.COMPLETE.name()); return response; } - // Execute the action ActionExecutionStatus executionStatus = actionExecutorService.execute( - ActionType.IN_FLOW_EXTENSION, - actionId, - flowContext, - context.getTenantDomain()); + ActionType.IN_FLOW_EXTENSION, actionId, flowContext, context.getTenantDomain()); - // Process the result - return processExecutionStatus(executionStatus, context); + return mapExecutionStatus(executionStatus); } catch (ActionExecutionException e) { LOG.error("Error executing In-Flow Extension action.", e); @@ -126,50 +146,42 @@ public List getInitiationData() { } @Override - public ExecutorResponse rollback(FlowExecutionContext context) throws FlowEngineException { + public ExecutorResponse rollback(FlowExecutionContext context) { - ExecutorResponse response = new ExecutorResponse(); - response.setResult(ExecutorResult.COMPLETE.name()); - return response; + return null; } /** - * Build the FlowContext for ActionExecutorService from FlowExecutionContext. + * Build the effective expose list from executor metadata. * - * @param context The FlowExecutionContext. - * @return The FlowContext for action execution. + * @param exposeJson JSON array of string prefixes from metadata. + * @return The effective expose list. */ - private FlowContext buildFlowContext(FlowExecutionContext context) { - - FlowContext flowContext = FlowContext.create(); - flowContext.add(CORRELATION_ID_KEY, context.getCorrelationId()); - flowContext.add(FLOW_USER_KEY, context.getFlowUser()); - flowContext.add(FLOW_USER_INPUT_DATA_KEY, context.getUserInputData()); - flowContext.add(FLOW_PROPERTIES_KEY, context.getProperties()); - flowContext.add(TENET_DOMAIN_KEY, context.getTenantDomain()); - flowContext.add(APPLICATION_ID_KEY, context.getApplicationId()); - flowContext.add(FLOW_TYPE_KEY, context.getFlowType()); - flowContext.add(CURRENT_NODE_KEY, context.getCurrentNode()); - - // Add allowed operations from executor metadata - String allowedOperationsJson = getAllowedOperationsFromMetadata(context); - if (allowedOperationsJson != null) { - flowContext.add(ALLOWED_OPERATIONS_KEY, allowedOperationsJson); + private List buildExposeList(String exposeJson) { + + List expose; + if (exposeJson != null && !exposeJson.isEmpty()) { + try { + expose = new ArrayList<>(OBJECT_MAPPER.readValue(exposeJson, STRING_LIST_TYPE_REF)); + } catch (JsonProcessingException e) { + LOG.error("Failed to parse expose configuration: " + e.getMessage(), e); + expose = new ArrayList<>(HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + } + } else { + expose = new ArrayList<>(HierarchicalPrefixMatcher.DEFAULT_EXPOSE); } - return flowContext; + return Collections.unmodifiableList(expose); } /** - * Process the ActionExecutionStatus and map it to ExecutorResponse. + * Map the {@link ActionExecutionStatus} to an {@link ExecutorResponse}. + * Only performs status translation — context updates are handled by the response processor. * * @param executionStatus The status returned by ActionExecutorService. - * @param context The FlowExecutionContext to update with response data. - * @return The ExecutorResponse. + * @return The ExecutorResponse for the flow execution engine. */ - @SuppressWarnings("unchecked") - private ExecutorResponse processExecutionStatus(ActionExecutionStatus executionStatus, - FlowExecutionContext context) { + private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionStatus) { ExecutorResponse response = new ExecutorResponse(); @@ -181,8 +193,6 @@ private ExecutorResponse processExecutionStatus(ActionExecutionStatus executi switch (executionStatus.getStatus()) { case SUCCESS: response.setResult(ExecutorResult.COMPLETE.name()); - // Extract context updates from response and apply them - applyContextUpdates(executionStatus.getResponseContext(), context, response); break; case FAILED: @@ -204,7 +214,6 @@ private ExecutorResponse processExecutionStatus(ActionExecutionStatus executi break; case INCOMPLETE: - // INCOMPLETE status indicates the flow should wait for external input response.setResult(ExecutorResult.USER_INPUT_REQUIRED.name()); break; @@ -216,118 +225,47 @@ private ExecutorResponse processExecutionStatus(ActionExecutionStatus executi return response; } - /** - * Apply context updates from the action response to the FlowExecutionContext. - * - * @param responseContext The response context from action execution. - * @param flowContext The FlowExecutionContext to update. - * @param response The ExecutorResponse to populate with context properties. - */ - @SuppressWarnings("unchecked") - private void applyContextUpdates(Map responseContext, - FlowExecutionContext flowContext, - ExecutorResponse response) { - - if (responseContext == null || responseContext.isEmpty()) { - return; - } - - // Extract context updates from the response - Object contextUpdatesObj = responseContext.get(FLOW_CONTEXT_UPDATES_KEY); - if (contextUpdatesObj instanceof Map) { - Map contextUpdates = (Map) contextUpdatesObj; - - // Apply updates to FlowExecutionContext properties - for (Map.Entry entry : contextUpdates.entrySet()) { - flowContext.setProperty(entry.getKey(), entry.getValue()); - if (LOG.isDebugEnabled()) { - LOG.debug("Applied context update: " + entry.getKey()); - } - } - - // Also set the context properties in the executor response - response.setContextProperty(contextUpdates); - } - } - - /** - * Get the ActionExecutorService instance. - * - * @return The ActionExecutorService. - */ private ActionExecutorService getActionExecutorService() { return ActionExecutionServiceComponentHolder.getInstance().getActionExecutorService(); } /** - * Extract the action ID from the executor metadata configuration. - * The action ID should be configured in the flow JSON as part of the executor's meta object. + * Read a single metadata value from the current node's executor configuration. * - * @param context The FlowExecutionContext containing the current node configuration. - * @return The action ID if configured, null otherwise. - */ - private String getActionIdFromMetadata(FlowExecutionContext context) { - - NodeConfig currentNode = context.getCurrentNode(); - if (currentNode == null) { - LOG.debug("Current node is null, cannot extract action ID from metadata."); - return null; - } - - ExecutorDTO executorConfig = currentNode.getExecutorConfig(); - if (executorConfig == null) { - LOG.debug("Executor config is null, cannot extract action ID from metadata."); - return null; - } - - Map metadata = executorConfig.getMetadata(); - if (metadata == null || metadata.isEmpty()) { - LOG.debug("Executor metadata is null or empty, cannot extract action ID."); - return null; - } - - return metadata.get(ACTION_ID_METADATA_KEY); - } - - /** - * Extract the allowed operations configuration from the executor metadata. - * The allowed operations should be configured in the flow JSON as part of the executor's meta object. - * - * @param context The FlowExecutionContext containing the current node configuration. - * @return The allowed operations JSON string if configured, null otherwise. + * @param context The FlowExecutionContext. + * @param key The metadata key. + * @return The value, or {@code null} if not found. */ - private String getAllowedOperationsFromMetadata(FlowExecutionContext context) { + private String getMetadataValue(FlowExecutionContext context, String key) { NodeConfig currentNode = context.getCurrentNode(); if (currentNode == null) { return null; } - ExecutorDTO executorConfig = currentNode.getExecutorConfig(); if (executorConfig == null) { return null; } - Map metadata = executorConfig.getMetadata(); if (metadata == null || metadata.isEmpty()) { return null; } - - return metadata.get(ALLOWED_OPERATIONS_METADATA_KEY); + return metadata.get(key); } /** * Enum representing the possible results of executor execution. * These values must match the expected status values in the flow execution engine. + * * @see org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus */ public enum ExecutorResult { - COMPLETE, // Executor completed successfully - ERROR, // Server-side error occurred - USER_ERROR, // User-related error (e.g., validation failure) - USER_INPUT_REQUIRED,// Additional user input needed - EXTERNAL_REDIRECTION,// Redirect to external service - RETRY // Retry the current step + COMPLETE, + ERROR, + USER_ERROR, + USER_INPUT_REQUIRED, + EXTERNAL_REDIRECTION, + RETRY } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java index 8a3cb9d3a961..c69df6061b3e 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java @@ -24,31 +24,57 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionRequestBuilderException; -import org.wso2.carbon.identity.action.execution.api.model.*; +import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequest; +import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequestContext; +import org.wso2.carbon.identity.action.execution.api.model.ActionType; +import org.wso2.carbon.identity.action.execution.api.model.AllowedOperation; +import org.wso2.carbon.identity.action.execution.api.model.Application; +import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.action.execution.api.model.Operation; +import org.wso2.carbon.identity.action.execution.api.model.Tenant; +import org.wso2.carbon.identity.action.execution.api.model.User; +import org.wso2.carbon.identity.action.execution.api.model.UserClaim; +import org.wso2.carbon.identity.action.execution.api.model.UserStore; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionRequestBuilder; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +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.mgt.model.NodeConfig; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** - * This class is responsible for building the Action Execution Request for In-Flow Extension actions. - * It converts the FlowExecutionContext into the standard ActionExecutionRequest format. + * This class is responsible for building the {@link ActionExecutionRequest} for In-Flow Extension + * actions. + * + *

Responsibility: expose-based filtering and request construction. + * It receives a {@link FlowContext} containing the full {@link FlowExecutionContext}, the expose + * list, and the allowed-operations JSON. It filters the {@link FlowExecutionContext} data according + * to the expose configuration and maps the result into the {@link InFlowExtensionEvent} model.

*/ public class InFlowExtensionRequestBuilder implements ActionExecutionRequestBuilder { private static final Log LOG = LogFactory.getLog(InFlowExtensionRequestBuilder.class); private static final ObjectMapper objectMapper = new ObjectMapper(); - private static final TypeReference>> OPERATION_LIST_TYPE_REF = + private static final TypeReference>> OPERATION_LIST_TYPE_REF = new TypeReference>>() { }; + /** + * Regex pattern to match path type annotations. + * Matches a trailing bracket expression at the end of a path: + * - "[]" — denotes a string array value. + * - "[field1, field2, field3[]]" — denotes a complex object array with a schema. + * Group 1 captures the content inside the brackets (empty for simple arrays). + */ + private static final Pattern PATH_ANNOTATION_PATTERN = Pattern.compile("\\[([^\\]]*)]$"); + @Override public ActionType getSupportedActionType() { @@ -56,44 +82,52 @@ public ActionType getSupportedActionType() { } @Override + @SuppressWarnings("unchecked") public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContext, ActionExecutionRequestContext actionExecutionContext) throws ActionExecutionRequestBuilderException { -// FlowExecutionContext flowExecutionContext = flowContext.getValue(FLOW_EXECUTION_CONTEXT_KEY, -// FlowExecutionContext.class); - -// if (flowExecutionContext == null) { -// throw new ActionExecutionRequestBuilderException("FlowExecutionContext not found in flow context."); -// } + FlowExecutionContext execCtx = flowContext.getValue( + InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, FlowExecutionContext.class); + if (execCtx == null) { + throw new ActionExecutionRequestBuilderException( + "FlowExecutionContext not found in FlowContext."); + } - InFlowExtensionEvent event = buildEvent(flowContext); + List expose = flowContext.getValue(InFlowExtensionExecutor.EXPOSE_KEY, List.class); + if (expose == null) { + expose = HierarchicalPrefixMatcher.DEFAULT_EXPOSE; + } - // Build allowed operations from metadata configuration - List allowedOperations = buildAllowedOperations(flowContext); + InFlowExtensionEvent event = buildEvent(execCtx, expose); + List allowedOperations = buildAllowedOperations( + flowContext.getValue(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, String.class), + flowContext); return new ActionExecutionRequest.Builder() .actionType(ActionType.IN_FLOW_EXTENSION) - .flowId(flowContext.getValue(InFlowExtensionExecutor.CORRELATION_ID_KEY, String.class)) + .flowId(execCtx.getCorrelationId()) .event(event) .allowedOperations(allowedOperations) .build(); } /** - * Build allowed operations from the flow context. - * Parses the allowedOperations JSON from executor metadata. + * Parse the allowed-operations JSON into typed {@link AllowedOperation} objects. + * Strips path type annotations (e.g., "[]", "[schema]") from paths before creating + * AllowedOperation objects so that {@code OperationComparator} receives clean paths. + * The original annotations are stored in the FlowContext under + * {@link InFlowExtensionExecutor#PATH_TYPE_ANNOTATIONS_KEY} for the response processor. * - * @param flowContext The flow context containing allowed operations configuration. - * @return List of AllowedOperation objects. + * @param allowedOperationsJson The JSON string (maybe null). + * @param flowContext The FlowContext to store path annotations in. + * @return List of AllowedOperation objects with clean (annotation-free) paths. */ - private List buildAllowedOperations(FlowContext flowContext) { - - String allowedOperationsJson = flowContext.getValue( - InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, String.class); + private List buildAllowedOperations(String allowedOperationsJson, + FlowContext flowContext) { if (allowedOperationsJson == null || allowedOperationsJson.isEmpty()) { - LOG.debug("No allowed operations configured in executor metadata. Using empty list."); + LOG.debug("No allowed operations configured. Using empty list."); return Collections.emptyList(); } @@ -101,33 +135,45 @@ private List buildAllowedOperations(FlowContext flowContext) { List> operationConfigs = objectMapper.readValue( allowedOperationsJson, OPERATION_LIST_TYPE_REF); + // Map to store path type annotations: cleanPath -> annotation content. + // "" means simple string array (from []), non-empty means schema (from [schema]). + Map pathTypeAnnotations = new HashMap<>(); + List allowedOperations = new ArrayList<>(); for (Map config : operationConfigs) { - AllowedOperation operation = createAllowedOperationFromConfig(config); + AllowedOperation operation = createAllowedOperationFromConfig(config, pathTypeAnnotations); if (operation != null) { allowedOperations.add(operation); } } - if (LOG.isDebugEnabled()) { - LOG.debug("Built " + allowedOperations.size() + " allowed operations from executor metadata."); + // Store annotations in FlowContext for the response processor. + if (!pathTypeAnnotations.isEmpty()) { + flowContext.add(InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); } + if (LOG.isDebugEnabled()) { + LOG.debug("Built " + allowedOperations.size() + " allowed operations. " + + "Path annotations: " + pathTypeAnnotations.size()); + } return allowedOperations; } catch (JsonProcessingException e) { - LOG.error("Failed to parse allowed operations from executor metadata: " + e.getMessage(), e); + LOG.error("Failed to parse allowed operations: " + e.getMessage(), e); return Collections.emptyList(); } } /** * Create an AllowedOperation from a configuration map. + * Strips path type annotations and records them in the annotations map. * - * @param config The configuration map containing 'op' and 'paths'. - * @return AllowedOperation object or null if invalid. + * @param config The raw configuration map from JSON. + * @param pathTypeAnnotations Map to record extracted annotations (cleanPath → annotation). + * @return The AllowedOperation with clean paths, or null if invalid. */ @SuppressWarnings("unchecked") - private AllowedOperation createAllowedOperationFromConfig(Map config) { + private AllowedOperation createAllowedOperationFromConfig(Map config, + Map pathTypeAnnotations) { String opString = (String) config.get("op"); Object pathsObj = config.get("paths"); @@ -153,127 +199,201 @@ private AllowedOperation createAllowedOperationFromConfig(Map co return null; } + // Strip type annotations from paths and record them. + List cleanPaths = new ArrayList<>(); + for (String rawPath : paths) { + Matcher matcher = PATH_ANNOTATION_PATTERN.matcher(rawPath); + if (matcher.find()) { + String cleanPath = rawPath.substring(0, matcher.start()); + String annotationContent = matcher.group(1); // "" for [], or schema content for [schema] + cleanPaths.add(cleanPath); + pathTypeAnnotations.put(cleanPath, annotationContent); + if (LOG.isDebugEnabled()) { + LOG.debug("Path annotation extracted: " + rawPath + " -> clean: " + cleanPath + + ", annotation: [" + annotationContent + "]"); + } + } else { + cleanPaths.add(rawPath); + } + } + AllowedOperation allowedOperation = new AllowedOperation(); allowedOperation.setOp(operation); - allowedOperation.setPaths(new ArrayList<>(paths)); + allowedOperation.setPaths(cleanPaths); return allowedOperation; } /** - * Build the InFlowExtensionEvent from FlowExecutionContext. + * Build the {@link InFlowExtensionEvent} from the {@link FlowExecutionContext}, + * filtering data according to the expose configuration. * - * @param context The FlowExecutionContext. + * @param context The FlowExecutionContext (full source of truth). + * @param expose The expose prefix list controlling which data is included. * @return The InFlowExtensionEvent. */ - private InFlowExtensionEvent buildEvent(FlowContext context) { + private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List expose) { InFlowExtensionEvent.Builder eventBuilder = new InFlowExtensionEvent.Builder(); - // Set tenant information - String tenantDomain = context.getValue(InFlowExtensionExecutor.TENET_DOMAIN_KEY, String.class); - if (tenantDomain != null) { - int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); - eventBuilder.tenant(new Tenant(String.valueOf(tenantId), tenantDomain)); - } + // TODO: Consider moving to a dynamic approach if the number of fields grows, to avoid hardcoding each field. - // Set application information if available - String applicationId = context.getValue(InFlowExtensionExecutor.APPLICATION_ID_KEY, String.class); - if (applicationId != null) { - eventBuilder.application(new Application(applicationId, null)); + // Tenant + if (isExposed(HierarchicalPrefixMatcher.FLOW_TENANT_PATH, expose)) { + String tenantDomain = context.getTenantDomain(); + if (tenantDomain != null) { + int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); + eventBuilder.tenant(new Tenant(String.valueOf(tenantId), tenantDomain)); + } } - // Set user information from FlowUser - FlowUser flowUser = context.getValue(InFlowExtensionExecutor.FLOW_USER_KEY, FlowUser.class); - if (flowUser != null) { - eventBuilder.user(buildUser(flowUser)); + // Application + if (isExposed(HierarchicalPrefixMatcher.FLOW_APP_ID_PATH, expose)) { + String appId = context.getApplicationId(); + if (appId != null) { + eventBuilder.application(new Application(appId, null)); + } + } - // Set user store if available - String userStoreDomain = flowUser.getUserStoreDomain(); - if (userStoreDomain != null) { - eventBuilder.userStore(new UserStore(userStoreDomain)); + // User + if (isExposed(HierarchicalPrefixMatcher.USER_PREFIX, expose)) { + FlowUser flowUser = context.getFlowUser(); + if (flowUser != null) { + eventBuilder.user(buildUser(flowUser, expose)); + + if (isExposed(HierarchicalPrefixMatcher.USER_STORE_DOMAIN_PATH, expose)) { + String userStoreDomain = flowUser.getUserStoreDomain(); + if (userStoreDomain != null) { + eventBuilder.userStore(new UserStore(userStoreDomain)); + } + } } } - // Set flow-specific information - eventBuilder.flowType(context.getValue(InFlowExtensionExecutor.FLOW_TYPE_KEY, String.class)); + // Flow type + if (isExposed(HierarchicalPrefixMatcher.FLOW_TYPE_PATH, expose)) { + eventBuilder.flowType(context.getFlowType()); + } - // Set current node ID if available - NodeConfig currentNode = context.getValue(InFlowExtensionExecutor.CURRENT_NODE_KEY, NodeConfig.class); - if (currentNode != null) { - eventBuilder.currentNodeId(currentNode.getId()); + // Current node + if (isExposed(HierarchicalPrefixMatcher.GRAPH_CURRENT_NODE_PREFIX, expose)) { + NodeConfig currentNode = context.getCurrentNode(); + if (currentNode != null) { + eventBuilder.currentNodeId(currentNode.getId()); + } } - // Set user inputs - eventBuilder.userInputs(context.getValue(InFlowExtensionExecutor.FLOW_USER_INPUT_DATA_KEY, Map.class)); + // User inputs + if (isExposed(HierarchicalPrefixMatcher.INPUT_PREFIX, expose)) { + Map userInputs = context.getUserInputData(); + if (userInputs != null && !userInputs.isEmpty()) { + eventBuilder.userInputs(filterMap(userInputs, HierarchicalPrefixMatcher.INPUT_PREFIX, expose)); + } + } - // Set flow properties (filtering out sensitive data) - eventBuilder.flowProperties(filterFlowProperties(context.getValue(InFlowExtensionExecutor.FLOW_PROPERTIES_KEY, Map.class))); + // Flow properties + if (isExposed(HierarchicalPrefixMatcher.PROPERTIES_PREFIX, expose)) { + Map properties = context.getProperties(); + if (properties != null && !properties.isEmpty()) { + eventBuilder.flowProperties( + filterMap(properties, HierarchicalPrefixMatcher.PROPERTIES_PREFIX, expose)); + } + } return eventBuilder.build(); } /** - * Build the User model from FlowUser. + * Build the {@link User} model from {@link FlowUser}, filtering by expose config. * - * @param flowUser The FlowUser from flow context. - * @return The User model for the event. + * @param flowUser The FlowUser from the FlowExecutionContext. + * @param expose The expose prefix list. + * @return The filtered User model. */ - private User buildUser(FlowUser flowUser) { + private User buildUser(FlowUser flowUser, List expose) { + + String userId = isExposed(HierarchicalPrefixMatcher.USER_ID_PATH, expose) + ? flowUser.getUserId() : null; + User.Builder userBuilder = new User.Builder(userId); + + if (isExposed(HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX, expose)) { + Map claims = flowUser.getClaims(); + if (claims != null && !claims.isEmpty()) { + List userClaims = new ArrayList<>(); + boolean hasSpecificFilter = hasSpecificSubPathFilter( + expose, HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX); + + for (Map.Entry claim : claims.entrySet()) { + if (!hasSpecificFilter || isExposed( + HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX + claim.getKey(), expose)) { + userClaims.add(new UserClaim(claim.getKey(), claim.getValue())); + } + } + if (!userClaims.isEmpty()) { + userBuilder.claims(userClaims); + } + } + } - User.Builder userBuilder = new User.Builder(flowUser.getUserId()); + if (isExposed(HierarchicalPrefixMatcher.USER_CREDENTIALS_PREFIX, expose)) { + Map credentials = flowUser.getUserCredentials(); + if (credentials != null && !credentials.isEmpty()) { - // Convert FlowUser claims to UserClaims - Map claims = flowUser.getClaims(); - if (claims != null && !claims.isEmpty()) { - List userClaims = new ArrayList<>(); - for (Map.Entry claim : claims.entrySet()) { - userClaims.add(new UserClaim(claim.getKey(), claim.getValue())); + userBuilder.userCredentials(new HashMap<>(credentials)); } - userBuilder.claims(userClaims); } return userBuilder.build(); } /** - * Filter flow properties to remove sensitive data before sending to external service. + * Filter a map to only include entries whose paths are exposed. * - * @param properties The original flow properties. - * @return Filtered properties map. + * @param map The source map. + * @param areaPrefix The area prefix (e.g. "/properties/"). + * @param expose The expose prefix list. + * @param The value type. + * @return A new map containing only exposed entries. */ - private Map filterFlowProperties(Map properties) { + private Map filterMap(Map map, String areaPrefix, List expose) { - if (properties == null) { - return new HashMap<>(); + if (map == null) { + return null; + } + + boolean hasSpecificFilter = hasSpecificSubPathFilter(expose, areaPrefix); + if (!hasSpecificFilter) { + // The entire area is exposed — return a copy. + return new HashMap<>(map); } - Map filtered = new HashMap<>(); - for (Map.Entry entry : properties.entrySet()) { - String key = entry.getKey(); - // Filter out sensitive keys - if (!isSensitiveKey(key)) { - filtered.put(key, entry.getValue()); + Map filtered = new HashMap<>(); + for (Map.Entry entry : map.entrySet()) { + if (isExposed(areaPrefix + entry.getKey(), expose)) { + filtered.put(entry.getKey(), entry.getValue()); } } return filtered; } /** - * Check if a property key is sensitive and should be filtered out. - * - * @param key The property key. - * @return true if the key is sensitive, false otherwise. + * Check if a path is exposed using bidirectional prefix matching. + */ + private boolean isExposed(String path, List expose) { + + return HierarchicalPrefixMatcher.matchesAnyExpose(path, expose); + } + + /** + * Check whether the expose list contains specific sub-paths under a given area prefix, + * indicating that per-key filtering is required rather than exposing the entire area. */ - private boolean isSensitiveKey(String key) { + private boolean hasSpecificSubPathFilter(List expose, String areaPrefix) { - if (key == null) { - return false; + for (String prefix : expose) { + if (prefix.startsWith(areaPrefix) && !prefix.equals(areaPrefix)) { + return true; + } } - String lowerKey = key.toLowerCase(Locale.ENGLISH); - return lowerKey.contains("password") || - lowerKey.contains("secret") || - lowerKey.contains("credential") || - lowerKey.contains("token") || - lowerKey.contains("key"); + return false; } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java index 11de53df8ee8..996e219fb648 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java @@ -36,37 +36,65 @@ import org.wso2.carbon.identity.action.execution.api.model.FailedStatus; import org.wso2.carbon.identity.action.execution.api.model.Failure; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.action.execution.api.model.SuccessStatus; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionResponseProcessor; +import org.wso2.carbon.identity.action.execution.internal.component.ActionExecutionServiceComponentHolder; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; +import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; +import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; +import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; +import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; +import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; import org.wso2.carbon.utils.DiagnosticLog; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; /** * This class is responsible for processing the response from In-Flow Extension actions. - * It processes operations (ADD, REMOVE, REPLACE) on flow context properties and user claims, - * with dynamic path validation based on the allowed operations configured in the request. + * + *

Responsibility: operation processing and applying context updates directly to + * the {@link FlowExecutionContext}. It processes operations (ADD, REMOVE, REPLACE) on flow + * properties, user claims, and user inputs.

+ * + *

The {@code allowedOperations} list (sent to the external service in the request and enforced + * upstream by {@code ActionExecutorServiceImpl}) is the sole mechanism for gating which operations + * are permitted. This processor performs two additional validations:

+ *
    + *
  • Read-only areas: No modifications allowed to {@code /flow/} or {@code /graph/}.
  • + *
  • Claim URI validation: For ADD operations on {@code /user/claims/}, validates that + * the claim URI exists in the system via {@code ClaimMetadataManagementService}.
  • + *
*/ + +// TODO: Consider separating claim validation and read-only path checks into utility classes. + public class InFlowExtensionResponseProcessor implements ActionExecutionResponseProcessor { private static final Log LOG = LogFactory.getLog(InFlowExtensionResponseProcessor.class); - private static final String CONTEXT_UPDATES_KEY = "contextUpdates"; - - // Path prefixes for In-Flow Extension context + + // Path prefixes for In-Flow Extension context (unified hierarchy) private static final String PROPERTIES_PATH_PREFIX = "/properties/"; private static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; - private static final String USER_INPUTS_PATH_PREFIX = "/userInputs/"; - + private static final String USER_INPUTS_PATH_PREFIX = "/input/"; + // Legacy prefix for backward compatibility + private static final String LEGACY_USER_INPUTS_PATH_PREFIX = "/userInputs/"; + private static final char PATH_SEPARATOR = '/'; private static final String LAST_ELEMENT_CHARACTER = "-"; + // Cache for valid claim URIs (per tenant) + private Map> validClaimUrisCache = new HashMap<>(); + @Override public ActionType getSupportedActionType() { @@ -74,162 +102,380 @@ public ActionType getSupportedActionType() { } @Override + @SuppressWarnings("unchecked") public ActionExecutionStatus processSuccessResponse(FlowContext flowContext, ActionExecutionResponseContext responseContext) throws ActionExecutionResponseProcessorException { - Map responseContextMap = new HashMap<>(); - Map contextUpdates = new HashMap<>(); - - // Initialize sub-maps for different context areas - Map propertiesUpdates = new HashMap<>(); - Map userClaimsUpdates = new HashMap<>(); - Map userInputsUpdates = new HashMap<>(); - - List operationExecutionResultList = new ArrayList<>(); - - // Get operations from the response (already filtered by ActionExecutorServiceImpl) - List operationsToPerform = - responseContext.getActionInvocationResponse().getOperations(); - - if (operationsToPerform != null && !operationsToPerform.isEmpty()) { - for (PerformableOperation operation : operationsToPerform) { - OperationExecutionResult result = processOperation(operation, - propertiesUpdates, userClaimsUpdates, userInputsUpdates); - operationExecutionResultList.add(result); - } + FlowExecutionContext execCtx = flowContext.getValue( + InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, FlowExecutionContext.class); + String tenantDomain = execCtx != null ? execCtx.getTenantDomain() : null; + + // Read path type annotations set by the request builder. + // Maps clean paths (e.g., "/properties/riskFactors") to annotation content + // ("" for string arrays, or schema content for complex object arrays). + Map pathTypeAnnotations = flowContext.getValue( + InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); + if (pathTypeAnnotations == null) { + pathTypeAnnotations = Collections.emptyMap(); } - - // Log operation execution results - logOperationExecutionResults(operationExecutionResultList); - - // Build the context updates map from processed operations - // Add properties directly to contextUpdates (not nested under "properties") - for (Map.Entry entry : propertiesUpdates.entrySet()) { - Object value = entry.getValue(); - // If value is OperationValue, extract the actual value - if (value instanceof OperationValue) { - OperationValue opValue = (OperationValue) value; - if (!"REMOVE".equals(opValue.getOperation())) { - contextUpdates.put(entry.getKey(), opValue.getValue()); + + List results = new ArrayList<>(); + + // Get operations from the response (already filtered by ActionExecutorServiceImpl). + List operations = + responseContext.getActionInvocationResponse().getOperations(); + + if (operations != null && !operations.isEmpty()) { + for (PerformableOperation operation : operations) { + // Normalize legacy paths. + // TODO: Remove this normalization logic in future once external services are updated to use unified paths. + String normalizedPath = HierarchicalPrefixMatcher.normalizePath(operation.getPath()); + if (!normalizedPath.equals(operation.getPath())) { + PerformableOperation normalizedOp = new PerformableOperation(); + normalizedOp.setOp(operation.getOp()); + normalizedOp.setPath(normalizedPath); + normalizedOp.setValue(operation.getValue()); + operation = normalizedOp; } - // For REMOVE, we don't add to contextUpdates (removal handled by executor) - } else { - contextUpdates.put(entry.getKey(), value); - } - } - - // User claims and inputs are kept as separate maps for the executor to handle - if (!userClaimsUpdates.isEmpty()) { - contextUpdates.put("userClaims", userClaimsUpdates); - } - if (!userInputsUpdates.isEmpty()) { - contextUpdates.put("userInputs", userInputsUpdates); - } - - if (!contextUpdates.isEmpty()) { - responseContextMap.put(CONTEXT_UPDATES_KEY, contextUpdates); - if (LOG.isDebugEnabled()) { - LOG.debug("Processed " + operationExecutionResultList.size() + - " operations from In-Flow Extension response."); + + results.add(processOperation(operation, execCtx, tenantDomain, pathTypeAnnotations)); } } + logOperationExecutionResults(results); + return new SuccessStatus.Builder() .setSuccess(new InFlowExtensionSuccess()) - .setResponseContext(responseContextMap) + .setResponseContext(Collections.emptyMap()) .build(); } - + + /** - * Process a single operation and apply it to the appropriate context map. + * Process a single operation by validating and applying it directly to the + * {@link FlowExecutionContext}. * - * @param operation The operation to process. - * @param propertiesUpdates Map to store property updates. - * @param userClaimsUpdates Map to store user claim updates. - * @param userInputsUpdates Map to store user input updates. + * @param operation The operation to process. + * @param context The FlowExecutionContext to apply updates to. + * @param tenantDomain The tenant domain for claim validation. + * @param pathTypeAnnotations Map of clean paths to their type annotations from allowed operations. * @return The result of the operation execution. */ private OperationExecutionResult processOperation(PerformableOperation operation, - Map propertiesUpdates, - Map userClaimsUpdates, - Map userInputsUpdates) { - + FlowExecutionContext context, String tenantDomain, + Map pathTypeAnnotations) { + String path = operation.getPath(); - - // Route to appropriate handler based on path prefix + + // Check if operation is on a read-only area. + if (HierarchicalPrefixMatcher.isReadOnly(path)) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Path is in a read-only area. Modifications not allowed: " + path); + } + + // Route to appropriate handler based on path prefix. if (path.startsWith(PROPERTIES_PATH_PREFIX)) { - return handlePropertyOperation(operation, propertiesUpdates); + return handlePropertyOperation(operation, context, pathTypeAnnotations); } else if (path.startsWith(USER_CLAIMS_PATH_PREFIX)) { - return handleUserClaimOperation(operation, userClaimsUpdates); - } else if (path.startsWith(USER_INPUTS_PATH_PREFIX)) { - return handleUserInputOperation(operation, userInputsUpdates); + return handleUserClaimOperation(operation, context, tenantDomain); + } else if (path.startsWith(USER_INPUTS_PATH_PREFIX) || path.startsWith(LEGACY_USER_INPUTS_PATH_PREFIX)) { + return handleUserInputOperation(operation, context); } - + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Unknown path prefix. Supported prefixes: " + PROPERTIES_PATH_PREFIX + - ", " + USER_CLAIMS_PATH_PREFIX + ", " + USER_INPUTS_PATH_PREFIX); + "Unknown path prefix. Supported: " + PROPERTIES_PATH_PREFIX + + ", " + USER_CLAIMS_PATH_PREFIX + ", " + USER_INPUTS_PATH_PREFIX); } - + + // ========================= Property operations ========================= + /** - * Handle operations on flow properties. + * Handle operations on flow properties — apply directly to {@link FlowExecutionContext}. * - * @param operation The operation to perform. - * @param propertiesUpdates Map to store property updates. - * @return The result of the operation execution. + *

Supports terminal paths with nested segments. For example:

+ *
    + *
  • {@code /properties/riskScore} → flat property "riskScore"
  • + *
  • {@code /properties/risk_margin/lower_margin} → nested: sets "lower_margin" inside + * a "risk_margin" Map, auto-creating the parent Map if needed.
  • + *
+ * + *

Value type rules (based on path type annotations from allowed operations):

+ *
    + *
  • No annotation: value must be a string (or convertible via String.valueOf).
  • + *
  • {@code []} annotation: value must be a List of strings.
  • + *
  • {@code [schema]} annotation: value must be a List of objects matching the schema.
  • + *
+ * + *

REPLACE validation: the target path must already exist in the context.

+ * + * @param operation The performable operation. + * @param context The FlowExecutionContext. + * @param pathTypeAnnotations Path type annotations map from request builder. */ + @SuppressWarnings("unchecked") private OperationExecutionResult handlePropertyOperation(PerformableOperation operation, - Map propertiesUpdates) { - - String propertyName = extractNameFromPath(operation.getPath(), PROPERTIES_PATH_PREFIX); - - if (propertyName == null || propertyName.isEmpty()) { + FlowExecutionContext context, Map pathTypeAnnotations) { + + String remaining = extractNameFromPath(operation.getPath(), PROPERTIES_PATH_PREFIX); + + if (remaining == null || remaining.isEmpty()) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Invalid property path. Property name is required."); } - + + // Split into path segments for nested property support. + // e.g., "risk_margin/lower_margin" -> ["risk_margin", "lower_margin"] + String[] segments = remaining.split("/"); + switch (operation.getOp()) { case ADD: + return handlePropertyAdd(operation, context, segments, pathTypeAnnotations); + case REPLACE: - if (operation.getValue() == null) { - return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Value is required for " + operation.getOp() + " operation."); - } - // For ADD and REPLACE, store the value with operation type for later processing - propertiesUpdates.put(propertyName, new OperationValue(operation.getOp().name(), - operation.getValue())); - return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, - "Property " + operation.getOp().name().toLowerCase(Locale.ENGLISH) + " operation queued."); - + return handlePropertyReplace(operation, context, segments, pathTypeAnnotations); + case REMOVE: - // Mark property for removal - propertiesUpdates.put(propertyName, new OperationValue("REMOVE", null)); - return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, - "Property remove operation queued."); - + return handlePropertyRemove(operation, context, segments); + default: return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Unsupported operation: " + operation.getOp()); } } - + + /** + * Handle ADD operation on properties. Auto-creates parent Maps for nested paths. + */ + @SuppressWarnings("unchecked") + private OperationExecutionResult handlePropertyAdd(PerformableOperation operation, + FlowExecutionContext context, String[] segments, + Map pathTypeAnnotations) { + + if (operation.getValue() == null) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Value is required for ADD operation."); + } + + // Coerce value based on path type annotation. + Object coercedValue = coerceValue(operation.getPath(), operation.getValue(), pathTypeAnnotations); + + if (segments.length == 1) { + // Flat property: /properties/riskScore -> setProperty("riskScore", value) + context.setProperty(segments[0], coercedValue); + } else { + // Nested property: /properties/risk_margin/lower_margin + // Auto-create parent Map(s) and set the leaf value. + setNestedProperty(context, segments, coercedValue); + } + + return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, + "Property add applied."); + } + + /** + * Handle REPLACE operation on properties. Validates that the target path exists. + */ + @SuppressWarnings("unchecked") + private OperationExecutionResult handlePropertyReplace(PerformableOperation operation, + FlowExecutionContext context, String[] segments, + Map pathTypeAnnotations) { + + if (operation.getValue() == null) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Value is required for REPLACE operation."); + } + + // Validate that the target path exists before replacing. + if (segments.length == 1) { + if (!context.getProperties().containsKey(segments[0])) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Cannot REPLACE: property '" + segments[0] + "' does not exist in context."); + } + } else { + // For nested paths, validate the full path exists. + Object existing = resolveNestedProperty(context, segments); + if (existing == null) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Cannot REPLACE: nested property path '" + String.join("/", segments) + + "' does not exist in context."); + } + } + + // Coerce value based on path type annotation. + Object coercedValue = coerceValue(operation.getPath(), operation.getValue(), pathTypeAnnotations); + + if (segments.length == 1) { + context.setProperty(segments[0], coercedValue); + } else { + setNestedProperty(context, segments, coercedValue); + } + + return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, + "Property replace applied."); + } + /** - * Handle operations on user claims. + * Handle REMOVE operation on properties. + */ + @SuppressWarnings("unchecked") + private OperationExecutionResult handlePropertyRemove(PerformableOperation operation, + FlowExecutionContext context, String[] segments) { + + if (segments.length == 1) { + context.getProperties().remove(segments[0]); + } else { + // For nested paths, remove the leaf key from the parent Map. + removeNestedProperty(context, segments); + } + + return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, + "Property removed."); + } + + /** + * Set a value at a nested path in the properties map, auto-creating parent Maps. + * e.g., segments=["risk_margin", "lower_margin"], value="20" + * Creates: properties.risk_margin = { lower_margin: "20" } + */ + @SuppressWarnings("unchecked") + private void setNestedProperty(FlowExecutionContext context, String[] segments, Object value) { + + Map current = context.getProperties(); + + // Navigate/create parent maps for all segments except the last. + for (int i = 0; i < segments.length - 1; i++) { + Object child = current.get(segments[i]); + if (child instanceof Map) { + current = (Map) child; + } else { + // Auto-create parent Map. + Map newMap = new HashMap<>(); + current.put(segments[i], newMap); + current = newMap; + } + } + + // Set the leaf value. + current.put(segments[segments.length - 1], value); + } + + /** + * Resolve a nested property path, returning the leaf value or null if any segment is missing. + */ + @SuppressWarnings("unchecked") + private Object resolveNestedProperty(FlowExecutionContext context, String[] segments) { + + Map current = context.getProperties(); + + for (int i = 0; i < segments.length - 1; i++) { + Object child = current.get(segments[i]); + if (child instanceof Map) { + current = (Map) child; + } else { + return null; + } + } + + return current.get(segments[segments.length - 1]); + } + + /** + * Remove a leaf key from a nested property path. + */ + @SuppressWarnings("unchecked") + private void removeNestedProperty(FlowExecutionContext context, String[] segments) { + + Map current = context.getProperties(); + + for (int i = 0; i < segments.length - 1; i++) { + Object child = current.get(segments[i]); + if (child instanceof Map) { + current = (Map) child; + } else { + return; // Parent doesn't exist, nothing to remove. + } + } + + current.remove(segments[segments.length - 1]); + } + + /** + * Coerce a value based on path type annotations. * - * @param operation The operation to perform. - * @param userClaimsUpdates Map to store user claim updates. - * @return The result of the operation execution. + *
    + *
  • No annotation: value is coerced to String via String.valueOf().
  • + *
  • "" annotation (from []): value is expected to be a List; each element is coerced to String.
  • + *
  • Non-empty annotation (from [schema]): value is expected to be a List of objects; + * passed through as-is (schema validation can be added later).
  • + *
+ * + * @param path The operation path. + * @param value The raw value from the operation. + * @param pathTypeAnnotations Path type annotations map. + * @return The coerced value. + */ + @SuppressWarnings("unchecked") + private Object coerceValue(String path, Object value, + Map pathTypeAnnotations) { + + String annotation = pathTypeAnnotations.get(path); + + if (annotation == null) { + // No annotation: coerce to String. + return String.valueOf(value); + } + + if (annotation.isEmpty()) { + // [] annotation: expect a List of strings. + if (value instanceof List) { + List rawList = (List) value; + List stringList = new ArrayList<>(); + for (Object item : rawList) { + stringList.add(String.valueOf(item)); + } + return stringList; + } + // Single value — wrap in a list. + List singleList = new ArrayList<>(); + singleList.add(String.valueOf(value)); + return singleList; + } + + // [schema] annotation: pass through as-is for now. + // TODO: Add schema validation for complex object arrays. + return value; + } + + // ========================= User claim operations ========================= + + /** + * Handle operations on user claims — validate and apply directly to {@link FlowUser}. */ private OperationExecutionResult handleUserClaimOperation(PerformableOperation operation, - Map userClaimsUpdates) { - + FlowExecutionContext context, String tenantDomain) { + String claimUri = extractNameFromPath(operation.getPath(), USER_CLAIMS_PATH_PREFIX); - + if (claimUri == null || claimUri.isEmpty()) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Invalid claim path. Claim URI is required."); } - + + // For ADD operations, validate that the claim URI exists in system configuration. + if (operation.getOp() == Operation.ADD) { + if (!isValidClaimUri(claimUri, tenantDomain)) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Invalid claim URI. Claim must be configured in the system: " + claimUri); + } + } + + FlowUser user = context.getFlowUser(); + if (user == null) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "No FlowUser in FlowExecutionContext. Cannot apply user claim operation."); + } + switch (operation.getOp()) { case ADD: case REPLACE: @@ -237,39 +483,36 @@ private OperationExecutionResult handleUserClaimOperation(PerformableOperation o return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Value is required for " + operation.getOp() + " operation."); } - userClaimsUpdates.put(claimUri, new OperationValue(operation.getOp().name(), - operation.getValue())); + user.addClaim(claimUri, String.valueOf(operation.getValue())); return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, - "User claim " + operation.getOp().name().toLowerCase(Locale.ENGLISH) + " operation queued."); - + "User claim " + operation.getOp().name().toLowerCase(Locale.ENGLISH) + " applied."); + case REMOVE: - userClaimsUpdates.put(claimUri, new OperationValue("REMOVE", null)); + if (user.getClaims() != null) { + user.getClaims().remove(claimUri); + } return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, - "User claim remove operation queued."); - + "User claim removed."); + default: return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Unsupported operation: " + operation.getOp()); } } - + /** - * Handle operations on user inputs. - * - * @param operation The operation to perform. - * @param userInputsUpdates Map to store user input updates. - * @return The result of the operation execution. + * Handle operations on user inputs — apply directly to {@link FlowExecutionContext}. */ private OperationExecutionResult handleUserInputOperation(PerformableOperation operation, - Map userInputsUpdates) { - + FlowExecutionContext context) { + String inputName = extractNameFromPath(operation.getPath(), USER_INPUTS_PATH_PREFIX); - + if (inputName == null || inputName.isEmpty()) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Invalid user input path. Input name is required."); } - + switch (operation.getOp()) { case ADD: case REPLACE: @@ -277,60 +520,100 @@ private OperationExecutionResult handleUserInputOperation(PerformableOperation o return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Value is required for " + operation.getOp() + " operation."); } - userInputsUpdates.put(inputName, new OperationValue(operation.getOp().name(), - operation.getValue())); + context.addUserInputData(inputName, String.valueOf(operation.getValue())); return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, - "User input " + operation.getOp().name().toLowerCase(Locale.ENGLISH) + " operation queued."); - + "User input " + operation.getOp().name().toLowerCase(Locale.ENGLISH) + " applied."); + case REMOVE: - userInputsUpdates.put(inputName, new OperationValue("REMOVE", null)); + context.getUserInputData().remove(inputName); return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, - "User input remove operation queued."); - + "User input removed."); + default: return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Unsupported operation: " + operation.getOp()); } } - + + /** + * Validate if a claim URI exists in system configuration. + */ + private boolean isValidClaimUri(String claimUri, String tenantDomain) { + + if (claimUri == null || claimUri.isEmpty()) { + return false; + } + return getValidClaimUris(tenantDomain).contains(claimUri); + } + + /** + * Get valid claim URIs for a tenant, loading from ClaimMetadataManagementService if not cached. + */ + private Set getValidClaimUris(String tenantDomain) { + + String cacheKey = tenantDomain != null ? tenantDomain : "carbon.super"; + + if (validClaimUrisCache.containsKey(cacheKey)) { + return validClaimUrisCache.get(cacheKey); + } + + Set validUris = new HashSet<>(); + try { + ClaimMetadataManagementService claimService = getClaimMetadataManagementService(); + if (claimService != null) { + List localClaims = claimService.getLocalClaims(tenantDomain); + if (localClaims != null) { + for (LocalClaim claim : localClaims) { + validUris.add(claim.getClaimURI()); + } + } + } + } catch (ClaimMetadataException e) { + LOG.error("Failed to load claim URIs for tenant: " + tenantDomain, e); + } + + validClaimUrisCache.put(cacheKey, validUris); + + if (LOG.isDebugEnabled()) { + LOG.debug("Loaded " + validUris.size() + " valid claim URIs for tenant: " + cacheKey); + } + return validUris; + } + + private ClaimMetadataManagementService getClaimMetadataManagementService() { + + return ActionExecutionServiceComponentHolder.getInstance().getClaimMetadataManagementService(); + } + /** * Extract the name/key from the operation path after the prefix. - * - * @param path The full operation path. - * @param prefix The path prefix to remove. - * @return The extracted name, or null if invalid. */ private String extractNameFromPath(String path, String prefix) { - + if (path == null || !path.startsWith(prefix)) { return null; } - + String remaining = path.substring(prefix.length()); - - // Handle trailing slash + if (remaining.endsWith("/")) { remaining = remaining.substring(0, remaining.length() - 1); } - - // Handle array index notation (e.g., /properties/items/0) + + // Handle array index notation (e.g., /properties/items/0). int lastSlash = remaining.lastIndexOf(PATH_SEPARATOR); if (lastSlash > 0) { String possibleIndex = remaining.substring(lastSlash + 1); if (LAST_ELEMENT_CHARACTER.equals(possibleIndex) || isNumeric(possibleIndex)) { - // Return path up to the index return remaining.substring(0, lastSlash); } } - + return remaining; } - - /** - * Check if a string is numeric. - */ + private boolean isNumeric(String str) { - + try { Integer.parseInt(str); return true; @@ -338,55 +621,6 @@ private boolean isNumeric(String str) { return false; } } - - /** - * Log operation execution results for diagnostics and debugging. - * - * @param operationExecutionResultList List of operation execution results. - */ - private void logOperationExecutionResults(List operationExecutionResultList) { - - if (operationExecutionResultList.isEmpty()) { - return; - } - - if (LoggerUtils.isDiagnosticLogsEnabled()) { - List> operationDetailsList = new ArrayList<>(); - operationExecutionResultList.forEach(result -> { - Map details = new HashMap<>(); - details.put("operation", result.getOperation().getOp() + " path: " + - result.getOperation().getPath()); - details.put("status", result.getStatus().toString()); - details.put("message", result.getMessage()); - operationDetailsList.add(details); - }); - - DiagnosticLog.DiagnosticLogBuilder diagLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( - ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, - ActionExecutionLogConstants.ActionIDs.PROCESS_ACTION_RESPONSE); - diagLogBuilder - .inputParam("executedOperations", operationDetailsList) - .resultMessage("Processed operations for " + getSupportedActionType().getDisplayName() + - " action.") - .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) - .resultStatus(DiagnosticLog.ResultStatus.SUCCESS) - .build(); - LoggerUtils.triggerDiagnosticLogEvent(diagLogBuilder); - } - - if (LOG.isDebugEnabled()) { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); - try { - String executionSummary = objectMapper.writeValueAsString(operationExecutionResultList); - LOG.debug(String.format("Processed response for action type: %s. Results of operations: %s", - getSupportedActionType(), executionSummary)); - } catch (JsonProcessingException e) { - LOG.debug("Error occurred while logging operation execution results.", e); - } - } - } @Override public ActionExecutionStatus processErrorResponse(FlowContext flowContext, @@ -400,7 +634,6 @@ public ActionExecutionStatus processErrorResponse(FlowContext flowContext LOG.debug("Processing error response from In-Flow Extension. Error: " + errorMessage + ", Description: " + errorDescription); } - return new ErrorStatus(new Error(errorMessage, errorDescription)); } @@ -416,38 +649,61 @@ public ActionExecutionStatus processFailureResponse(FlowContext flowCon LOG.debug("Processing failure response from In-Flow Extension. Reason: " + failureReason + ", Description: " + failureDescription); } - return new FailedStatus(new Failure(failureReason, failureDescription)); } /** - * Inner class representing a successful In-Flow Extension execution result. + * Log operation execution results for diagnostics and debugging. */ - public static class InFlowExtensionSuccess implements Success { + private void logOperationExecutionResults(List results) { + + if (results.isEmpty()) { + return; + } - // This class can be extended to include additional success metadata if needed + if (LoggerUtils.isDiagnosticLogsEnabled()) { + List> operationDetailsList = new ArrayList<>(); + results.forEach(result -> { + Map details = new HashMap<>(); + details.put("operation", result.getOperation().getOp() + " path: " + + result.getOperation().getPath()); + details.put("status", result.getStatus().toString()); + details.put("message", result.getMessage()); + operationDetailsList.add(details); + }); + + DiagnosticLog.DiagnosticLogBuilder diagLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( + ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, + ActionExecutionLogConstants.ActionIDs.PROCESS_ACTION_RESPONSE); + diagLogBuilder + .inputParam("executedOperations", operationDetailsList) + .resultMessage("Processed operations for " + getSupportedActionType().getDisplayName() + + " action.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.SUCCESS) + .build(); + LoggerUtils.triggerDiagnosticLogEvent(diagLogBuilder); + } + + if (LOG.isDebugEnabled()) { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + try { + String summary = mapper.writeValueAsString(results); + LOG.debug(String.format("Processed response for action type: %s. Results: %s", + getSupportedActionType(), summary)); + } catch (JsonProcessingException e) { + LOG.debug("Error occurred while logging operation execution results.", e); + } + } } - + /** - * Inner class to wrap operation type and value for context updates. - * This allows the executor to know what operation was performed on each field. + * Inner class representing a successful In-Flow Extension execution result. */ - public static class OperationValue { - - private final String operation; - private final Object value; - - public OperationValue(String operation, Object value) { - this.operation = operation; - this.value = value; - } - - public String getOperation() { - return operation; - } - - public Object getValue() { - return value; - } + public static class InFlowExtensionSuccess implements Success { + + // Marker class for successful execution. } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml index efb4e7bd98ae..40f3f77e9e28 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml @@ -52,10 +52,10 @@ org.ops4j.pax.logging pax-logging-api - - org.wso2.carbon.identity.framework - org.wso2.carbon.identity.application.authentication.framework - + + + + com.fasterxml.jackson.core jackson-databind From 8ac739b7d745bba9dcb47e7bffa19c6a71ca9318 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Fri, 20 Feb 2026 18:38:49 +0530 Subject: [PATCH 03/41] Implemented unit tests --- .../action/execution/api/model/User.java | 6 +- .../executor/HierarchicalPrefixMatcher.java | 7 +- .../executor/InFlowExtensionExecutor.java | 66 +- .../InFlowExtensionRequestBuilder.java | 47 +- .../InFlowExtensionResponseProcessor.java | 3 +- .../HierarchicalPrefixMatcherTest.java | 344 +++++++++ .../executor/InFlowExtensionExecutorTest.java | 563 +++++++++++++++ .../InFlowExtensionRequestBuilderTest.java | 513 +++++++++++++ .../InFlowExtensionResponseProcessorTest.java | 680 ++++++++++++++++++ .../src/test/resources/testng.xml | 8 + .../engine/core/FlowExecutionEngine.java | 16 +- 11 files changed, 2233 insertions(+), 20 deletions(-) create mode 100644 components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/HierarchicalPrefixMatcherTest.java create mode 100644 components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionExecutorTest.java create mode 100644 components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionRequestBuilderTest.java create mode 100644 components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionResponseProcessorTest.java diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/User.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/User.java index a365ec13d3fa..fe37ca5d3513 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/User.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/User.java @@ -18,7 +18,11 @@ package org.wso2.carbon.identity.action.execution.api.model; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * This class models the User. diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/HierarchicalPrefixMatcher.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/HierarchicalPrefixMatcher.java index fe08afc0fae8..ddf3b3ebb7c9 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/HierarchicalPrefixMatcher.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/HierarchicalPrefixMatcher.java @@ -85,7 +85,9 @@ public final class HierarchicalPrefixMatcher { public static final List DEFAULT_EXPOSE = Collections.unmodifiableList( Arrays.asList(USER_PREFIX, PROPERTIES_PREFIX, INPUT_PREFIX, FLOW_PREFIX, GRAPH_PREFIX)); - // Context area enum for categorization + /** + * Context area enum for categorization. + */ public enum ContextArea { USER_CLAIMS(USER_CLAIMS_PREFIX, true), // System-configured keys (claim URIs) USER_CREDENTIALS(USER_CREDENTIALS_PREFIX, true), // System-configured keys @@ -306,7 +308,8 @@ public static boolean matchesAnyExpose(String path, List exposePrefixes) * - /properties/ -> /properties/ (unchanged) */ - // TODO: This is a simple mapping for demonstration. In a real implementation, this could be loaded from configuration OR use the all context names unchanged. + // TODO: This is a simple mapping for demonstration. In a real implementation, + // this could be loaded from configuration OR use all context names unchanged. private static final Map LEGACY_PREFIX_MAPPING = createLegacyMapping(); private static Map createLegacyMapping() { diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java index 6f4104d08547..f40e7b53066b 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java @@ -71,10 +71,10 @@ public class InFlowExtensionExecutor implements Executor { private static final TypeReference> STRING_LIST_TYPE_REF = new TypeReference>() { }; - protected static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; - protected static final String EXPOSE_KEY = "expose"; - protected static final String ALLOWED_OPERATIONS_KEY = "allowedOperations"; - protected static final String PATH_TYPE_ANNOTATIONS_KEY = "pathTypeAnnotations"; + public static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; + public static final String EXPOSE_KEY = "expose"; + public static final String ALLOWED_OPERATIONS_KEY = "allowedOperations"; + public static final String PATH_TYPE_ANNOTATIONS_KEY = "pathTypeAnnotations"; private static final String ACTION_ID_METADATA_KEY = "actionId"; private static final String ALLOWED_OPERATIONS_METADATA_KEY = "allowedOperations"; private static final String EXPOSE_METADATA_KEY = "expose"; @@ -131,10 +131,8 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE } catch (ActionExecutionException e) { LOG.error("Error executing In-Flow Extension action.", e); - response.setResult(ExecutorResult.ERROR.name()); - response.setErrorMessage("Action execution failed"); - response.setErrorDescription(e.getMessage()); - response.setThrowable(e); + response.setResult(ExecutorResult.RETRY.name()); + response.setErrorMessage("An error occurred while processing the extension. Please try again."); return response; } } @@ -196,20 +194,18 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt break; case FAILED: - response.setResult(ExecutorResult.USER_ERROR.name()); + response.setResult(ExecutorResult.RETRY.name()); Failure failure = (Failure) executionStatus.getResponse(); if (failure != null) { - response.setErrorMessage(failure.getFailureReason()); - response.setErrorDescription(failure.getFailureDescription()); + response.setErrorMessage(buildUserFacingErrorMessage(failure)); } break; case ERROR: - response.setResult(ExecutorResult.ERROR.name()); + response.setResult(ExecutorResult.RETRY.name()); Error error = (Error) executionStatus.getResponse(); if (error != null) { - response.setErrorMessage(error.getErrorMessage()); - response.setErrorDescription(error.getErrorDescription()); + response.setErrorMessage(buildUserFacingErrorMessage(error)); } break; @@ -225,6 +221,48 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt return response; } + /** + * Build a user-facing error message from the failure details returned by the external service. + * Prefers the failureDescription (human-readable). Falls back to failureReason if description is absent. + * + * @param failure The failure object from the external service. + * @return A display-ready error message string. + */ + private String buildUserFacingErrorMessage(Failure failure) { + + String description = failure.getFailureDescription(); + String reason = failure.getFailureReason(); + + if (description != null && !description.isEmpty()) { + return description; + } + if (reason != null && !reason.isEmpty()) { + return reason; + } + return "The operation could not be completed due to an external service failure."; + } + + /** + * Build a user-facing error message from the error details returned by the external service. + * Prefers the errorDescription (human-readable). Falls back to errorMessage if description is absent. + * + * @param error The error object from the external service. + * @return A display-ready error message string. + */ + private String buildUserFacingErrorMessage(Error error) { + + String description = error.getErrorDescription(); + String message = error.getErrorMessage(); + + if (description != null && !description.isEmpty()) { + return description; + } + if (message != null && !message.isEmpty()) { + return message; + } + return "An unexpected error occurred in the external service."; + } + private ActionExecutorService getActionExecutorService() { return ActionExecutionServiceComponentHolder.getInstance().getActionExecutorService(); diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java index c69df6061b3e..b8c3b0d8c3f3 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java @@ -99,11 +99,17 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex expose = HierarchicalPrefixMatcher.DEFAULT_EXPOSE; } - InFlowExtensionEvent event = buildEvent(execCtx, expose); + // Build allowed operations first so REPLACE paths can augment the expose list. List allowedOperations = buildAllowedOperations( flowContext.getValue(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, String.class), flowContext); + // Augment expose with REPLACE paths so the external service can see the current values + // it may replace. + expose = augmentExposeWithReplacePaths(expose, allowedOperations); + + InFlowExtensionEvent event = buildEvent(execCtx, expose); + return new ActionExecutionRequest.Builder() .actionType(ActionType.IN_FLOW_EXTENSION) .flowId(execCtx.getCorrelationId()) @@ -396,4 +402,43 @@ private boolean hasSpecificSubPathFilter(List expose, String areaPrefix) } return false; } + + /** + * Augment the expose list with paths from REPLACE operations. + * REPLACE operations require the current values to be visible to the external service, + * so their paths must be exposed even if not explicitly in the expose configuration. + * + * @param expose The current expose list. + * @param allowedOperations The parsed allowed operations. + * @return The augmented expose list (new list if modified, original if unchanged). + */ + private List augmentExposeWithReplacePaths(List expose, + List allowedOperations) { + + if (allowedOperations == null || allowedOperations.isEmpty()) { + return expose; + } + + List replacePaths = new ArrayList<>(); + for (AllowedOperation op : allowedOperations) { + if (op.getOp() == Operation.REPLACE && op.getPaths() != null) { + for (String path : op.getPaths()) { + if (!isExposed(path, expose)) { + replacePaths.add(path); + } + } + } + } + + if (replacePaths.isEmpty()) { + return expose; + } + + List augmented = new ArrayList<>(expose); + augmented.addAll(replacePaths); + if (LOG.isDebugEnabled()) { + LOG.debug("Augmented expose list with REPLACE paths: " + replacePaths); + } + return augmented; + } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java index 996e219fb648..67a7544fb519 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java @@ -129,7 +129,8 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon if (operations != null && !operations.isEmpty()) { for (PerformableOperation operation : operations) { // Normalize legacy paths. - // TODO: Remove this normalization logic in future once external services are updated to use unified paths. + // TODO: Remove this normalization logic in future once external services are updated + // to use unified paths. String normalizedPath = HierarchicalPrefixMatcher.normalizePath(operation.getPath()); if (!normalizedPath.equals(operation.getPath())) { PerformableOperation normalizedOp = new PerformableOperation(); diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/HierarchicalPrefixMatcherTest.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/HierarchicalPrefixMatcherTest.java new file mode 100644 index 000000000000..3eebd6d886df --- /dev/null +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/HierarchicalPrefixMatcherTest.java @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2025, 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.action.execution.executor; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.action.execution.internal.executor.HierarchicalPrefixMatcher; +import org.wso2.carbon.identity.action.execution.internal.executor.HierarchicalPrefixMatcher.ContextArea; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +/** + * Unit tests for {@link HierarchicalPrefixMatcher}. + */ +public class HierarchicalPrefixMatcherTest { + + // ========================= identifyContextArea ========================= + + @DataProvider(name = "contextAreaPaths") + public Object[][] contextAreaPaths() { + + return new Object[][] { + { "/user/claims/http://wso2.org/claims/email", ContextArea.USER_CLAIMS }, + { "/user/claims/", ContextArea.USER_CLAIMS }, + { "/user/credentials/password", ContextArea.USER_CREDENTIALS }, + { "/user/credentials/", ContextArea.USER_CREDENTIALS }, + { "/user/federatedAssociations/google", ContextArea.USER_FEDERATED }, + { "/user/federatedAssociations/", ContextArea.USER_FEDERATED }, + { "/user/userId", ContextArea.USER_SCALAR }, + { "/user/username", ContextArea.USER_SCALAR }, + { "/user/userStoreDomain", ContextArea.USER_SCALAR }, + { "/properties/riskScore", ContextArea.PROPERTIES }, + { "/properties/", ContextArea.PROPERTIES }, + { "/input/someField", ContextArea.INPUT }, + { "/input/", ContextArea.INPUT }, + { "/flow/tenantDomain", ContextArea.FLOW }, + { "/flow/applicationId", ContextArea.FLOW }, + { "/flow/", ContextArea.FLOW }, + { "/graph/currentNode/id", ContextArea.GRAPH }, + { "/graph/", ContextArea.GRAPH }, + }; + } + + @Test(dataProvider = "contextAreaPaths") + public void testIdentifyContextArea(String path, ContextArea expected) { + + assertEquals(HierarchicalPrefixMatcher.identifyContextArea(path), expected); + } + + @DataProvider(name = "unknownPaths") + public Object[][] unknownPaths() { + + return new Object[][] { + { null }, + { "" }, + { "/unknown/path" }, + { "noSlash" }, + { "/other/" }, + }; + } + + @Test(dataProvider = "unknownPaths") + public void testIdentifyContextAreaReturnsNullForUnknownPaths(String path) { + + assertNull(HierarchicalPrefixMatcher.identifyContextArea(path)); + } + + // ========================= extractKey ========================= + + @DataProvider(name = "extractKeyData") + public Object[][] extractKeyData() { + + return new Object[][] { + { "/properties/riskScore", "/properties/", "riskScore" }, + { "/properties/nested/field", "/properties/", "nested" }, + { "/user/claims/http://wso2.org/claims/email", "/user/claims/", "http:" }, + { "/input/fieldName", "/input/", "fieldName" }, + }; + } + + @Test(dataProvider = "extractKeyData") + public void testExtractKey(String path, String prefix, String expected) { + + assertEquals(HierarchicalPrefixMatcher.extractKey(path, prefix), expected); + } + + @DataProvider(name = "extractKeyNullCases") + public Object[][] extractKeyNullCases() { + + return new Object[][] { + { null, "/properties/" }, + { "/properties/", "/properties/" }, + { "/user/claims/email", "/properties/" }, + }; + } + + @Test(dataProvider = "extractKeyNullCases") + public void testExtractKeyReturnsNullForInvalidInput(String path, String prefix) { + + assertNull(HierarchicalPrefixMatcher.extractKey(path, prefix)); + } + + // ========================= getSubPath ========================= + + @DataProvider(name = "subPathData") + public Object[][] subPathData() { + + return new Object[][] { + { "/properties/nested/field", "/properties/", "nested/field" }, + { "/properties/riskScore", "/properties/", "riskScore" }, + { "/user/claims/http://wso2.org/claims/email", "/user/claims/", "http://wso2.org/claims/email" }, + }; + } + + @Test(dataProvider = "subPathData") + public void testGetSubPath(String path, String prefix, String expected) { + + assertEquals(HierarchicalPrefixMatcher.getSubPath(path, prefix), expected); + } + + @Test + public void testGetSubPathReturnsNullForNullPath() { + + assertNull(HierarchicalPrefixMatcher.getSubPath(null, "/properties/")); + } + + @Test + public void testGetSubPathReturnsNullForMismatchedPrefix() { + + assertNull(HierarchicalPrefixMatcher.getSubPath("/user/claims/x", "/properties/")); + } + + // ========================= buildPath ========================= + + @Test + public void testBuildPath() { + + assertEquals(HierarchicalPrefixMatcher.buildPath("/properties/", "riskScore"), + "/properties/riskScore"); + } + + @Test + public void testBuildPathWithTrailingSlash() { + + assertEquals(HierarchicalPrefixMatcher.buildPath("/user/claims/", "http://wso2.org/claims/email"), + "/user/claims/http://wso2.org/claims/email"); + } + + @Test + public void testBuildPathReturnsNullForNullInputs() { + + assertNull(HierarchicalPrefixMatcher.buildPath(null, "key")); + assertNull(HierarchicalPrefixMatcher.buildPath("/properties/", null)); + } + + // ========================= isReadOnly ========================= + + @DataProvider(name = "readOnlyPaths") + public Object[][] readOnlyPaths() { + + return new Object[][] { + { "/flow/tenantDomain", true }, + { "/flow/applicationId", true }, + { "/flow/", true }, + { "/graph/currentNode/id", true }, + { "/graph/", true }, + { "/properties/riskScore", false }, + { "/user/claims/email", false }, + { "/input/field", false }, + { null, false }, + }; + } + + @Test(dataProvider = "readOnlyPaths") + public void testIsReadOnly(String path, boolean expected) { + + assertEquals(HierarchicalPrefixMatcher.isReadOnly(path), expected); + } + + // ========================= requiresSystemKeyValidation ========================= + + @DataProvider(name = "systemKeyPaths") + public Object[][] systemKeyPaths() { + + return new Object[][] { + { "/user/claims/http://wso2.org/claims/email", true }, + { "/user/credentials/password", true }, + { "/user/federatedAssociations/google", true }, + { "/properties/score", false }, + { "/input/field", false }, + { "/flow/tenantDomain", false }, + { "/user/userId", false }, + }; + } + + @Test(dataProvider = "systemKeyPaths") + public void testRequiresSystemKeyValidation(String path, boolean expected) { + + assertEquals(HierarchicalPrefixMatcher.requiresSystemKeyValidation(path), expected); + } + + // ========================= matchesAnyExpose ========================= + + @Test + public void testMatchesAnyExposePathStartsWithPrefix() { + + List expose = Arrays.asList("/user/", "/properties/"); + assertTrue(HierarchicalPrefixMatcher.matchesAnyExpose("/user/claims/email", expose)); + assertTrue(HierarchicalPrefixMatcher.matchesAnyExpose("/properties/riskScore", expose)); + } + + @Test + public void testMatchesAnyExposePrefixStartsWithPath() { + + // Bidirectional: path is parent of a prefix in the list. + List expose = Arrays.asList("/user/claims/http://wso2.org/claims/email"); + assertTrue(HierarchicalPrefixMatcher.matchesAnyExpose("/user/", expose)); + assertTrue(HierarchicalPrefixMatcher.matchesAnyExpose("/user/claims/", expose)); + } + + @Test + public void testMatchesAnyExposeNoMatch() { + + List expose = Arrays.asList("/flow/", "/graph/"); + assertFalse(HierarchicalPrefixMatcher.matchesAnyExpose("/properties/score", expose)); + assertFalse(HierarchicalPrefixMatcher.matchesAnyExpose("/user/claims/email", expose)); + } + + @Test + public void testMatchesAnyExposeNullPath() { + + assertFalse(HierarchicalPrefixMatcher.matchesAnyExpose(null, Arrays.asList("/user/"))); + } + + @Test + public void testMatchesAnyExposeEmptyList() { + + assertFalse(HierarchicalPrefixMatcher.matchesAnyExpose("/user/claims/email", Collections.emptyList())); + } + + @Test + public void testMatchesAnyExposeNullList() { + + assertFalse(HierarchicalPrefixMatcher.matchesAnyExpose("/user/claims/email", null)); + } + + @Test + public void testMatchesAnyExposeMultiplePrefixesOneMatches() { + + List expose = Arrays.asList("/flow/", "/graph/", "/properties/"); + assertTrue(HierarchicalPrefixMatcher.matchesAnyExpose("/properties/score", expose)); + } + + // ========================= normalizePath ========================= + + @Test + public void testNormalizePathLegacyUserInputs() { + + assertEquals(HierarchicalPrefixMatcher.normalizePath("/userInputs/field"), "/input/field"); + } + + @Test + public void testNormalizePathUnchanged() { + + assertEquals(HierarchicalPrefixMatcher.normalizePath("/properties/score"), "/properties/score"); + assertEquals(HierarchicalPrefixMatcher.normalizePath("/user/claims/email"), "/user/claims/email"); + } + + @Test + public void testNormalizePathNull() { + + assertNull(HierarchicalPrefixMatcher.normalizePath(null)); + } + + // ========================= DEFAULT_EXPOSE ========================= + + @Test + public void testDefaultExposeContainsAllAreas() { + + List defaultExpose = HierarchicalPrefixMatcher.DEFAULT_EXPOSE; + assertNotNull(defaultExpose); + assertEquals(defaultExpose.size(), 5); + assertTrue(defaultExpose.contains("/user/")); + assertTrue(defaultExpose.contains("/properties/")); + assertTrue(defaultExpose.contains("/input/")); + assertTrue(defaultExpose.contains("/flow/")); + assertTrue(defaultExpose.contains("/graph/")); + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void testDefaultExposeIsUnmodifiable() { + + HierarchicalPrefixMatcher.DEFAULT_EXPOSE.add("/new/"); + } + + // ========================= ContextArea enum ========================= + + @Test + public void testContextAreaPrefix() { + + assertEquals(ContextArea.USER_CLAIMS.getPrefix(), "/user/claims/"); + assertEquals(ContextArea.PROPERTIES.getPrefix(), "/properties/"); + assertEquals(ContextArea.INPUT.getPrefix(), "/input/"); + assertEquals(ContextArea.FLOW.getPrefix(), "/flow/"); + assertEquals(ContextArea.GRAPH.getPrefix(), "/graph/"); + } + + @Test + public void testContextAreaHasSystemConfiguredKeys() { + + assertTrue(ContextArea.USER_CLAIMS.hasSystemConfiguredKeys()); + assertTrue(ContextArea.USER_CREDENTIALS.hasSystemConfiguredKeys()); + assertTrue(ContextArea.USER_FEDERATED.hasSystemConfiguredKeys()); + assertFalse(ContextArea.USER_SCALAR.hasSystemConfiguredKeys()); + assertFalse(ContextArea.PROPERTIES.hasSystemConfiguredKeys()); + assertFalse(ContextArea.INPUT.hasSystemConfiguredKeys()); + assertFalse(ContextArea.FLOW.hasSystemConfiguredKeys()); + assertFalse(ContextArea.GRAPH.hasSystemConfiguredKeys()); + } +} diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionExecutorTest.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionExecutorTest.java new file mode 100644 index 000000000000..21e837c91326 --- /dev/null +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionExecutorTest.java @@ -0,0 +1,563 @@ +/* + * Copyright (c) 2025, 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.action.execution.executor; + +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionException; +import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionStatus; +import org.wso2.carbon.identity.action.execution.api.model.ActionType; +import org.wso2.carbon.identity.action.execution.api.model.Error; +import org.wso2.carbon.identity.action.execution.api.model.Failure; +import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.action.execution.api.model.Success; +import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; +import org.wso2.carbon.identity.action.execution.internal.component.ActionExecutionServiceComponentHolder; +import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionExecutor; +import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionExecutor.ExecutorResult; +import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; +import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; +import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; +import org.wso2.carbon.identity.flow.mgt.model.NodeConfig; + +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +/** + * Unit tests for {@link InFlowExtensionExecutor}. + */ +public class InFlowExtensionExecutorTest { + + private InFlowExtensionExecutor executor; + + @Mock + private ActionExecutorService actionExecutorService; + + private AutoCloseable mocks; + private MockedStatic holderMock; + + @BeforeMethod + public void setUp() { + + mocks = MockitoAnnotations.openMocks(this); + executor = new InFlowExtensionExecutor(); + + ActionExecutionServiceComponentHolder holderInstance = + mock(ActionExecutionServiceComponentHolder.class); + when(holderInstance.getActionExecutorService()).thenReturn(actionExecutorService); + + holderMock = mockStatic(ActionExecutionServiceComponentHolder.class); + holderMock.when(ActionExecutionServiceComponentHolder::getInstance).thenReturn(holderInstance); + } + + @AfterMethod + public void tearDown() throws Exception { + + holderMock.close(); + mocks.close(); + } + + // ========================= getName ========================= + + @Test + public void testGetName() { + + assertEquals(executor.getName(), "ExtensionExecutor"); + } + + // ========================= getInitiationData ========================= + + @Test + public void testGetInitiationData() { + + assertNotNull(executor.getInitiationData()); + assertTrue(executor.getInitiationData().isEmpty()); + } + + // ========================= rollback ========================= + + @Test + public void testRollback() { + + assertNull(executor.rollback(new FlowExecutionContext())); + } + + // ========================= execute — no actionId ========================= + + @Test + public void testExecuteNoActionId() throws Exception { + + FlowExecutionContext context = createContextWithMetadata(new HashMap<>()); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + verify(actionExecutorService, never()) + .execute(any(ActionType.class), anyString(), any(FlowContext.class), anyString()); + } + + @Test + public void testExecuteEmptyActionId() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", ""); + FlowExecutionContext context = createContextWithMetadata(metadata); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + } + + // ========================= execute — execution disabled ========================= + + @Test + public void testExecuteDisabledExecution() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(false); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + verify(actionExecutorService, never()) + .execute(any(ActionType.class), anyString(), any(FlowContext.class), anyString()); + } + + // ========================= execute — SUCCESS ========================= + + @Test + @SuppressWarnings("unchecked") + public void testExecuteSuccess() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + + ActionExecutionStatus successStatus = mock(ActionExecutionStatus.class); + when(successStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.SUCCESS); + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenReturn(successStatus); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + } + + // ========================= execute — FAILED ========================= + + @Test + @SuppressWarnings("unchecked") + public void testExecuteFailed() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + + Failure failure = new Failure("risk_detected", "Risk score exceeds threshold"); + ActionExecutionStatus failedStatus = mock(ActionExecutionStatus.class); + when(failedStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.FAILED); + when(failedStatus.getResponse()).thenReturn(failure); + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenReturn(failedStatus); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.RETRY.name()); + assertEquals(response.getErrorMessage(), "Risk score exceeds threshold"); + } + + @Test + @SuppressWarnings("unchecked") + public void testExecuteFailedNoDescription() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + + Failure failure = new Failure("risk_detected", null); + ActionExecutionStatus failedStatus = mock(ActionExecutionStatus.class); + when(failedStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.FAILED); + when(failedStatus.getResponse()).thenReturn(failure); + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenReturn(failedStatus); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.RETRY.name()); + // Falls back to reason when description is null. + assertEquals(response.getErrorMessage(), "risk_detected"); + } + + @Test + @SuppressWarnings("unchecked") + public void testExecuteFailedBothNull() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + + Failure failure = new Failure(null, null); + ActionExecutionStatus failedStatus = mock(ActionExecutionStatus.class); + when(failedStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.FAILED); + when(failedStatus.getResponse()).thenReturn(failure); + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenReturn(failedStatus); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.RETRY.name()); + assertEquals(response.getErrorMessage(), + "The operation could not be completed due to an external service failure."); + } + + // ========================= execute — ERROR ========================= + + @Test + @SuppressWarnings("unchecked") + public void testExecuteError() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + + Error error = new Error("internal_error", "DB connection failed"); + ActionExecutionStatus errorStatus = mock(ActionExecutionStatus.class); + when(errorStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.ERROR); + when(errorStatus.getResponse()).thenReturn(error); + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenReturn(errorStatus); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.RETRY.name()); + assertEquals(response.getErrorMessage(), "DB connection failed"); + } + + @Test + @SuppressWarnings("unchecked") + public void testExecuteErrorNoDescription() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + + Error error = new Error("internal_error", null); + ActionExecutionStatus errorStatus = mock(ActionExecutionStatus.class); + when(errorStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.ERROR); + when(errorStatus.getResponse()).thenReturn(error); + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenReturn(errorStatus); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.RETRY.name()); + assertEquals(response.getErrorMessage(), "internal_error"); + } + + @Test + @SuppressWarnings("unchecked") + public void testExecuteErrorBothNull() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + + Error error = new Error(null, null); + ActionExecutionStatus errorStatus = mock(ActionExecutionStatus.class); + when(errorStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.ERROR); + when(errorStatus.getResponse()).thenReturn(error); + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenReturn(errorStatus); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.RETRY.name()); + assertEquals(response.getErrorMessage(), + "An unexpected error occurred in the external service."); + } + + // ========================= execute — INCOMPLETE ========================= + + @Test + @SuppressWarnings("unchecked") + public void testExecuteIncomplete() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + + ActionExecutionStatus incompleteStatus = mock(ActionExecutionStatus.class); + when(incompleteStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.INCOMPLETE); + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenReturn(incompleteStatus); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.USER_INPUT_REQUIRED.name()); + } + + // ========================= execute — null status ========================= + + @Test + public void testExecuteNullStatus() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenReturn(null); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.USER_INPUT_REQUIRED.name()); + } + + // ========================= execute — exception ========================= + + @Test + public void testExecuteActionException() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenThrow(new ActionExecutionException("Connection timeout")); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.RETRY.name()); + assertEquals(response.getErrorMessage(), + "An error occurred while processing the extension. Please try again."); + } + + // ========================= execute — service unavailable ========================= + + @Test(expectedExceptions = org.wso2.carbon.identity.flow.execution.engine.exception.FlowEngineException.class) + public void testExecuteServiceUnavailable() throws Exception { + + // Override holder mock to return null service. + holderMock.close(); + + ActionExecutionServiceComponentHolder holderInstance = + mock(ActionExecutionServiceComponentHolder.class); + when(holderInstance.getActionExecutorService()).thenReturn(null); + + holderMock = mockStatic(ActionExecutionServiceComponentHolder.class); + holderMock.when(ActionExecutionServiceComponentHolder::getInstance).thenReturn(holderInstance); + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + // Should throw FlowEngineException since service is unavailable. + executor.execute(context); + } + + // ========================= execute — expose JSON parsing ========================= + + @Test + @SuppressWarnings("unchecked") + public void testExecuteWithValidExposeJson() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + metadata.put("expose", "[\"/properties\",\"/user/claims\"]"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + + ActionExecutionStatus successStatus = mock(ActionExecutionStatus.class); + when(successStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.SUCCESS); + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenReturn(successStatus); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + } + + @Test + @SuppressWarnings("unchecked") + public void testExecuteWithMalformedExposeJson() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + metadata.put("expose", "not_valid_json"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + + ActionExecutionStatus successStatus = mock(ActionExecutionStatus.class); + when(successStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.SUCCESS); + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenReturn(successStatus); + + // Should fall back to default expose but not crash. + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + } + + @Test + @SuppressWarnings("unchecked") + public void testExecuteWithAllowedOperationsJson() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + metadata.put("allowedOperations", + "[{\"op\":\"add\",\"paths\":[\"/properties/riskScore\"]}]"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + + ActionExecutionStatus successStatus = mock(ActionExecutionStatus.class); + when(successStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.SUCCESS); + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenReturn(successStatus); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + } + + // ========================= execute — no node config ========================= + + @Test + public void testExecuteNoNodeConfig() throws Exception { + + FlowExecutionContext context = new FlowExecutionContext(); + context.setTenantDomain("carbon.super"); + // No current node set → getMetadataValue returns null. + + ExecutorResponse response = executor.execute(context); + + // actionId is null → COMPLETE. + assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + } + + @Test + public void testExecuteNoExecutorDTO() throws Exception { + + FlowExecutionContext context = new FlowExecutionContext(); + context.setTenantDomain("carbon.super"); + + NodeConfig nodeConfig = new NodeConfig.Builder().build(); + context.setCurrentNode(nodeConfig); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + } + + // ========================= ExecutorResult enum ========================= + + @Test + public void testExecutorResultValues() { + + assertEquals(ExecutorResult.values().length, 6); + assertNotNull(ExecutorResult.valueOf("COMPLETE")); + assertNotNull(ExecutorResult.valueOf("ERROR")); + assertNotNull(ExecutorResult.valueOf("USER_ERROR")); + assertNotNull(ExecutorResult.valueOf("USER_INPUT_REQUIRED")); + assertNotNull(ExecutorResult.valueOf("EXTERNAL_REDIRECTION")); + assertNotNull(ExecutorResult.valueOf("RETRY")); + } + + // ========================= Helper methods ========================= + + private FlowExecutionContext createContextWithMetadata(Map metadata) { + + FlowExecutionContext context = new FlowExecutionContext(); + context.setTenantDomain("carbon.super"); + + ExecutorDTO executorDTO = new ExecutorDTO("extensionExecutor", metadata); + NodeConfig nodeConfig = new NodeConfig.Builder() + .executorConfig(executorDTO) + .build(); + context.setCurrentNode(nodeConfig); + + return context; + } +} diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionRequestBuilderTest.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionRequestBuilderTest.java new file mode 100644 index 000000000000..9b0556352737 --- /dev/null +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionRequestBuilderTest.java @@ -0,0 +1,513 @@ +/* + * Copyright (c) 2025, 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.action.execution.executor; + +import org.mockito.MockedStatic; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionRequestBuilderException; +import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequest; +import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequestContext; +import org.wso2.carbon.identity.action.execution.api.model.ActionType; +import org.wso2.carbon.identity.action.execution.api.model.AllowedOperation; +import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.action.execution.api.model.Operation; +import org.wso2.carbon.identity.action.execution.internal.executor.HierarchicalPrefixMatcher; +import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionEvent; +import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionExecutor; +import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionRequestBuilder; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +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.mgt.model.NodeConfig; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +/** + * Unit tests for {@link InFlowExtensionRequestBuilder}. + */ +public class InFlowExtensionRequestBuilderTest { + + private InFlowExtensionRequestBuilder requestBuilder; + private MockedStatic identityTenantUtilMock; + + @BeforeMethod + public void setUp() { + + requestBuilder = new InFlowExtensionRequestBuilder(); + identityTenantUtilMock = mockStatic(IdentityTenantUtil.class); + identityTenantUtilMock.when(() -> IdentityTenantUtil.getTenantId(anyString())).thenReturn(1); + } + + @AfterMethod + public void tearDown() { + + identityTenantUtilMock.close(); + } + + // ========================= getSupportedActionType ========================= + + @Test + public void testGetSupportedActionType() { + + assertEquals(requestBuilder.getSupportedActionType(), ActionType.IN_FLOW_EXTENSION); + } + + // ========================= buildActionExecutionRequest — basics ========================= + + @Test(expectedExceptions = ActionExecutionRequestBuilderException.class) + public void testBuildRequestThrowsWhenFlowExecutionContextMissing() + throws ActionExecutionRequestBuilderException { + + FlowContext flowContext = FlowContext.create(); + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + } + + @Test + public void testBuildRequestWithMinimalContext() throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + assertNotNull(request); + assertEquals(request.getActionType(), ActionType.IN_FLOW_EXTENSION); + assertNotNull(request.getEvent()); + } + + @Test + public void testBuildRequestUsesDefaultExposeWhenExposeIsNull() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + // With DEFAULT_EXPOSE, all areas are exposed — event should have user, properties, inputs, etc. + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event); + assertNotNull(event.getFlowType()); + assertNotNull(event.getUserInputs()); + } + + // ========================= buildAllowedOperations ========================= + + @Test + public void testBuildRequestWithValidAllowedOperations() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + String allowedOpsJson = "[{\"op\":\"ADD\",\"paths\":[\"/properties/riskScore\"]}]"; + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, allowedOpsJson) + .add(InFlowExtensionExecutor.EXPOSE_KEY, + Arrays.asList("/user/", "/properties/", "/input/", "/flow/", "/graph/")); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + List ops = request.getAllowedOperations(); + assertNotNull(ops); + assertEquals(ops.size(), 1); + assertEquals(ops.get(0).getOp(), Operation.ADD); + assertTrue(ops.get(0).getPaths().contains("/properties/riskScore")); + } + + @Test + public void testBuildRequestWithNullAllowedOperationsJson() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + List ops = request.getAllowedOperations(); + assertNotNull(ops); + assertTrue(ops.isEmpty()); + } + + @Test + public void testBuildRequestWithMalformedAllowedOperationsJson() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, "not-valid-json") + .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + List ops = request.getAllowedOperations(); + assertNotNull(ops); + assertTrue(ops.isEmpty()); + } + + @Test + public void testBuildRequestSkipsInvalidOperationConfig() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + // Missing "op" key — should be skipped. + String json = "[{\"paths\":[\"/properties/score\"]},{\"op\":\"ADD\",\"paths\":[\"/properties/level\"]}]"; + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) + .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + List ops = request.getAllowedOperations(); + assertEquals(ops.size(), 1); + assertEquals(ops.get(0).getOp(), Operation.ADD); + } + + @Test + public void testBuildRequestSkipsUnknownOperationType() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + String json = "[{\"op\":\"PATCH\",\"paths\":[\"/properties/score\"]}]"; + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) + .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + assertTrue(request.getAllowedOperations().isEmpty()); + } + + // ========================= Path annotation stripping ========================= + + @Test + @SuppressWarnings("unchecked") + public void testPathAnnotationStrippingSimpleArray() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + String json = "[{\"op\":\"ADD\",\"paths\":[\"/properties/riskFactors[]\"]}]"; + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) + .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + // AllowedOperation should have clean path (without []). + AllowedOperation op = request.getAllowedOperations().get(0); + assertTrue(op.getPaths().contains("/properties/riskFactors")); + assertFalse(op.getPaths().contains("/properties/riskFactors[]")); + + // Annotations should be stored in FlowContext. + Map annotations = flowContext.getValue( + InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); + assertNotNull(annotations); + assertEquals(annotations.get("/properties/riskFactors"), ""); + } + + @Test + @SuppressWarnings("unchecked") + public void testPathAnnotationStrippingSchemaAnnotation() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + String json = "[{\"op\":\"ADD\",\"paths\":[\"/properties/items[name,count]\"]}]"; + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) + .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + AllowedOperation op = request.getAllowedOperations().get(0); + assertTrue(op.getPaths().contains("/properties/items")); + assertFalse(op.getPaths().contains("/properties/items[name,count]")); + + Map annotations = flowContext.getValue( + InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); + assertEquals(annotations.get("/properties/items"), "name,count"); + } + + @Test + @SuppressWarnings("unchecked") + public void testPathWithoutAnnotationNotStoredInAnnotationsMap() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + String json = "[{\"op\":\"ADD\",\"paths\":[\"/properties/riskScore\"]}]"; + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) + .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + // No annotations should be stored when paths have no annotations. + Map annotations = flowContext.getValue( + InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); + assertNull(annotations); + } + + @Test + @SuppressWarnings("unchecked") + public void testMultipleAnnotatedPaths() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + String json = "[{\"op\":\"ADD\",\"paths\":" + + "[\"/properties/riskScore\",\"/properties/riskFactors[]\",\"/properties/items[name,count]\"]}]"; + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) + .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + AllowedOperation op = request.getAllowedOperations().get(0); + assertEquals(op.getPaths().size(), 3); + assertTrue(op.getPaths().contains("/properties/riskScore")); + assertTrue(op.getPaths().contains("/properties/riskFactors")); + assertTrue(op.getPaths().contains("/properties/items")); + + Map annotations = flowContext.getValue( + InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); + assertEquals(annotations.size(), 2); + assertEquals(annotations.get("/properties/riskFactors"), ""); + assertEquals(annotations.get("/properties/items"), "name,count"); + } + + // ========================= REPLACE paths auto-expose ========================= + + @Test + public void testReplacePathsAreAutoExposed() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + // Expose only /input/ — /properties/ not exposed. + // But REPLACE on /properties/riskScore should auto-expose it. + List expose = Arrays.asList("/input/", "/flow/"); + String json = "[{\"op\":\"REPLACE\",\"paths\":[\"/properties/riskScore\"]}]"; + + // Pre-set the property so it appears in the event if exposed. + execCtx.setProperty("riskScore", "50"); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) + .add(InFlowExtensionExecutor.EXPOSE_KEY, expose); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + // The event should contain properties because REPLACE path auto-exposed it. + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event.getFlowProperties()); + assertTrue(event.getFlowProperties().containsKey("riskScore")); + assertEquals(event.getFlowProperties().get("riskScore"), "50"); + } + + @Test + public void testReplacePathAlreadyExposedNoAugmentation() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + // /properties/ already exposed. + List expose = Arrays.asList("/properties/", "/input/", "/flow/"); + String json = "[{\"op\":\"REPLACE\",\"paths\":[\"/properties/riskScore\"]}]"; + + execCtx.setProperty("riskScore", "50"); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) + .add(InFlowExtensionExecutor.EXPOSE_KEY, expose); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + // Should still work — no errors, properties exposed. + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event.getFlowProperties()); + assertTrue(event.getFlowProperties().containsKey("riskScore")); + } + + @Test + public void testOnlyReplacePathsAreAutoExposedNotAddPaths() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + List expose = Arrays.asList("/input/", "/flow/"); + // ADD on /properties/riskLevel should NOT be auto-exposed. + // REPLACE on /properties/riskScore SHOULD be auto-exposed. + String json = "[{\"op\":\"ADD\",\"paths\":[\"/properties/riskLevel\"]}," + + "{\"op\":\"REPLACE\",\"paths\":[\"/properties/riskScore\"]}]"; + + execCtx.setProperty("riskScore", "50"); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) + .add(InFlowExtensionExecutor.EXPOSE_KEY, expose); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + // Properties should be in event due to REPLACE auto-expose. + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event.getFlowProperties()); + // riskScore exposed via REPLACE, riskLevel only configured for ADD (not auto-exposed). + assertTrue(event.getFlowProperties().containsKey("riskScore")); + } + + // ========================= Expose filtering ========================= + + @Test + public void testExposeFilteringOnlyExposedAreaIncluded() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + // Only expose /flow/ — no user, no properties, no input. + List expose = Arrays.asList("/flow/tenantDomain", "/flow/flowType"); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.EXPOSE_KEY, expose); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + // User should NOT be in the event since /user/ is not exposed. + assertNull(event.getUser()); + // Flow type should be present. + assertNotNull(event.getFlowType()); + // Tenant should be present. + assertNotNull(event.getTenant()); + } + + @Test + public void testExposeFilteringSpecificClaim() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + // Only expose a specific claim. + List expose = Arrays.asList( + "/user/claims/http://wso2.org/claims/email", + "/user/userId"); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.EXPOSE_KEY, expose); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event.getUser()); + // Only the email claim should be present, not the country claim. + List claims = event.getUser().getClaims(); + assertEquals(claims.size(), 1); + } + + // ========================= Helper methods ========================= + + private FlowExecutionContext createMinimalFlowExecutionContext() { + + FlowExecutionContext context = new FlowExecutionContext(); + context.setTenantDomain("carbon.super"); + context.setContextIdentifier("test-correlation-id"); + + NodeConfig node = new NodeConfig.Builder() + .id("node1") + .type("EXECUTION") + .build(); + context.setCurrentNode(node); + + return context; + } + + private FlowExecutionContext createFullFlowExecutionContext() { + + FlowExecutionContext context = new FlowExecutionContext(); + context.setTenantDomain("carbon.super"); + context.setContextIdentifier("test-correlation-id"); + context.setApplicationId("app-123"); + context.setFlowType("REGISTRATION"); + + NodeConfig node = new NodeConfig.Builder() + .id("execution_node_1") + .type("EXECUTION") + .build(); + context.setCurrentNode(node); + + FlowUser flowUser = new FlowUser(); + flowUser.setUserId("user-456"); + flowUser.setUsername("testuser"); + flowUser.setUserStoreDomain("PRIMARY"); + flowUser.addClaim("http://wso2.org/claims/email", "test@example.com"); + flowUser.addClaim("http://wso2.org/claims/country", "US"); + context.setFlowUser(flowUser); + + context.addUserInputData("username", "testuser"); + context.addUserInputData("consent", "true"); + + context.setProperty("existingProp", "existingValue"); + + return context; + } +} diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionResponseProcessorTest.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionResponseProcessorTest.java new file mode 100644 index 000000000000..bac7d028fd39 --- /dev/null +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionResponseProcessorTest.java @@ -0,0 +1,680 @@ +/* + * Copyright (c) 2025, 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.action.execution.executor; + +import org.mockito.MockedStatic; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionResponseProcessorException; +import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionResponseContext; +import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionStatus; +import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationErrorResponse; +import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationFailureResponse; +import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationSuccessResponse; +import org.wso2.carbon.identity.action.execution.api.model.ActionType; +import org.wso2.carbon.identity.action.execution.api.model.Error; +import org.wso2.carbon.identity.action.execution.api.model.Failure; +import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.action.execution.api.model.Operation; +import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; +import org.wso2.carbon.identity.action.execution.api.model.Success; +import org.wso2.carbon.identity.action.execution.internal.component.ActionExecutionServiceComponentHolder; +import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionExecutor; +import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionResponseProcessor; +import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; +import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; +import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; +import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; +import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +/** + * Unit tests for {@link InFlowExtensionResponseProcessor}. + */ +public class InFlowExtensionResponseProcessorTest { + + private InFlowExtensionResponseProcessor responseProcessor; + private MockedStatic loggerUtilsMock; + private ClaimMetadataManagementService claimService; + + @BeforeMethod + public void setUp() throws Exception { + + responseProcessor = new InFlowExtensionResponseProcessor(); + loggerUtilsMock = mockStatic(LoggerUtils.class); + loggerUtilsMock.when(LoggerUtils::isDiagnosticLogsEnabled).thenReturn(false); + + // Mock claim service for claim validation + claimService = mock(ClaimMetadataManagementService.class); + LocalClaim emailClaim = mock(LocalClaim.class); + when(emailClaim.getClaimURI()).thenReturn("http://wso2.org/claims/email"); + LocalClaim countryClaim = mock(LocalClaim.class); + when(countryClaim.getClaimURI()).thenReturn("http://wso2.org/claims/country"); + when(claimService.getLocalClaims(anyString())).thenReturn(Arrays.asList(emailClaim, countryClaim)); + + ActionExecutionServiceComponentHolder.getInstance() + .setClaimMetadataManagementService(claimService); + } + + @AfterMethod + public void tearDown() { + + loggerUtilsMock.close(); + } + + // ========================= getSupportedActionType ========================= + + @Test + public void testGetSupportedActionType() { + + assertEquals(responseProcessor.getSupportedActionType(), ActionType.IN_FLOW_EXTENSION); + } + + // ========================= processSuccessResponse — Property ADD ========================= + + @Test + public void testPropertyAddFlat() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + PerformableOperation addOp = createOperation(Operation.ADD, "/properties/riskScore", "75"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, addOp, Collections.emptyMap()); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + assertEquals(execCtx.getProperties().get("riskScore"), "75"); + } + + @Test + public void testPropertyAddNested() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + PerformableOperation addOp = createOperation(Operation.ADD, "/properties/risk/level", "HIGH"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, addOp, Collections.emptyMap()); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + Object riskMap = execCtx.getProperties().get("risk"); + assertNotNull(riskMap); + assertTrue(riskMap instanceof Map); + assertEquals(((Map) riskMap).get("level"), "HIGH"); + } + + @Test + public void testPropertyAddWithArrayAnnotation() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + Map annotations = new HashMap<>(); + annotations.put("/properties/riskFactors", ""); + + List factors = Arrays.asList("ip_mismatch", "new_device"); + PerformableOperation addOp = createOperation(Operation.ADD, "/properties/riskFactors", factors); + ActionExecutionStatus status = executeSuccessResponse(execCtx, addOp, annotations); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + Object stored = execCtx.getProperties().get("riskFactors"); + assertTrue(stored instanceof List); + assertEquals(((List) stored).size(), 2); + } + + @Test + public void testPropertyAddWithSchemaAnnotation() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + Map annotations = new HashMap<>(); + annotations.put("/properties/items", "name,count"); + + List> items = new ArrayList<>(); + Map item = new HashMap<>(); + item.put("name", "item1"); + item.put("count", 5); + items.add(item); + + PerformableOperation addOp = createOperation(Operation.ADD, "/properties/items", items); + ActionExecutionStatus status = executeSuccessResponse(execCtx, addOp, annotations); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + // Schema annotation → value passed through as-is. + Object stored = execCtx.getProperties().get("items"); + assertTrue(stored instanceof List); + } + + @Test + public void testPropertyAddWithoutAnnotationCoercesToString() + throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + // Integer value with no annotation → coerced to String. + PerformableOperation addOp = createOperation(Operation.ADD, "/properties/riskScore", 75); + ActionExecutionStatus status = executeSuccessResponse(execCtx, addOp, Collections.emptyMap()); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + assertEquals(execCtx.getProperties().get("riskScore"), "75"); + } + + @Test + public void testPropertyAddNullValue() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + PerformableOperation addOp = createOperation(Operation.ADD, "/properties/riskScore", null); + // Should still succeed (status is SUCCESS overall) but the individual operation should fail. + ActionExecutionStatus status = executeSuccessResponse(execCtx, addOp, Collections.emptyMap()); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + // Property should NOT be set since value is null. + assertFalse(execCtx.getProperties().containsKey("riskScore")); + } + + @Test + public void testPropertyAddArrayAnnotationSingleValueWrapped() + throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + Map annotations = new HashMap<>(); + annotations.put("/properties/tags", ""); + + // Single value with [] annotation → wrapped in a list. + PerformableOperation addOp = createOperation(Operation.ADD, "/properties/tags", "singleTag"); + executeSuccessResponse(execCtx, addOp, annotations); + + Object stored = execCtx.getProperties().get("tags"); + assertTrue(stored instanceof List); + assertEquals(((List) stored).size(), 1); + assertEquals(((List) stored).get(0), "singleTag"); + } + + // ========================= processSuccessResponse — Property REPLACE ========================= + + @Test + public void testPropertyReplaceFlatExists() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + execCtx.setProperty("riskScore", "50"); + + PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/riskScore", "80"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, Collections.emptyMap()); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + assertEquals(execCtx.getProperties().get("riskScore"), "80"); + } + + @Test + public void testPropertyReplaceFlatDoesNotExist() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + // riskScore not set — REPLACE should fail. + + PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/riskScore", "80"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, Collections.emptyMap()); + + // Overall status is SUCCESS but the property should not be set. + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + assertFalse(execCtx.getProperties().containsKey("riskScore")); + } + + @Test + public void testPropertyReplaceNestedExists() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + Map riskMap = new HashMap<>(); + riskMap.put("score", "50"); + execCtx.setProperty("risk", riskMap); + + PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/risk/score", "80"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, Collections.emptyMap()); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + @SuppressWarnings("unchecked") + Map updatedRisk = (Map) execCtx.getProperties().get("risk"); + assertEquals(updatedRisk.get("score"), "80"); + } + + @Test + public void testPropertyReplaceNestedParentMissing() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + // No "risk" property set. + + PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/risk/score", "80"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, Collections.emptyMap()); + + // Should fail — nested path doesn't exist. + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + assertFalse(execCtx.getProperties().containsKey("risk")); + } + + // ========================= processSuccessResponse — Property REMOVE ========================= + + @Test + public void testPropertyRemoveFlat() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + execCtx.setProperty("riskScore", "50"); + + PerformableOperation removeOp = createOperation(Operation.REMOVE, "/properties/riskScore", null); + executeSuccessResponse(execCtx, removeOp, Collections.emptyMap()); + + assertFalse(execCtx.getProperties().containsKey("riskScore")); + } + + @Test + public void testPropertyRemoveNested() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + Map riskMap = new HashMap<>(); + riskMap.put("score", "50"); + riskMap.put("level", "MEDIUM"); + execCtx.setProperty("risk", riskMap); + + PerformableOperation removeOp = createOperation(Operation.REMOVE, "/properties/risk/score", null); + executeSuccessResponse(execCtx, removeOp, Collections.emptyMap()); + + @SuppressWarnings("unchecked") + Map updatedRisk = (Map) execCtx.getProperties().get("risk"); + assertFalse(updatedRisk.containsKey("score")); + assertTrue(updatedRisk.containsKey("level")); + } + + @Test + public void testPropertyRemoveNestedParentMissing() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + // No "risk" property — remove on nested path should be graceful (no error). + PerformableOperation removeOp = createOperation(Operation.REMOVE, "/properties/risk/score", null); + ActionExecutionStatus status = executeSuccessResponse(execCtx, removeOp, Collections.emptyMap()); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + } + + // ========================= processSuccessResponse — User claim operations ========================= + + @Test + public void testUserClaimAdd() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + PerformableOperation claimOp = createOperation( + Operation.ADD, "/user/claims/http://wso2.org/claims/country", "US"); + executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); + + assertEquals(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/country"), "US"); + } + + @Test + public void testUserClaimReplace() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + execCtx.getFlowUser().addClaim("http://wso2.org/claims/email", "old@example.com"); + + PerformableOperation claimOp = createOperation( + Operation.REPLACE, "/user/claims/http://wso2.org/claims/email", "new@example.com"); + executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); + + assertEquals(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/email"), "new@example.com"); + } + + @Test + public void testUserClaimRemove() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + execCtx.getFlowUser().addClaim("http://wso2.org/claims/email", "test@example.com"); + + PerformableOperation claimOp = createOperation( + Operation.REMOVE, "/user/claims/http://wso2.org/claims/email", null); + executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); + + assertFalse(execCtx.getFlowUser().getClaims().containsKey("http://wso2.org/claims/email")); + } + + @Test + public void testUserClaimAddNullValue() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + PerformableOperation claimOp = createOperation( + Operation.ADD, "/user/claims/http://wso2.org/claims/email", null); + ActionExecutionStatus status = executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); + + // Operation should fail — null value. + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + assertNull(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/email")); + } + + @Test + public void testUserClaimAddEmptyClaimUri() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + PerformableOperation claimOp = createOperation(Operation.ADD, "/user/claims/", "value"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); + + // Should fail — empty claim URI. + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + } + + @Test + public void testUserClaimAddNoFlowUser() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + execCtx.setFlowUser(null); + + PerformableOperation claimOp = createOperation( + Operation.ADD, "/user/claims/http://wso2.org/claims/email", "test@email.com"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); + + // Should fail — no FlowUser. + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + } + + // ========================= processSuccessResponse — User input operations ========================= + + @Test + public void testUserInputAdd() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + PerformableOperation inputOp = createOperation(Operation.ADD, "/input/consent", "true"); + executeSuccessResponse(execCtx, inputOp, Collections.emptyMap()); + + assertEquals(execCtx.getUserInputData().get("consent"), "true"); + } + + @Test + public void testUserInputReplace() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + execCtx.addUserInputData("consent", "false"); + + PerformableOperation inputOp = createOperation(Operation.REPLACE, "/input/consent", "true"); + executeSuccessResponse(execCtx, inputOp, Collections.emptyMap()); + + assertEquals(execCtx.getUserInputData().get("consent"), "true"); + } + + @Test + public void testUserInputRemove() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + execCtx.addUserInputData("consent", "true"); + + PerformableOperation inputOp = createOperation(Operation.REMOVE, "/input/consent", null); + executeSuccessResponse(execCtx, inputOp, Collections.emptyMap()); + + assertFalse(execCtx.getUserInputData().containsKey("consent")); + } + + // ========================= processSuccessResponse — Read-only paths ========================= + + @Test + public void testReadOnlyFlowPath() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + PerformableOperation op = createOperation(Operation.ADD, "/flow/tenantDomain", "newValue"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, op, Collections.emptyMap()); + + // Operation should fail but overall status is SUCCESS. + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + } + + @Test + public void testReadOnlyGraphPath() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + PerformableOperation op = createOperation(Operation.ADD, "/graph/currentNode/id", "newId"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, op, Collections.emptyMap()); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + } + + // ========================= processSuccessResponse — Unknown path ========================= + + @Test + public void testUnknownPathPrefix() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + PerformableOperation op = createOperation(Operation.ADD, "/unknown/path", "value"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, op, Collections.emptyMap()); + + // Unknown path → operation fails, but overall status is SUCCESS. + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + } + + // ========================= processSuccessResponse — Legacy path normalization ========================= + + @Test + public void testLegacyUserInputsPathNormalized() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + // Legacy /userInputs/ path should be normalized to /input/. + PerformableOperation op = createOperation(Operation.ADD, "/userInputs/legacyField", "value"); + executeSuccessResponse(execCtx, op, Collections.emptyMap()); + + assertEquals(execCtx.getUserInputData().get("legacyField"), "value"); + } + + // ========================= processSuccessResponse — Multiple operations ========================= + + @Test + public void testMultipleOperationsMixedResults() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + execCtx.setProperty("existingProp", "old"); + + List operations = new ArrayList<>(); + operations.add(createOperation(Operation.ADD, "/properties/newProp", "newValue")); + operations.add(createOperation(Operation.REPLACE, "/properties/existingProp", "updated")); + operations.add(createOperation(Operation.ADD, "/flow/readonly", "fail")); // This should fail. + + ActionExecutionStatus status = executeSuccessResponse(execCtx, operations, Collections.emptyMap()); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + assertEquals(execCtx.getProperties().get("newProp"), "newValue"); + assertEquals(execCtx.getProperties().get("existingProp"), "updated"); + } + + @Test + public void testEmptyOperations() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + ActionExecutionStatus status = executeSuccessResponse( + execCtx, Collections.emptyList(), Collections.emptyMap()); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + } + + // ========================= processFailureResponse ========================= + + @Test + public void testProcessFailureResponse() throws ActionExecutionResponseProcessorException { + + ActionInvocationFailureResponse failureResponse = mock(ActionInvocationFailureResponse.class); + when(failureResponse.getFailureReason()).thenReturn("high_risk_detected"); + when(failureResponse.getFailureDescription()).thenReturn("Risk score exceeds threshold"); + + @SuppressWarnings("unchecked") + ActionExecutionResponseContext responseContext = + mock(ActionExecutionResponseContext.class); + when(responseContext.getActionInvocationResponse()).thenReturn(failureResponse); + + FlowContext flowContext = FlowContext.create(); + + ActionExecutionStatus status = responseProcessor.processFailureResponse( + flowContext, responseContext); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.FAILED); + assertNotNull(status.getResponse()); + assertEquals(status.getResponse().getFailureReason(), "high_risk_detected"); + assertEquals(status.getResponse().getFailureDescription(), "Risk score exceeds threshold"); + } + + // ========================= processErrorResponse ========================= + + @Test + public void testProcessErrorResponse() throws ActionExecutionResponseProcessorException { + + ActionInvocationErrorResponse errorResponse = mock(ActionInvocationErrorResponse.class); + when(errorResponse.getErrorMessage()).thenReturn("internal_error"); + when(errorResponse.getErrorDescription()).thenReturn("Database connection failed"); + + @SuppressWarnings("unchecked") + ActionExecutionResponseContext responseContext = + mock(ActionExecutionResponseContext.class); + when(responseContext.getActionInvocationResponse()).thenReturn(errorResponse); + + FlowContext flowContext = FlowContext.create(); + + ActionExecutionStatus status = responseProcessor.processErrorResponse( + flowContext, responseContext); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.ERROR); + assertNotNull(status.getResponse()); + assertEquals(status.getResponse().getErrorMessage(), "internal_error"); + assertEquals(status.getResponse().getErrorDescription(), "Database connection failed"); + } + + // ========================= processSuccessResponse — Invalid property path ========================= + + @Test + public void testPropertyAddEmptyPropertyName() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + PerformableOperation op = createOperation(Operation.ADD, "/properties/", "value"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, op, Collections.emptyMap()); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + } + + // ========================= processSuccessResponse — Three-level nesting ========================= + + @Test + public void testPropertyAddThreeLevelNesting() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + PerformableOperation addOp = createOperation( + Operation.ADD, "/properties/deep/nested/value", "deepValue"); + executeSuccessResponse(execCtx, addOp, Collections.emptyMap()); + + @SuppressWarnings("unchecked") + Map deep = (Map) execCtx.getProperties().get("deep"); + assertNotNull(deep); + @SuppressWarnings("unchecked") + Map nested = (Map) deep.get("nested"); + assertNotNull(nested); + assertEquals(nested.get("value"), "deepValue"); + } + + @Test + public void testPropertyReplaceNullValue() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + execCtx.setProperty("score", "50"); + + PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/score", null); + ActionExecutionStatus status = executeSuccessResponse( + execCtx, replaceOp, Collections.emptyMap()); + + // Null value should fail for REPLACE. + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + // Original value should remain. + assertEquals(execCtx.getProperties().get("score"), "50"); + } + + // ========================= Helper methods ========================= + + private FlowExecutionContext createFlowExecutionContext() { + + FlowExecutionContext context = new FlowExecutionContext(); + context.setTenantDomain("carbon.super"); + context.setContextIdentifier("test-id"); + + FlowUser flowUser = new FlowUser(); + flowUser.setUserId("user-1"); + flowUser.setUsername("testuser"); + context.setFlowUser(flowUser); + + return context; + } + + private PerformableOperation createOperation(Operation op, String path, Object value) { + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(op); + operation.setPath(path); + if (value != null) { + operation.setValue(value); + } + return operation; + } + + private ActionExecutionStatus executeSuccessResponse( + FlowExecutionContext execCtx, PerformableOperation operation, + Map pathTypeAnnotations) + throws ActionExecutionResponseProcessorException { + + return executeSuccessResponse(execCtx, Collections.singletonList(operation), pathTypeAnnotations); + } + + @SuppressWarnings("unchecked") + private ActionExecutionStatus executeSuccessResponse( + FlowExecutionContext execCtx, List operations, + Map pathTypeAnnotations) + throws ActionExecutionResponseProcessorException { + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + if (pathTypeAnnotations != null && !pathTypeAnnotations.isEmpty()) { + flowContext.add(InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); + } + + ActionInvocationSuccessResponse successResponse = mock(ActionInvocationSuccessResponse.class); + when(successResponse.getOperations()).thenReturn(operations); + + ActionExecutionResponseContext responseContext = + mock(ActionExecutionResponseContext.class); + when(responseContext.getActionInvocationResponse()).thenReturn(successResponse); + + return responseProcessor.processSuccessResponse(flowContext, responseContext); + } +} diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/resources/testng.xml b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/resources/testng.xml index b4b05647ee16..599ea71b5dcc 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/resources/testng.xml +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/resources/testng.xml @@ -45,4 +45,12 @@ + + + + + + + + diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/core/FlowExecutionEngine.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/core/FlowExecutionEngine.java index abe67392b055..93fe2befeca8 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/core/FlowExecutionEngine.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/core/FlowExecutionEngine.java @@ -35,6 +35,7 @@ import org.wso2.carbon.identity.flow.mgt.model.DataDTO; import org.wso2.carbon.identity.flow.mgt.model.GraphConfig; import org.wso2.carbon.identity.flow.mgt.model.NodeConfig; +import org.wso2.carbon.identity.flow.mgt.model.StepDTO; import java.util.Map; @@ -233,7 +234,20 @@ private NodeResponse triggerNode(NodeConfig nodeConfig, FlowExecutionContext con private FlowExecutionStep resolveStepForPrompt(GraphConfig graph, NodeConfig currentNode, FlowExecutionContext context, NodeResponse nodeResponse) throws FlowEngineServerException { - DataDTO dataDTO = graph.getNodePageMappings().get(currentNode.getId()).getData(); + StepDTO stepDTO = graph.getNodePageMappings().get(currentNode.getId()); + + // If the current node has no page mapping (e.g., a background EXECUTION node), + // fall back to the previous node's page mapping so the previous form re-renders + // with any error message displayed inline. + if (stepDTO == null && currentNode.getPreviousNodeId() != null) { + stepDTO = graph.getNodePageMappings().get(currentNode.getPreviousNodeId()); + if (LOG.isDebugEnabled()) { + LOG.debug("Node " + currentNode.getId() + " has no page mapping. " + + "Falling back to previous node: " + currentNode.getPreviousNodeId()); + } + } + + DataDTO dataDTO = stepDTO != null ? stepDTO.getData() : null; DataDTO finalDataDTO = null; if (dataDTO != null) { From 31c0f2e3f6cb2f64d418c88d06ec4d86d30a8447 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Mon, 23 Feb 2026 11:37:47 +0530 Subject: [PATCH 04/41] Moved the In-flow extension executor and its util classes to flow execution engine --- .../pom.xml | 14 ------ .../ActionExecutionServiceComponent.java | 37 ---------------- ...ActionExecutionServiceComponentHolder.java | 22 ---------- .../src/test/resources/testng.xml | 8 ---- .../pom.xml | 39 ++++++++++++++--- .../executor/HierarchicalPrefixMatcher.java | 2 +- .../executor/InFlowExtensionEvent.java | 6 +-- .../executor/InFlowExtensionExecutor.java | 6 +-- .../InFlowExtensionRequestBuilder.java | 2 +- .../InFlowExtensionResponseProcessor.java | 6 +-- .../executor/OperationExecutionResult.java | 2 +- .../FlowExecutionEngineDataHolder.java | 22 ++++++++++ .../FlowExecutionEngineServiceComponent.java | 43 +++++++++++++++++-- .../HierarchicalPrefixMatcherTest.java | 5 +-- .../executor/InFlowExtensionExecutorTest.java | 25 ++++++----- .../InFlowExtensionRequestBuilderTest.java | 6 +-- .../InFlowExtensionResponseProcessorTest.java | 14 +++--- .../src/test/resources/testng.xml | 10 ++++- 18 files changed, 139 insertions(+), 130 deletions(-) rename components/{action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal => flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine}/executor/HierarchicalPrefixMatcher.java (99%) rename components/{action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal => flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine}/executor/InFlowExtensionEvent.java (95%) rename components/{action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal => flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine}/executor/InFlowExtensionExecutor.java (97%) rename components/{action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal => flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine}/executor/InFlowExtensionRequestBuilder.java (99%) rename components/{action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal => flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine}/executor/InFlowExtensionResponseProcessor.java (99%) rename components/{action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal => flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine}/executor/OperationExecutionResult.java (96%) rename components/{action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution => flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine}/executor/HierarchicalPrefixMatcherTest.java (97%) rename components/{action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution => flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine}/executor/InFlowExtensionExecutorTest.java (95%) rename components/{action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution => flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine}/executor/InFlowExtensionRequestBuilderTest.java (98%) rename components/{action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution => flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine}/executor/InFlowExtensionResponseProcessorTest.java (98%) diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/pom.xml b/components/action-mgt/org.wso2.carbon.identity.action.execution/pom.xml index e2fb17bade38..0e2c448fc24c 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/pom.xml +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/pom.xml @@ -49,14 +49,6 @@ org.wso2.carbon.identity.framework org.wso2.carbon.identity.central.log.mgt - - org.wso2.carbon.identity.framework - org.wso2.carbon.identity.flow.execution.engine - - - org.wso2.carbon.identity.framework - org.wso2.carbon.identity.claim.metadata.mgt - com.fasterxml.jackson.core jackson-databind @@ -113,12 +105,6 @@ version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.rule.evaluation.api.*; version="${carbon.identity.package.import.version.range}", - org.wso2.carbon.identity.flow.execution.engine.*; - version="${carbon.identity.package.import.version.range}", - org.wso2.carbon.identity.flow.mgt.*; - version="${carbon.identity.package.import.version.range}", - org.wso2.carbon.identity.claim.metadata.mgt.*; - version="${carbon.identity.package.import.version.range}", org.apache.commons.lang; version="${commons-lang.wso2.osgi.version.range}", org.apache.commons.logging; version="${import.package.version.commons.logging}", org.apache.commons.collections; version="${commons-collections.wso2.osgi.version.range}", diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java index 45879b425d0b..831ab6ae0403 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java @@ -33,17 +33,12 @@ import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.action.execution.api.service.ActionInvocationResponseClassProvider; import org.wso2.carbon.identity.action.execution.api.service.ActionVersioningHandler; -import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionExecutor; -import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionRequestBuilder; -import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionResponseProcessor; import org.wso2.carbon.identity.action.execution.internal.service.impl.ActionExecutionRequestBuilderFactory; import org.wso2.carbon.identity.action.execution.internal.service.impl.ActionExecutionResponseProcessorFactory; import org.wso2.carbon.identity.action.execution.internal.service.impl.ActionExecutorServiceImpl; import org.wso2.carbon.identity.action.execution.internal.service.impl.ActionInvocationResponseClassFactory; import org.wso2.carbon.identity.action.execution.internal.service.impl.ActionVersioningHandlerFactory; import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; -import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; -import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; import org.wso2.carbon.identity.rule.evaluation.api.service.RuleEvaluationService; import org.wso2.carbon.identity.secret.mgt.core.SecretManager; import org.wso2.carbon.identity.secret.mgt.core.SecretResolveManager; @@ -73,15 +68,6 @@ protected void activate(ComponentContext context) { // Store reference for internal use ActionExecutionServiceComponentHolder.getInstance().setActionExecutorService(actionExecutorService); - // Register In-Flow Extension services - bundleCtx.registerService(ActionExecutionRequestBuilder.class.getName(), - new InFlowExtensionRequestBuilder(), null); - bundleCtx.registerService(ActionExecutionResponseProcessor.class.getName(), - new InFlowExtensionResponseProcessor(), null); - - // Register InFlowExtensionExecutor for the flow engine - bundleCtx.registerService(Executor.class.getName(), new InFlowExtensionExecutor(), null); - LOG.debug("Action execution bundle is activated."); } catch (Throwable e) { LOG.error("Error while initializing Action execution service component.", e); @@ -296,27 +282,4 @@ protected void unsetRealmService(RealmService realmService) { LOG.debug("RealmService unset in ActionExecutionServiceComponentHolder bundle."); } } - - @Reference( - name = "claim.metadata.management.service", - service = ClaimMetadataManagementService.class, - cardinality = ReferenceCardinality.MANDATORY, - policy = ReferencePolicy.DYNAMIC, - unbind = "unsetClaimMetadataManagementService" - ) - protected void setClaimMetadataManagementService(ClaimMetadataManagementService claimMetadataManagementService) { - - LOG.debug("Registering a reference for ClaimMetadataManagementService in the ActionExecutionServiceComponent."); - ActionExecutionServiceComponentHolder.getInstance() - .setClaimMetadataManagementService(claimMetadataManagementService); - } - - protected void unsetClaimMetadataManagementService(ClaimMetadataManagementService claimMetadataManagementService) { - - LOG.debug("Unregistering reference for ClaimMetadataManagementService in the ActionExecutionServiceComponent."); - if (ActionExecutionServiceComponentHolder.getInstance().getClaimMetadataManagementService() - .equals(claimMetadataManagementService)) { - ActionExecutionServiceComponentHolder.getInstance().setClaimMetadataManagementService(null); - } - } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java index 3ff81c81179b..05f2708d7fc9 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java @@ -20,7 +20,6 @@ import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; -import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.rule.evaluation.api.service.RuleEvaluationService; import org.wso2.carbon.identity.secret.mgt.core.SecretManager; import org.wso2.carbon.identity.secret.mgt.core.SecretResolveManager; @@ -39,7 +38,6 @@ public class ActionExecutionServiceComponentHolder { private SecretResolveManager secretResolveManager; private RealmService realmService; private ActionExecutorService actionExecutorService; - private ClaimMetadataManagementService claimMetadataManagementService; private ActionExecutionServiceComponentHolder() { @@ -139,24 +137,4 @@ public void setActionExecutorService(ActionExecutorService actionExecutorService this.actionExecutorService = actionExecutorService; } - - /** - * Get the ClaimMetadataManagementService instance. - * - * @return ClaimMetadataManagementService instance. - */ - public ClaimMetadataManagementService getClaimMetadataManagementService() { - - return claimMetadataManagementService; - } - - /** - * Set the ClaimMetadataManagementService instance. - * - * @param claimMetadataManagementService ClaimMetadataManagementService instance. - */ - public void setClaimMetadataManagementService(ClaimMetadataManagementService claimMetadataManagementService) { - - this.claimMetadataManagementService = claimMetadataManagementService; - } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/resources/testng.xml b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/resources/testng.xml index 599ea71b5dcc..b4b05647ee16 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/resources/testng.xml +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/resources/testng.xml @@ -45,12 +45,4 @@ - - - - - - - - diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml index 40f3f77e9e28..50298951caa1 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml @@ -70,6 +70,18 @@ org.wso2.carbon.identity.framework org.wso2.carbon.identity.flow.mgt + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.action.execution + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.claim.metadata.mgt + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.central.log.mgt + org.wso2.carbon.identity.framework org.wso2.carbon.identity.input.validation.mgt @@ -99,10 +111,6 @@ org.wso2.carbon.identity.framework org.wso2.carbon.identity.user.action - - org.wso2.carbon.identity.framework - org.wso2.carbon.identity.claim.metadata.mgt - @@ -165,7 +173,17 @@ org.wso2.carbon.identity.user.action.api.exception; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.core.util; version="${carbon.kernel.package.import.version.range}", - org.wso2.carbon.identity.event.*; version="${carbon.identity.package.import.version.range}" + org.wso2.carbon.identity.event.*; version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.action.execution.api.*; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.central.log.mgt.utils; + version="${carbon.identity.package.import.version.range}", + com.fasterxml.jackson.core.*; version="${com.fasterxml.jackson.annotation.version.range}", + com.fasterxml.jackson.databind.*; + version="${com.fasterxml.jackson.annotation.version.range}", + com.fasterxml.jackson.annotation.*; + version="${com.fasterxml.jackson.annotation.version.range}", + org.wso2.carbon.utils; version="${carbon.kernel.package.import.version.range}", !org.wso2.carbon.identity.flow.execution.internal, @@ -179,12 +197,23 @@ org.wso2.carbon.identity.flow.execution.engine.listener, org.wso2.carbon.identity.flow.execution.engine.validation, org.wso2.carbon.identity.flow.execution.engine.exception, + org.wso2.carbon.identity.flow.execution.engine.executor, org.wso2.carbon.identity.flow.execution.engine.graph; version="${carbon.identity.package.export.version}" + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.plugin.version} + + + src/test/resources/testng.xml + + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/HierarchicalPrefixMatcher.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcher.java similarity index 99% rename from components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/HierarchicalPrefixMatcher.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcher.java index ddf3b3ebb7c9..bd786d305446 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/HierarchicalPrefixMatcher.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcher.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.action.execution.internal.executor; +package org.wso2.carbon.identity.flow.execution.engine.executor; import java.util.Arrays; import java.util.Collections; diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionEvent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionEvent.java similarity index 95% rename from components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionEvent.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionEvent.java index 18582e2e4a19..14c2b674769e 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionEvent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionEvent.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.action.execution.internal.executor; +package org.wso2.carbon.identity.flow.execution.engine.executor; import org.wso2.carbon.identity.action.execution.api.model.Application; import org.wso2.carbon.identity.action.execution.api.model.Event; @@ -163,13 +163,13 @@ public Builder currentNodeId(String currentNodeId) { public Builder userInputs(Map userInputs) { - this.userInputs = userInputs; + this.userInputs = userInputs != null ? new HashMap<>(userInputs) : null; return this; } public Builder flowProperties(Map flowProperties) { - this.flowProperties = flowProperties; + this.flowProperties = flowProperties != null ? new HashMap<>(flowProperties) : null; return this; } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutor.java similarity index 97% rename from components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutor.java index f40e7b53066b..a5e82204b125 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutor.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.action.execution.internal.executor; +package org.wso2.carbon.identity.flow.execution.engine.executor; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -30,8 +30,8 @@ import org.wso2.carbon.identity.action.execution.api.model.Failure; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; -import org.wso2.carbon.identity.action.execution.internal.component.ActionExecutionServiceComponentHolder; 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.graph.Executor; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; @@ -265,7 +265,7 @@ private String buildUserFacingErrorMessage(Error error) { private ActionExecutorService getActionExecutorService() { - return ActionExecutionServiceComponentHolder.getInstance().getActionExecutorService(); + return FlowExecutionEngineDataHolder.getInstance().getActionExecutorService(); } /** diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilder.java similarity index 99% rename from components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilder.java index b8c3b0d8c3f3..dcaa99f57004 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilder.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.action.execution.internal.executor; +package org.wso2.carbon.identity.flow.execution.engine.executor; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessor.java similarity index 99% rename from components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessor.java index 67a7544fb519..de4f84cc6de8 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessor.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.action.execution.internal.executor; +package org.wso2.carbon.identity.flow.execution.engine.executor; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; @@ -41,11 +41,11 @@ import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.action.execution.api.model.SuccessStatus; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionResponseProcessor; -import org.wso2.carbon.identity.action.execution.internal.component.ActionExecutionServiceComponentHolder; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; +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.utils.DiagnosticLog; @@ -583,7 +583,7 @@ private Set getValidClaimUris(String tenantDomain) { private ClaimMetadataManagementService getClaimMetadataManagementService() { - return ActionExecutionServiceComponentHolder.getInstance().getClaimMetadataManagementService(); + return FlowExecutionEngineDataHolder.getInstance().getClaimMetadataManagementService(); } /** diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/OperationExecutionResult.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/OperationExecutionResult.java similarity index 96% rename from components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/OperationExecutionResult.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/OperationExecutionResult.java index 2b42c4f6af25..f4907c37c5d8 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/executor/OperationExecutionResult.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/OperationExecutionResult.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.action.execution.internal.executor; +package org.wso2.carbon.identity.flow.execution.engine.executor; import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java index adf1b6225b53..ed14a2f04d39 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java @@ -18,6 +18,7 @@ package org.wso2.carbon.identity.flow.execution.engine.internal; +import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.application.mgt.ApplicationManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.event.services.IdentityEventService; @@ -47,6 +48,7 @@ public class FlowExecutionEngineDataHolder { private ApplicationManagementService applicationManagementService; private FederatedAssociationManager federatedAssociationManager; private IdentityEventService identityEventService; + private ActionExecutorService actionExecutorService; private List flowExecutionListeners = new ArrayList<>(); private FlowExecutionEngineDataHolder() { @@ -218,4 +220,24 @@ public void setIdentityEventService(IdentityEventService identityEventService) { this.identityEventService = identityEventService; } + + /** + * Get the ActionExecutorService instance. + * + * @return ActionExecutorService instance. + */ + public ActionExecutorService getActionExecutorService() { + + return actionExecutorService; + } + + /** + * Set the ActionExecutorService instance. + * + * @param actionExecutorService ActionExecutorService instance. + */ + public void setActionExecutorService(ActionExecutorService actionExecutorService) { + + this.actionExecutorService = actionExecutorService; + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java index 7aeaf911c88f..40eaa79cc1e3 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java @@ -28,10 +28,16 @@ import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; +import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionRequestBuilder; +import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionResponseProcessor; +import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.application.mgt.ApplicationManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.flow.execution.engine.FlowExecutionService; +import org.wso2.carbon.identity.flow.execution.engine.executor.InFlowExtensionExecutor; +import org.wso2.carbon.identity.flow.execution.engine.executor.InFlowExtensionRequestBuilder; +import org.wso2.carbon.identity.flow.execution.engine.executor.InFlowExtensionResponseProcessor; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; import org.wso2.carbon.identity.flow.execution.engine.listener.FlowExecutionListener; import org.wso2.carbon.identity.flow.execution.engine.validation.InputProcessingListener; @@ -79,6 +85,14 @@ protected void activate(ComponentContext context) { FlowExecutionService.getInstance(), null); bundleContext.registerService(FlowExecutionListener.class.getName(), new InputProcessingListener(), null); + + // Register In-Flow Extension executor and its action execution services + bundleContext.registerService(Executor.class.getName(), new InFlowExtensionExecutor(), null); + bundleContext.registerService(ActionExecutionRequestBuilder.class.getName(), + new InFlowExtensionRequestBuilder(), null); + bundleContext.registerService(ActionExecutionResponseProcessor.class.getName(), + new InFlowExtensionResponseProcessor(), null); + LOG.debug("Flow Engine service successfully activated."); } catch (Throwable e) { LOG.error("Error while initiating Flow Engine service", e); @@ -232,13 +246,15 @@ public void unsetFederatedAssociationManager(FederatedAssociationManager federat ) protected void setClaimMetadataManagementService(ClaimMetadataManagementService claimMetadataManagementService) { - LOG.debug("Setting the Claim Metadata Management Service in the Flow Engine component."); - FlowExecutionEngineDataHolder.getInstance().setClaimMetadataManagementService(claimMetadataManagementService); + LOG.debug("Setting the ClaimMetadataManagementService in the Flow Engine component."); + FlowExecutionEngineDataHolder.getInstance() + .setClaimMetadataManagementService(claimMetadataManagementService); } - protected void unsetClaimMetadataManagementService(ClaimMetadataManagementService claimMetadataManagementService) { + protected void unsetClaimMetadataManagementService( + ClaimMetadataManagementService claimMetadataManagementService) { - LOG.debug("Unsetting the Claim Metadata Management Service in the Flow Engine component."); + LOG.debug("Unsetting the ClaimMetadataManagementService in the Flow Engine component."); FlowExecutionEngineDataHolder.getInstance().setClaimMetadataManagementService(null); } @@ -258,4 +274,23 @@ protected void unsetIdentityEventService(IdentityEventService identityEventServi FlowExecutionEngineDataHolder.getInstance().setIdentityEventService(null); } + + @Reference( + name = "ActionExecutorService", + service = ActionExecutorService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetActionExecutorService" + ) + protected void setActionExecutorService(ActionExecutorService actionExecutorService) { + + LOG.debug("Setting the ActionExecutorService in the Flow Engine component."); + FlowExecutionEngineDataHolder.getInstance().setActionExecutorService(actionExecutorService); + } + + protected void unsetActionExecutorService(ActionExecutorService actionExecutorService) { + + LOG.debug("Unsetting the ActionExecutorService in the Flow Engine component."); + FlowExecutionEngineDataHolder.getInstance().setActionExecutorService(null); + } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/HierarchicalPrefixMatcherTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java similarity index 97% rename from components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/HierarchicalPrefixMatcherTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java index 3eebd6d886df..52d2222dd6c1 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/HierarchicalPrefixMatcherTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java @@ -16,12 +16,11 @@ * under the License. */ -package org.wso2.carbon.identity.action.execution.executor; +package org.wso2.carbon.identity.flow.execution.engine.executor; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import org.wso2.carbon.identity.action.execution.internal.executor.HierarchicalPrefixMatcher; -import org.wso2.carbon.identity.action.execution.internal.executor.HierarchicalPrefixMatcher.ContextArea; +import org.wso2.carbon.identity.flow.execution.engine.executor.HierarchicalPrefixMatcher.ContextArea; import java.util.Arrays; import java.util.Collections; diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java similarity index 95% rename from components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionExecutorTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java index 21e837c91326..395e01e52c16 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.action.execution.executor; +package org.wso2.carbon.identity.flow.execution.engine.executor; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -32,9 +32,8 @@ import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; -import org.wso2.carbon.identity.action.execution.internal.component.ActionExecutionServiceComponentHolder; -import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionExecutor; -import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionExecutor.ExecutorResult; +import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; +import org.wso2.carbon.identity.flow.execution.engine.executor.InFlowExtensionExecutor.ExecutorResult; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; @@ -67,7 +66,7 @@ public class InFlowExtensionExecutorTest { private ActionExecutorService actionExecutorService; private AutoCloseable mocks; - private MockedStatic holderMock; + private MockedStatic holderMock; @BeforeMethod public void setUp() { @@ -75,12 +74,12 @@ public void setUp() { mocks = MockitoAnnotations.openMocks(this); executor = new InFlowExtensionExecutor(); - ActionExecutionServiceComponentHolder holderInstance = - mock(ActionExecutionServiceComponentHolder.class); + FlowExecutionEngineDataHolder holderInstance = + mock(FlowExecutionEngineDataHolder.class); when(holderInstance.getActionExecutorService()).thenReturn(actionExecutorService); - holderMock = mockStatic(ActionExecutionServiceComponentHolder.class); - holderMock.when(ActionExecutionServiceComponentHolder::getInstance).thenReturn(holderInstance); + holderMock = mockStatic(FlowExecutionEngineDataHolder.class); + holderMock.when(FlowExecutionEngineDataHolder::getInstance).thenReturn(holderInstance); } @AfterMethod @@ -414,12 +413,12 @@ public void testExecuteServiceUnavailable() throws Exception { // Override holder mock to return null service. holderMock.close(); - ActionExecutionServiceComponentHolder holderInstance = - mock(ActionExecutionServiceComponentHolder.class); + FlowExecutionEngineDataHolder holderInstance = + mock(FlowExecutionEngineDataHolder.class); when(holderInstance.getActionExecutorService()).thenReturn(null); - holderMock = mockStatic(ActionExecutionServiceComponentHolder.class); - holderMock.when(ActionExecutionServiceComponentHolder::getInstance).thenReturn(holderInstance); + holderMock = mockStatic(FlowExecutionEngineDataHolder.class); + holderMock.when(FlowExecutionEngineDataHolder::getInstance).thenReturn(holderInstance); Map metadata = new HashMap<>(); metadata.put("actionId", "test-action-001"); diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java similarity index 98% rename from components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionRequestBuilderTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java index 9b0556352737..19c81a4b865d 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.action.execution.executor; +package org.wso2.carbon.identity.flow.execution.engine.executor; import org.mockito.MockedStatic; import org.testng.annotations.AfterMethod; @@ -29,10 +29,6 @@ import org.wso2.carbon.identity.action.execution.api.model.AllowedOperation; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.model.Operation; -import org.wso2.carbon.identity.action.execution.internal.executor.HierarchicalPrefixMatcher; -import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionEvent; -import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionExecutor; -import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionRequestBuilder; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java similarity index 98% rename from components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionResponseProcessorTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java index bac7d028fd39..ea0f71492e8d 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.action.execution.executor; +package org.wso2.carbon.identity.flow.execution.engine.executor; import org.mockito.MockedStatic; import org.testng.annotations.AfterMethod; @@ -35,9 +35,7 @@ import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; import org.wso2.carbon.identity.action.execution.api.model.Success; -import org.wso2.carbon.identity.action.execution.internal.component.ActionExecutionServiceComponentHolder; -import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionExecutor; -import org.wso2.carbon.identity.action.execution.internal.executor.InFlowExtensionResponseProcessor; +import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; @@ -68,6 +66,7 @@ public class InFlowExtensionResponseProcessorTest { private InFlowExtensionResponseProcessor responseProcessor; private MockedStatic loggerUtilsMock; + private MockedStatic holderMock; private ClaimMetadataManagementService claimService; @BeforeMethod @@ -85,13 +84,16 @@ public void setUp() throws Exception { when(countryClaim.getClaimURI()).thenReturn("http://wso2.org/claims/country"); when(claimService.getLocalClaims(anyString())).thenReturn(Arrays.asList(emailClaim, countryClaim)); - ActionExecutionServiceComponentHolder.getInstance() - .setClaimMetadataManagementService(claimService); + FlowExecutionEngineDataHolder holderInstance = mock(FlowExecutionEngineDataHolder.class); + when(holderInstance.getClaimMetadataManagementService()).thenReturn(claimService); + holderMock = mockStatic(FlowExecutionEngineDataHolder.class); + holderMock.when(FlowExecutionEngineDataHolder::getInstance).thenReturn(holderInstance); } @AfterMethod public void tearDown() { + holderMock.close(); loggerUtilsMock.close(); } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml index 83303d82a279..47e0b300668a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml @@ -27,7 +27,15 @@ - + + + + + + + + + From 201197663c981ab88b95f95157849e4ea7e53d89 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Fri, 27 Feb 2026 08:09:03 +0530 Subject: [PATCH 05/41] Executor Package re-arrangements --- .../component/ActionExecutionServiceComponent.java | 10 ++-------- .../pom.xml | 2 +- .../extension}/executor/HierarchicalPrefixMatcher.java | 2 +- .../extension}/executor/InFlowExtensionEvent.java | 2 +- .../extension}/executor/InFlowExtensionExecutor.java | 2 +- .../executor/InFlowExtensionRequestBuilder.java | 2 +- .../executor/InFlowExtensionResponseProcessor.java | 2 +- .../extension}/executor/OperationExecutionResult.java | 2 +- .../internal/FlowExecutionEngineServiceComponent.java | 6 +++--- .../engine/executor/HierarchicalPrefixMatcherTest.java | 3 ++- .../engine/executor/InFlowExtensionExecutorTest.java | 3 ++- .../executor/InFlowExtensionRequestBuilderTest.java | 4 ++++ .../executor/InFlowExtensionResponseProcessorTest.java | 2 ++ 13 files changed, 22 insertions(+), 20 deletions(-) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/{ => inflow/extension}/executor/HierarchicalPrefixMatcher.java (99%) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/{ => inflow/extension}/executor/InFlowExtensionEvent.java (98%) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/{ => inflow/extension}/executor/InFlowExtensionExecutor.java (99%) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/{ => inflow/extension}/executor/InFlowExtensionRequestBuilder.java (99%) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/{ => inflow/extension}/executor/InFlowExtensionResponseProcessor.java (99%) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/{ => inflow/extension}/executor/OperationExecutionResult.java (95%) diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java index 831ab6ae0403..185114e5da7c 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponent.java @@ -60,14 +60,8 @@ protected void activate(ComponentContext context) { try { BundleContext bundleCtx = context.getBundleContext(); - - // Register ActionExecutorService - ActionExecutorService actionExecutorService = ActionExecutorServiceImpl.getInstance(); - bundleCtx.registerService(ActionExecutorService.class.getName(), actionExecutorService, null); - - // Store reference for internal use - ActionExecutionServiceComponentHolder.getInstance().setActionExecutorService(actionExecutorService); - + bundleCtx.registerService(ActionExecutorService.class.getName(), ActionExecutorServiceImpl.getInstance(), + null); LOG.debug("Action execution bundle is activated."); } catch (Throwable e) { LOG.error("Error while initializing Action execution service component.", e); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml index 50298951caa1..0cb418592268 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml @@ -197,7 +197,7 @@ org.wso2.carbon.identity.flow.execution.engine.listener, org.wso2.carbon.identity.flow.execution.engine.validation, org.wso2.carbon.identity.flow.execution.engine.exception, - org.wso2.carbon.identity.flow.execution.engine.executor, + org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor, org.wso2.carbon.identity.flow.execution.engine.graph; version="${carbon.identity.package.export.version}" diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcher.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcher.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java index bd786d305446..216349750199 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcher.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.executor; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; import java.util.Arrays; import java.util.Collections; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionEvent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionEvent.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionEvent.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionEvent.java index 14c2b674769e..7671fa208974 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionEvent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionEvent.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.executor; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; import org.wso2.carbon.identity.action.execution.api.model.Application; import org.wso2.carbon.identity.action.execution.api.model.Event; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutor.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java index a5e82204b125..d0b694aa4fe7 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.executor; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilder.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java index dcaa99f57004..f949217b9990 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.executor; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessor.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java index de4f84cc6de8..4770b229fc40 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.executor; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/OperationExecutionResult.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/OperationExecutionResult.java similarity index 95% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/OperationExecutionResult.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/OperationExecutionResult.java index f4907c37c5d8..b8a3faff499b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/executor/OperationExecutionResult.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/OperationExecutionResult.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.executor; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java index 40eaa79cc1e3..59521b9b6739 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java @@ -35,10 +35,10 @@ import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.flow.execution.engine.FlowExecutionService; -import org.wso2.carbon.identity.flow.execution.engine.executor.InFlowExtensionExecutor; -import org.wso2.carbon.identity.flow.execution.engine.executor.InFlowExtensionRequestBuilder; -import org.wso2.carbon.identity.flow.execution.engine.executor.InFlowExtensionResponseProcessor; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionRequestBuilder; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionResponseProcessor; import org.wso2.carbon.identity.flow.execution.engine.listener.FlowExecutionListener; import org.wso2.carbon.identity.flow.execution.engine.validation.InputProcessingListener; import org.wso2.carbon.identity.flow.mgt.FlowMgtService; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java index 52d2222dd6c1..4b96b51b80f6 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java @@ -20,7 +20,8 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import org.wso2.carbon.identity.flow.execution.engine.executor.HierarchicalPrefixMatcher.ContextArea; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.HierarchicalPrefixMatcher; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.HierarchicalPrefixMatcher.ContextArea; import java.util.Arrays; import java.util.Collections; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java index 395e01e52c16..a66b1653369e 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java @@ -33,7 +33,8 @@ import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; -import org.wso2.carbon.identity.flow.execution.engine.executor.InFlowExtensionExecutor.ExecutorResult; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor.ExecutorResult; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java index 19c81a4b865d..92d06ad41b78 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java @@ -30,6 +30,10 @@ import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.HierarchicalPrefixMatcher; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionEvent; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionRequestBuilder; 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.mgt.model.NodeConfig; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java index ea0f71492e8d..96e6d6088425 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java @@ -36,6 +36,8 @@ import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionResponseProcessor; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; From c6526ba55f5fe82c3dd05f92e42455b18e0df408 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Thu, 5 Mar 2026 18:40:59 +0530 Subject: [PATCH 06/41] Add In-Flow Extension Action Management and Access Configuration - Introduced InFlowExtensionActionDTOModelResolver for managing In-Flow Extension action properties. - Created AccessConfig model to define exposed paths and allowed operations for In-Flow Extension actions. - Implemented InFlowExtensionAction class to extend base Action with AccessConfig and flow type overrides. - Enhanced FlowExecutionEngineDataHolder to include ActionManagementService for managing actions. - Registered In-Flow Extension action management services in FlowExecutionEngineServiceComponent. - Added FlowUpdateInterceptor interface to allow pre-persistence processing of flow updates. - Updated FlowMgtService to notify interceptors before flow persistence. - Implemented FlowMgtServiceDataHolder to manage flow update interceptors. --- .../action/management/api/model/Action.java | 5 +- .../impl/ActionDTOModelResolverFactory.java | 2 + .../service/impl/ActionConverterFactory.java | 10 +- .../impl/ActionManagementServiceImpl.java | 10 +- .../management/model/ActionTypesTest.java | 10 +- .../pom.xml | 8 + .../executor/InFlowExtensionExecutor.java | 106 +++-- .../AccessConfigFlowUpdateInterceptor.java | 174 ++++++++ .../InFlowExtensionActionConstants.java | 78 ++++ .../InFlowExtensionActionConverter.java | 162 ++++++++ ...InFlowExtensionActionDTOModelResolver.java | 388 ++++++++++++++++++ .../inflow/extension/model/AccessConfig.java | 76 ++++ .../model/InFlowExtensionAction.java | 250 +++++++++++ .../FlowExecutionEngineDataHolder.java | 22 + .../FlowExecutionEngineServiceComponent.java | 36 ++ .../identity/flow/mgt/FlowMgtService.java | 10 + .../flow/mgt/FlowUpdateInterceptor.java | 52 +++ .../mgt/internal/FlowMgtServiceComponent.java | 17 + .../internal/FlowMgtServiceDataHolder.java | 21 + 19 files changed, 1386 insertions(+), 51 deletions(-) create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowUpdateInterceptor.java diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java index 2dfacceff73b..4b238f69af1f 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java @@ -81,7 +81,7 @@ public enum ActionTypes { "IN_FLOW_EXTENSION", "In-Flow Extension", "Configure an extension point within an any flow via a custom service.", - Category.IN_FLOW); + Category.EXTENSION); private final String pathParam; private final String actionType; @@ -137,7 +137,8 @@ public static ActionTypes[] filterByCategory(Category category) { */ public enum Category { PRE_POST, - IN_FLOW + IN_FLOW, + EXTENSION } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/dao/impl/ActionDTOModelResolverFactory.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/dao/impl/ActionDTOModelResolverFactory.java index 10de38ca1a09..6fdc4186b4af 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/dao/impl/ActionDTOModelResolverFactory.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/dao/impl/ActionDTOModelResolverFactory.java @@ -46,6 +46,8 @@ public static ActionDTOModelResolver getActionDTOModelResolver(Action.ActionType return actionDTOModelResolvers.get(Action.ActionTypes.PRE_UPDATE_PASSWORD); case PRE_ISSUE_ACCESS_TOKEN: return actionDTOModelResolvers.get(Action.ActionTypes.PRE_ISSUE_ACCESS_TOKEN); + case IN_FLOW_EXTENSION: + return actionDTOModelResolvers.get(Action.ActionTypes.IN_FLOW_EXTENSION); default: return null; } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionConverterFactory.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionConverterFactory.java index a4a50dd8a307..ec8ec72c4a3a 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionConverterFactory.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionConverterFactory.java @@ -39,15 +39,7 @@ private ActionConverterFactory() { public static ActionConverter getActionConverter(Action.ActionTypes actionType) { - switch (actionType) { - case PRE_UPDATE_PROFILE: - return actionConverters.get(Action.ActionTypes.PRE_UPDATE_PROFILE); - case PRE_UPDATE_PASSWORD: - return actionConverters.get(Action.ActionTypes.PRE_UPDATE_PASSWORD); - case PRE_ISSUE_ACCESS_TOKEN: - default: - return null; - } + return actionConverters.get(actionType); } public static void registerActionConverter(ActionConverter actionConverter) { diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java index 2f023d603b0f..619505c374d4 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java @@ -317,8 +317,10 @@ private String getActionTypeFromPath(String actionType) throws ActionMgtClientEx */ private void validateMaxActionsPerType(String actionType, String tenantDomain) throws ActionMgtException { - // In-flow actions are not limited by the maximum actions per action type; eg: AUTHENTICATION action type. - if (Action.ActionTypes.Category.IN_FLOW.equals(Action.ActionTypes.valueOf(actionType).getCategory())) { + // In-flow and extension actions are not limited by the maximum actions per action type. + Action.ActionTypes.Category category = Action.ActionTypes.valueOf(actionType).getCategory(); + if (Action.ActionTypes.Category.IN_FLOW.equals(category) + || Action.ActionTypes.Category.EXTENSION.equals(category)) { return; } Map actionsCountPerType = getActionsCountPerType(tenantDomain); @@ -365,8 +367,8 @@ private ActionDTO buildActionDTOForCreation(String actionType, String actionId, throws ActionMgtServerException { Action.ActionTypes resolvedActionType = Action.ActionTypes.valueOf(actionType); - Action.Status resolvedStatus = resolvedActionType.getCategory() == Action.ActionTypes.Category.IN_FLOW ? - Action.Status.ACTIVE : Action.Status.INACTIVE; + Action.Status resolvedStatus = resolvedActionType.getCategory() == Action.ActionTypes.Category.PRE_POST ? + Action.Status.INACTIVE : Action.Status.ACTIVE; String actionVersion = ActionManagementConfig.getInstance().getLatestVersion(resolvedActionType); diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java index 250805692876..1c17dc2d0478 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java @@ -52,7 +52,11 @@ public Object[][] actionTypesProvider() { {Action.ActionTypes.PRE_ISSUE_ID_TOKEN, "preIssueIdToken", "PRE_ISSUE_ID_TOKEN", "Pre Issue ID Token", "Configure an extension point for modifying ID token via a custom service.", - Action.ActionTypes.Category.PRE_POST} + Action.ActionTypes.Category.PRE_POST}, + {Action.ActionTypes.IN_FLOW_EXTENSION, "inFlowExtension", "IN_FLOW_EXTENSION", + "In-Flow Extension", + "Configure an extension point within an any flow via a custom service.", + Action.ActionTypes.Category.EXTENSION} }; } @@ -76,7 +80,9 @@ public Object[][] filterByCategoryProvider() { new Action.ActionTypes[]{Action.ActionTypes.PRE_ISSUE_ACCESS_TOKEN, Action.ActionTypes.PRE_UPDATE_PASSWORD, Action.ActionTypes.PRE_UPDATE_PROFILE, Action.ActionTypes.PRE_REGISTRATION, Action.ActionTypes.PRE_ISSUE_ID_TOKEN}}, - {Action.ActionTypes.Category.IN_FLOW, new Action.ActionTypes[]{Action.ActionTypes.AUTHENTICATION}} + {Action.ActionTypes.Category.IN_FLOW, new Action.ActionTypes[]{Action.ActionTypes.AUTHENTICATION}}, + {Action.ActionTypes.Category.EXTENSION, + new Action.ActionTypes[]{Action.ActionTypes.IN_FLOW_EXTENSION}} }; } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml index 0cb418592268..178d4f046935 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml @@ -74,6 +74,10 @@ org.wso2.carbon.identity.framework org.wso2.carbon.identity.action.execution + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.action.management + org.wso2.carbon.identity.framework org.wso2.carbon.identity.claim.metadata.mgt @@ -176,6 +180,8 @@ org.wso2.carbon.identity.event.*; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.action.execution.api.*; version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.action.management.api.*; + version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.central.log.mgt.utils; version="${carbon.identity.package.import.version.range}", com.fasterxml.jackson.core.*; version="${com.fasterxml.jackson.annotation.version.range}", @@ -198,6 +204,8 @@ org.wso2.carbon.identity.flow.execution.engine.validation, org.wso2.carbon.identity.flow.execution.engine.exception, org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor, + org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model, + org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management, org.wso2.carbon.identity.flow.execution.engine.graph; version="${carbon.identity.package.export.version}" diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java index d0b694aa4fe7..5b238cc749fb 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java @@ -19,7 +19,6 @@ package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -30,9 +29,14 @@ import org.wso2.carbon.identity.action.execution.api.model.Failure; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; +import org.wso2.carbon.identity.action.management.api.exception.ActionMgtException; +import org.wso2.carbon.identity.action.management.api.model.Action; +import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; 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.graph.Executor; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; @@ -51,8 +55,9 @@ * *

Execution lifecycle:

*
    - *
  1. Extract executor metadata: {@code actionId}, {@code expose}, {@code allowedOperations}.
  2. - *
  3. Build the final expose list
  4. + *
  5. Extract executor metadata: {@code actionId}.
  6. + *
  7. Resolve access config from the action (with per-flow-type override support via + * {@link ActionManagementService}). Falls back to system defaults if unavailable.
  8. *
  9. Build a minimal {@link FlowContext} containing only three entries: * the full {@link FlowExecutionContext}, the expose list, and the allowed operations * JSON. The request builder will use these to construct the filtered request.
  10. @@ -61,6 +66,10 @@ * Context updates are already applied directly to the {@link FlowExecutionContext} * by the response processor. *
+ * + *

Note: Access config is never read from executor metadata. It is stored exclusively + * in action properties (default and per-flow-type overrides in {@code IDN_ACTION_PROPERTIES}). + * Executor metadata only provides the {@code actionId}.

*/ public class InFlowExtensionExecutor implements Executor { @@ -68,16 +77,11 @@ public class InFlowExtensionExecutor implements Executor { private static final String EXECUTOR_NAME = "ExtensionExecutor"; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final TypeReference> STRING_LIST_TYPE_REF = - new TypeReference>() { }; - public static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; public static final String EXPOSE_KEY = "expose"; public static final String ALLOWED_OPERATIONS_KEY = "allowedOperations"; public static final String PATH_TYPE_ANNOTATIONS_KEY = "pathTypeAnnotations"; private static final String ACTION_ID_METADATA_KEY = "actionId"; - private static final String ALLOWED_OPERATIONS_METADATA_KEY = "allowedOperations"; - private static final String EXPOSE_METADATA_KEY = "expose"; @Override public String getName() { @@ -102,14 +106,32 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE LOG.debug("Executing In-Flow Extension action with ID: " + actionId); } - String exposeJson = getMetadataValue(context, EXPOSE_METADATA_KEY); - List expose = buildExposeList(exposeJson); + // Resolve access config exclusively from the action (with flow-type override support). + // Access config is NOT stored in executor metadata — it is stored in action properties. + AccessConfig resolvedConfig = resolveAccessConfigFromAction(actionId, context); + + List expose; + String allowedOpsJson = null; + + if (resolvedConfig != null && resolvedConfig.getExpose() != null) { + expose = resolvedConfig.getExpose(); + } else { + // No access config on action — use system defaults. + expose = new ArrayList<>(HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + } + + if (resolvedConfig != null && resolvedConfig.getAllowedOperations() != null) { + try { + allowedOpsJson = OBJECT_MAPPER.writeValueAsString(resolvedConfig.getAllowedOperations()); + } catch (JsonProcessingException e) { + LOG.error("Failed to serialize resolved allowed operations.", e); + } + } FlowContext flowContext = FlowContext.create() .add(FLOW_EXECUTION_CONTEXT_KEY, context) .add(EXPOSE_KEY, expose); - String allowedOpsJson = getMetadataValue(context, ALLOWED_OPERATIONS_METADATA_KEY); if (allowedOpsJson != null) { flowContext.add(ALLOWED_OPERATIONS_KEY, allowedOpsJson); } @@ -149,29 +171,6 @@ public ExecutorResponse rollback(FlowExecutionContext context) { return null; } - /** - * Build the effective expose list from executor metadata. - * - * @param exposeJson JSON array of string prefixes from metadata. - * @return The effective expose list. - */ - private List buildExposeList(String exposeJson) { - - List expose; - if (exposeJson != null && !exposeJson.isEmpty()) { - try { - expose = new ArrayList<>(OBJECT_MAPPER.readValue(exposeJson, STRING_LIST_TYPE_REF)); - } catch (JsonProcessingException e) { - LOG.error("Failed to parse expose configuration: " + e.getMessage(), e); - expose = new ArrayList<>(HierarchicalPrefixMatcher.DEFAULT_EXPOSE); - } - } else { - expose = new ArrayList<>(HierarchicalPrefixMatcher.DEFAULT_EXPOSE); - } - - return Collections.unmodifiableList(expose); - } - /** * Map the {@link ActionExecutionStatus} to an {@link ExecutorResponse}. * Only performs status translation — context updates are handled by the response processor. @@ -268,6 +267,45 @@ private ActionExecutorService getActionExecutorService() { return FlowExecutionEngineDataHolder.getInstance().getActionExecutorService(); } + private ActionManagementService getActionManagementService() { + + return FlowExecutionEngineDataHolder.getInstance().getActionManagementService(); + } + + /** + * Resolve the effective access config for the given action ID and flow context. + * Uses the action's flow-type-specific overrides if available, otherwise falls back to + * the action's default access config. + * + * @param actionId The action ID. + * @param context The flow execution context (contains flow type and tenant info). + * @return The resolved AccessConfig, or {@code null} if the action cannot be resolved. + */ + private AccessConfig resolveAccessConfigFromAction(String actionId, FlowExecutionContext context) { + + ActionManagementService actionMgtService = getActionManagementService(); + if (actionMgtService == null) { + LOG.warn("ActionManagementService is not available. Using system defaults for access config."); + return null; + } + + try { + Action action = actionMgtService.getActionByActionId( + Action.ActionTypes.IN_FLOW_EXTENSION.getActionType(), + actionId, context.getTenantDomain()); + + if (action instanceof InFlowExtensionAction) { + InFlowExtensionAction extensionAction = (InFlowExtensionAction) action; + String flowType = context.getFlowType(); + return extensionAction.resolveAccessConfig(flowType); + } + } catch (ActionMgtException e) { + LOG.error("Error retrieving action " + actionId + " for access config resolution. " + + "Using system defaults.", e); + } + return null; + } + /** * Read a single metadata value from the current node's executor configuration. * diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java new file mode 100644 index 000000000000..4f989dfe19d4 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2025, 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.inflow.extension.management; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.action.management.api.exception.ActionMgtException; +import org.wso2.carbon.identity.action.management.api.model.Action; +import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; +import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; +import org.wso2.carbon.identity.flow.mgt.FlowUpdateInterceptor; +import org.wso2.carbon.identity.flow.mgt.exception.FlowMgtFrameworkException; +import org.wso2.carbon.identity.flow.mgt.exception.FlowMgtServerException; +import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; +import org.wso2.carbon.identity.flow.mgt.model.GraphConfig; +import org.wso2.carbon.identity.flow.mgt.model.NodeConfig; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_METADATA_KEY; + +/** + * Flow update interceptor that extracts access config overrides from executor metadata + * and stores them as per-flow-type action properties. + *

+ * This interceptor is invoked before the flow graph is persisted. It: + *

    + *
  1. Iterates graph nodes looking for In-Flow Extension executors with an + * {@code accessConfig} metadata entry.
  2. + *
  3. Parses the unified {@code accessConfig} JSON object + * ({@code {"expose": [...], "allowedOperations": [...]}}).
  4. + *
  5. Saves the parsed data as per-flow-type override properties on the corresponding action + * via {@code ActionManagementService.updateAction()}.
  6. + *
  7. Strips the {@code accessConfig} key from executor metadata so it is NOT persisted + * to the flow executor metadata table (avoiding VARCHAR(255) column size limitations).
  8. + *
+ *

+ */ +public class AccessConfigFlowUpdateInterceptor implements FlowUpdateInterceptor { + + private static final Log LOG = LogFactory.getLog(AccessConfigFlowUpdateInterceptor.class); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final TypeReference> MAP_TYPE_REF = + new TypeReference>() { }; + + private static final String ACTION_ID_METADATA_KEY = "actionId"; + private static final String EXPOSE_FIELD = "expose"; + private static final String ALLOWED_OPERATIONS_FIELD = "allowedOperations"; + + @Override + public void onFlowUpdate(String flowType, GraphConfig graphConfig, int tenantId) + throws FlowMgtFrameworkException { + + if (graphConfig == null || graphConfig.getNodeConfigs() == null) { + return; + } + + ActionManagementService actionMgtService = + FlowExecutionEngineDataHolder.getInstance().getActionManagementService(); + if (actionMgtService == null) { + LOG.warn("ActionManagementService is not available. Skipping access config override processing."); + return; + } + + String tenantDomain = IdentityTenantUtil.getTenantDomain(tenantId); + + for (NodeConfig node : graphConfig.getNodeConfigs().values()) { + processNode(node, flowType, tenantDomain, actionMgtService); + } + } + + /** + * Process a single node: extract unified accessConfig from executor metadata, save as action + * properties, and strip the accessConfig key from metadata before DAO persistence. + */ + @SuppressWarnings("unchecked") + private void processNode(NodeConfig node, String flowType, String tenantDomain, + ActionManagementService actionMgtService) throws FlowMgtFrameworkException { + + ExecutorDTO executorConfig = node.getExecutorConfig(); + if (executorConfig == null || executorConfig.getMetadata() == null) { + return; + } + + Map metadata = executorConfig.getMetadata(); + String actionId = metadata.get(ACTION_ID_METADATA_KEY); + if (actionId == null || actionId.isEmpty()) { + return; + } + + String accessConfigJson = metadata.get(ACCESS_CONFIG_METADATA_KEY); + if (accessConfigJson == null) { + return; + } + + // Always strip the accessConfig key from metadata — it must NOT be persisted + // to executor metadata (VARCHAR(255) column size limitation). + metadata.remove(ACCESS_CONFIG_METADATA_KEY); + + try { + // Parse the unified accessConfig JSON: {"expose": [...], "allowedOperations": [...]} + Map accessConfigMap = OBJECT_MAPPER.readValue(accessConfigJson, MAP_TYPE_REF); + + List expose = accessConfigMap.containsKey(EXPOSE_FIELD) + ? (List) accessConfigMap.get(EXPOSE_FIELD) : null; + List> allowedOps = accessConfigMap.containsKey(ALLOWED_OPERATIONS_FIELD) + ? (List>) accessConfigMap.get(ALLOWED_OPERATIONS_FIELD) : null; + AccessConfig overrideConfig = new AccessConfig(expose, allowedOps); + + // Retrieve the existing action to merge overrides (preserving other flow types). + Action currentAction = actionMgtService.getActionByActionId( + Action.ActionTypes.IN_FLOW_EXTENSION.getPathParam(), actionId, tenantDomain); + if (!(currentAction instanceof InFlowExtensionAction)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Action " + actionId + " is not an InFlowExtensionAction. Skipping override."); + } + return; + } + + InFlowExtensionAction existingAction = (InFlowExtensionAction) currentAction; + + // Merge: preserve other flow types, add/replace this one. + Map overrides = new HashMap<>(existingAction.getFlowTypeOverrides()); + overrides.put(flowType, overrideConfig); + + InFlowExtensionAction updatedAction = new InFlowExtensionAction.RequestBuilder(existingAction) + .accessConfig(existingAction.getAccessConfig()) + .flowTypeOverrides(overrides) + .build(); + + actionMgtService.updateAction( + Action.ActionTypes.IN_FLOW_EXTENSION.getPathParam(), + actionId, updatedAction, tenantDomain); + + if (LOG.isDebugEnabled()) { + LOG.debug("Updated access config override for action " + actionId + + " in flow type " + flowType + " and stripped from executor metadata."); + } + + } catch (ActionMgtException e) { + throw new FlowMgtServerException("FLOW-60010", + "Error updating access config override.", + "Error updating access config override for action: " + actionId, e); + } catch (JsonProcessingException e) { + throw new FlowMgtServerException("FLOW-60011", + "Error parsing access config metadata.", + "Error parsing access config metadata for action: " + actionId, e); + } + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java new file mode 100644 index 000000000000..2dbdaae25e70 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025, 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.inflow.extension.management; + +/** + * Constants for In-Flow Extension action management. + */ +public class InFlowExtensionActionConstants { + + /** + * Property key for the expose path prefixes stored in IDN_ACTION_PROPERTIES. + * Stored as a BLOB (JSON-serialized list of strings). + */ + public static final String ACCESS_CONFIG_EXPOSE = "ACCESS_CONFIG_EXPOSE"; + + /** + * Property key for the allowed operations stored in IDN_ACTION_PROPERTIES. + * Stored as a BLOB (JSON-serialized list of operation descriptors). + */ + public static final String ACCESS_CONFIG_ALLOWED_OPERATIONS = "ACCESS_CONFIG_ALLOWED_OPERATIONS"; + + /** + * Separator used between property key and flow type in override keys. + * For example: "ACCESS_CONFIG_EXPOSE:REGISTRATION" + */ + public static final String OVERRIDE_KEY_SEPARATOR = ":"; + + /** + * Prefix for flow-type-specific expose override properties. + * Full key format: "ACCESS_CONFIG_EXPOSE:<FLOW_TYPE>" + */ + public static final String ACCESS_CONFIG_EXPOSE_PREFIX = ACCESS_CONFIG_EXPOSE + OVERRIDE_KEY_SEPARATOR; + + /** + * Prefix for flow-type-specific allowed operations override properties. + * Full key format: "ACCESS_CONFIG_ALLOWED_OPERATIONS:<FLOW_TYPE>" + */ + public static final String ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX = + ACCESS_CONFIG_ALLOWED_OPERATIONS + OVERRIDE_KEY_SEPARATOR; + + /** + * Metadata key used in executor metadata (flow configuration) to pass a unified access config + * object containing both {@code expose} and {@code allowedOperations}. + *

+ * The value is a JSON object: {@code {"expose": [...], "allowedOperations": [...]}} + *

+ *

+ * This key is extracted by the {@code AccessConfigFlowUpdateInterceptor} during flow updates, + * stored as per-flow-type action properties, and stripped from executor metadata before + * the flow graph is persisted (to avoid VARCHAR(255) column size limitations). + *

+ */ + public static final String ACCESS_CONFIG_METADATA_KEY = "accessConfig"; + + /** + * Maximum number of expose path prefixes allowed per action. + */ + public static final int MAX_EXPOSE_PATHS = 50; + + private InFlowExtensionActionConstants() { + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java new file mode 100644 index 000000000000..fea0f774718d --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2025, 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.inflow.extension.management; + +import org.wso2.carbon.identity.action.management.api.model.Action; +import org.wso2.carbon.identity.action.management.api.model.ActionDTO; +import org.wso2.carbon.identity.action.management.api.model.ActionProperty; +import org.wso2.carbon.identity.action.management.api.service.ActionConverter; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_ALLOWED_OPERATIONS; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE_PREFIX; + +/** + * ActionConverter implementation for In-Flow Extension actions. + *

+ * Handles the conversion between {@link InFlowExtensionAction} (domain model) and + * {@link ActionDTO} (data transfer object) by mapping the {@link AccessConfig} fields + * to/from action properties. + *

+ */ +public class InFlowExtensionActionConverter implements ActionConverter { + + @Override + public Action.ActionTypes getSupportedActionType() { + + return Action.ActionTypes.IN_FLOW_EXTENSION; + } + + /** + * Converts an {@link InFlowExtensionAction} to an {@link ActionDTO} for persistence. + * Maps the access config fields (expose, allowedOperations) and flow type overrides + * into the DTO's properties map using prefixed keys. + * + * @param action The InFlowExtensionAction to convert. + * @return ActionDTO with access config properties. + */ + @Override + public ActionDTO buildActionDTO(Action action) { + + if (!(action instanceof InFlowExtensionAction)) { + return new ActionDTO.Builder(action).build(); + } + + InFlowExtensionAction inFlowExtensionAction = (InFlowExtensionAction) action; + AccessConfig accessConfig = inFlowExtensionAction.getAccessConfig(); + + Map properties = new HashMap<>(); + // Default access config (no prefix). + if (accessConfig != null) { + if (accessConfig.getExpose() != null) { + properties.put(ACCESS_CONFIG_EXPOSE, + new ActionProperty.BuilderForService(accessConfig.getExpose()).build()); + } + if (accessConfig.getAllowedOperations() != null) { + properties.put(ACCESS_CONFIG_ALLOWED_OPERATIONS, + new ActionProperty.BuilderForService(accessConfig.getAllowedOperations()).build()); + } + } + + // Per-flow-type overrides using prefixed keys. + Map overrides = inFlowExtensionAction.getFlowTypeOverrides(); + if (overrides != null) { + for (Map.Entry entry : overrides.entrySet()) { + String flowType = entry.getKey(); + AccessConfig overrideConfig = entry.getValue(); + if (overrideConfig.getExpose() != null) { + properties.put(ACCESS_CONFIG_EXPOSE_PREFIX + flowType, + new ActionProperty.BuilderForService(overrideConfig.getExpose()).build()); + } + if (overrideConfig.getAllowedOperations() != null) { + properties.put(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX + flowType, + new ActionProperty.BuilderForService(overrideConfig.getAllowedOperations()).build()); + } + } + } + + return new ActionDTO.Builder(inFlowExtensionAction) + .properties(properties) + .build(); + } + + /** + * Converts an {@link ActionDTO} back to an {@link InFlowExtensionAction}. + * Reconstructs the default {@link AccessConfig} and per-flow-type overrides from the DTO's properties map. + * + * @param actionDTO The ActionDTO to convert. + * @return InFlowExtensionAction with access config and overrides populated. + */ + @SuppressWarnings("unchecked") + @Override + public Action buildAction(ActionDTO actionDTO) { + + // Default access config. + List expose = (List) actionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE); + List> allowedOperations = + (List>) actionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS); + + AccessConfig accessConfig = null; + if (expose != null || allowedOperations != null) { + accessConfig = new AccessConfig(expose, allowedOperations); + } + + // Reconstruct per-flow-type overrides from prefixed keys. + Map flowTypeOverrides = new HashMap<>(); + if (actionDTO.getProperties() != null) { + for (String propertyKey : actionDTO.getProperties().keySet()) { + if (propertyKey.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX)) { + String flowType = propertyKey.substring(ACCESS_CONFIG_EXPOSE_PREFIX.length()); + AccessConfig existing = flowTypeOverrides.getOrDefault(flowType, new AccessConfig(null, null)); + flowTypeOverrides.put(flowType, new AccessConfig( + (List) actionDTO.getPropertyValue(propertyKey), + existing.getAllowedOperations())); + } else if (propertyKey.startsWith(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX)) { + String flowType = propertyKey.substring(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX.length()); + AccessConfig existing = flowTypeOverrides.getOrDefault(flowType, new AccessConfig(null, null)); + flowTypeOverrides.put(flowType, new AccessConfig( + existing.getExpose(), + (List>) actionDTO.getPropertyValue(propertyKey))); + } + } + } + + return new InFlowExtensionAction.ResponseBuilder() + .id(actionDTO.getId()) + .type(actionDTO.getType()) + .name(actionDTO.getName()) + .description(actionDTO.getDescription()) + .status(actionDTO.getStatus()) + .actionVersion(actionDTO.getActionVersion()) + .createdAt(actionDTO.getCreatedAt()) + .updatedAt(actionDTO.getUpdatedAt()) + .endpoint(actionDTO.getEndpoint()) + .accessConfig(accessConfig) + .flowTypeOverrides(flowTypeOverrides) + .rule(actionDTO.getActionRule()) + .build(); + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java new file mode 100644 index 000000000000..ba420ce5af44 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2025, 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.inflow.extension.management; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.action.management.api.exception.ActionDTOModelResolverClientException; +import org.wso2.carbon.identity.action.management.api.exception.ActionDTOModelResolverException; +import org.wso2.carbon.identity.action.management.api.model.Action; +import org.wso2.carbon.identity.action.management.api.model.ActionDTO; +import org.wso2.carbon.identity.action.management.api.model.ActionProperty; +import org.wso2.carbon.identity.action.management.api.model.BinaryObject; +import org.wso2.carbon.identity.action.management.api.service.ActionDTOModelResolver; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.util.Collections.emptyList; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_ALLOWED_OPERATIONS; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE_PREFIX; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.MAX_EXPOSE_PATHS; + +/** + * ActionDTOModelResolver implementation for In-Flow Extension actions. + *

+ * Responsible for validating and transforming access config properties (expose paths and + * allowed operations) between the service layer representation and the DAO layer BLOB format. + *

+ * + *
    + *
  • Add operation: Validates expose paths and allowed operations, then serializes + * them to JSON {@link BinaryObject}s for BLOB storage in IDN_ACTION_PROPERTIES.
  • + *
  • Get operation: Deserializes BLOBs back to service-layer list objects.
  • + *
  • Update operation: Validates updated values or preserves existing ones (PUT semantics).
  • + *
  • Delete operation: No-op (properties are cascade-deleted with the action).
  • + *
+ */ +public class InFlowExtensionActionDTOModelResolver implements ActionDTOModelResolver { + + private static final Log LOG = LogFactory.getLog(InFlowExtensionActionDTOModelResolver.class); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final TypeReference> STRING_LIST_TYPE_REF = + new TypeReference>() { }; + private static final TypeReference>> MAP_LIST_TYPE_REF = + new TypeReference>>() { }; + + @Override + public Action.ActionTypes getSupportedActionType() { + + return Action.ActionTypes.IN_FLOW_EXTENSION; + } + + @Override + public ActionDTO resolveForAddOperation(ActionDTO actionDTO, String tenantDomain) + throws ActionDTOModelResolverException { + + Map properties = new HashMap<>(); + + Object exposeValue = actionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE); + // Expose is an optional field. + if (exposeValue != null) { + List validatedExpose = validateExpose(exposeValue); + properties.put(ACCESS_CONFIG_EXPOSE, createBlobProperty(validatedExpose)); + } + + Object allowedOpsValue = actionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS); + // Allowed operations is an optional field. + if (allowedOpsValue != null) { + List> validatedOps = validateAllowedOperations(allowedOpsValue); + properties.put(ACCESS_CONFIG_ALLOWED_OPERATIONS, createBlobProperty(validatedOps)); + } + + // Handle per-flow-type override properties (prefixed keys). + if (actionDTO.getProperties() != null) { + for (Map.Entry entry : actionDTO.getProperties().entrySet()) { + String key = entry.getKey(); + if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX)) { + Object overrideExpose = actionDTO.getPropertyValue(key); + if (overrideExpose != null) { + List validatedOverrideExpose = validateExpose(overrideExpose); + properties.put(key, createBlobProperty(validatedOverrideExpose)); + } + } else if (key.startsWith(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX)) { + Object overrideOps = actionDTO.getPropertyValue(key); + if (overrideOps != null) { + List> validatedOverrideOps = validateAllowedOperations(overrideOps); + properties.put(key, createBlobProperty(validatedOverrideOps)); + } + } + } + } + + return new ActionDTO.Builder(actionDTO) + .properties(properties) + .build(); + } + + @Override + public ActionDTO resolveForGetOperation(ActionDTO actionDTO, String tenantDomain) + throws ActionDTOModelResolverException { + + Map properties = new HashMap<>(); + + // Default access config properties. + if (actionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE) != null) { + properties.put(ACCESS_CONFIG_EXPOSE, deserializeExposeProperty( + ((BinaryObject) actionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE)).getJSONString())); + } + + if (actionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS) != null) { + properties.put(ACCESS_CONFIG_ALLOWED_OPERATIONS, deserializeAllowedOpsProperty( + ((BinaryObject) actionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS)).getJSONString())); + } + + // Deserialize per-flow-type override properties (prefixed keys). + if (actionDTO.getProperties() != null) { + for (Map.Entry entry : actionDTO.getProperties().entrySet()) { + String key = entry.getKey(); + if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX) + && actionDTO.getPropertyValue(key) != null) { + properties.put(key, deserializeExposeProperty( + ((BinaryObject) actionDTO.getPropertyValue(key)).getJSONString())); + } else if (key.startsWith(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX) + && actionDTO.getPropertyValue(key) != null) { + properties.put(key, deserializeAllowedOpsProperty( + ((BinaryObject) actionDTO.getPropertyValue(key)).getJSONString())); + } + } + } + + return new ActionDTO.Builder(actionDTO) + .properties(properties) + .build(); + } + + @Override + public List resolveForGetOperation(List actionDTOList, String tenantDomain) + throws ActionDTOModelResolverException { + + List resolvedList = new ArrayList<>(); + for (ActionDTO actionDTO : actionDTOList) { + resolvedList.add(resolveForGetOperation(actionDTO, tenantDomain)); + } + return resolvedList; + } + + /** + * Resolves the actionDTO for the update operation. + * When properties are updated, the existing properties are replaced with the new properties. + * When properties are not updated, the existing properties should be sent to the upstream component. + * + * @param updatingActionDTO ActionDTO that needs to be updated. + * @param existingActionDTO Existing ActionDTO. + * @param tenantDomain Tenant domain. + * @return Resolved ActionDTO. + * @throws ActionDTOModelResolverException ActionDTOModelResolverException. + */ + @Override + public ActionDTO resolveForUpdateOperation(ActionDTO updatingActionDTO, ActionDTO existingActionDTO, + String tenantDomain) throws ActionDTOModelResolverException { + + Map properties = new HashMap<>(); + + // Action Properties updating operation is treated as a PUT in DAO layer. Therefore if no properties are + // updated the existing properties should be sent to the DAO layer. + + // Handle default access config properties. + List expose = getResolvedUpdatingExpose(updatingActionDTO, existingActionDTO); + if (!expose.isEmpty()) { + properties.put(ACCESS_CONFIG_EXPOSE, createBlobProperty(expose)); + } + + List> allowedOps = + getResolvedUpdatingAllowedOps(updatingActionDTO, existingActionDTO); + if (!allowedOps.isEmpty()) { + properties.put(ACCESS_CONFIG_ALLOWED_OPERATIONS, createBlobProperty(allowedOps)); + } + + // Handle per-flow-type override properties. Since DAO treats update as PUT (full replace), + // we must carry forward all existing overrides that are not explicitly being updated. + // First, carry forward all existing per-flow-type overrides. + if (existingActionDTO.getProperties() != null) { + for (Map.Entry entry : existingActionDTO.getProperties().entrySet()) { + String key = entry.getKey(); + if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX) + || key.startsWith(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX)) { + properties.put(key, createBlobProperty(existingActionDTO.getPropertyValue(key))); + } + } + } + // Then, overlay with any explicitly updated per-flow-type overrides. + if (updatingActionDTO.getProperties() != null) { + for (Map.Entry entry : updatingActionDTO.getProperties().entrySet()) { + String key = entry.getKey(); + if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX)) { + Object overrideExpose = updatingActionDTO.getPropertyValue(key); + if (overrideExpose != null) { + List validatedOverrideExpose = validateExpose(overrideExpose); + properties.put(key, createBlobProperty(validatedOverrideExpose)); + } + } else if (key.startsWith(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX)) { + Object overrideOps = updatingActionDTO.getPropertyValue(key); + if (overrideOps != null) { + List> validatedOverrideOps = validateAllowedOperations(overrideOps); + properties.put(key, createBlobProperty(validatedOverrideOps)); + } + } + } + } + + return new ActionDTO.Builder(updatingActionDTO) + .properties(properties) + .build(); + } + + @Override + public void resolveForDeleteOperation(ActionDTO deletingActionDTO, String tenantDomain) + throws ActionDTOModelResolverException { + // No-op: properties are cascade-deleted with the action in IDN_ACTION_PROPERTIES. + } + + // ---- Update helpers ---- + + @SuppressWarnings("unchecked") + private List getResolvedUpdatingExpose(ActionDTO updatingActionDTO, ActionDTO existingActionDTO) + throws ActionDTOModelResolverException { + + if (updatingActionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE) != null) { + return validateExpose(updatingActionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE)); + } else if (existingActionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE) != null) { + return (List) existingActionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE); + } + return emptyList(); + } + + @SuppressWarnings("unchecked") + private List> getResolvedUpdatingAllowedOps(ActionDTO updatingActionDTO, + ActionDTO existingActionDTO) + throws ActionDTOModelResolverException { + + if (updatingActionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS) != null) { + return validateAllowedOperations(updatingActionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS)); + } else if (existingActionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS) != null) { + return (List>) existingActionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS); + } + return emptyList(); + } + + // ---- Validation ---- + + @SuppressWarnings("unchecked") + private List validateExpose(Object exposeValue) throws ActionDTOModelResolverException { + + if (!(exposeValue instanceof List)) { + throw new ActionDTOModelResolverClientException("Invalid expose format.", + "Expose should be provided as a list of strings."); + } + + List exposeList = (List) exposeValue; + for (Object item : exposeList) { + if (!(item instanceof String)) { + throw new ActionDTOModelResolverClientException("Invalid expose format.", + "Expose must contain only string values."); + } + } + + List expose = (List) exposeValue; + validateExposeCount(expose); + validateExposePathFormat(expose); + return expose; + } + + private void validateExposeCount(List expose) throws ActionDTOModelResolverClientException { + + if (expose.size() > MAX_EXPOSE_PATHS) { + throw new ActionDTOModelResolverClientException("Maximum expose paths limit exceeded.", + String.format("The number of configured expose paths: %d exceeds the maximum allowed limit: %d.", + expose.size(), MAX_EXPOSE_PATHS)); + } + } + + private void validateExposePathFormat(List expose) throws ActionDTOModelResolverClientException { + + Set seen = new HashSet<>(); + for (String path : expose) { + if (path == null || path.trim().isEmpty()) { + throw new ActionDTOModelResolverClientException("Invalid expose path.", + "Expose paths must not be null or empty."); + } + if (!path.startsWith("/")) { + throw new ActionDTOModelResolverClientException("Invalid expose path.", + String.format("Expose path '%s' must start with '/'.", path)); + } + if (!seen.add(path)) { + throw new ActionDTOModelResolverClientException("Duplicate expose path.", + String.format("The expose path '%s' is duplicated.", path)); + } + } + } + + @SuppressWarnings("unchecked") + private List> validateAllowedOperations(Object allowedOpsValue) + throws ActionDTOModelResolverClientException { + + if (!(allowedOpsValue instanceof List)) { + throw new ActionDTOModelResolverClientException("Invalid allowed operations format.", + "Allowed operations should be provided as a list."); + } + + List opsList = (List) allowedOpsValue; + for (Object item : opsList) { + if (!(item instanceof Map)) { + throw new ActionDTOModelResolverClientException("Invalid allowed operations format.", + "Each allowed operation must be an object with 'op' and 'paths' keys."); + } + Map opMap = (Map) item; + if (!opMap.containsKey("op") || !(opMap.get("op") instanceof String)) { + throw new ActionDTOModelResolverClientException("Invalid allowed operation.", + "Each allowed operation must contain an 'op' field with a string value."); + } + if (!opMap.containsKey("paths") || !(opMap.get("paths") instanceof List)) { + throw new ActionDTOModelResolverClientException("Invalid allowed operation.", + "Each allowed operation must contain a 'paths' field with a list of strings."); + } + } + + return (List>) allowedOpsValue; + } + + // ---- Serialization helpers ---- + + private ActionProperty createBlobProperty(Object value) throws ActionDTOModelResolverException { + + try { + BinaryObject binaryObject = BinaryObject.fromJsonString(OBJECT_MAPPER.writeValueAsString(value)); + return new ActionProperty.BuilderForDAO(binaryObject).build(); + } catch (JsonProcessingException e) { + throw new ActionDTOModelResolverException("Failed to serialize access config property to JSON.", e); + } + } + + private ActionProperty deserializeExposeProperty(String jsonString) throws ActionDTOModelResolverException { + + try { + List expose = OBJECT_MAPPER.readValue(jsonString, STRING_LIST_TYPE_REF); + return new ActionProperty.BuilderForService(expose).build(); + } catch (IOException e) { + throw new ActionDTOModelResolverException("Error reading expose values from storage.", e); + } + } + + private ActionProperty deserializeAllowedOpsProperty(String jsonString) + throws ActionDTOModelResolverException { + + try { + List> allowedOps = OBJECT_MAPPER.readValue(jsonString, MAP_LIST_TYPE_REF); + return new ActionProperty.BuilderForService(allowedOps).build(); + } catch (IOException e) { + throw new ActionDTOModelResolverException("Error reading allowed operations from storage.", e); + } + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java new file mode 100644 index 000000000000..77164398e91d --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025, 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.inflow.extension.model; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Access Configuration for In-Flow Extension actions. + *

+ * Defines which parts of the flow context are exposed to the external service + * and what operations the service is allowed to perform. + *

+ * + *
    + *
  • {@code expose} – hierarchical path prefixes that control which context data is sent + * in the action execution request (e.g. {@code "/user/claims/"}, {@code "/flow/properties/"}).
  • + *
  • {@code allowedOperations} – a structured list of operation descriptors, where each entry + * specifies an operation type ({@code "op"}) and the paths it applies to ({@code "paths"}).
  • + *
+ */ +public class AccessConfig { + + private final List expose; + private final List> allowedOperations; + + /** + * Constructs an AccessConfig with the given expose paths and allowed operations. + * + * @param expose List of hierarchical path prefixes to expose. May be {@code null}. + * @param allowedOperations List of operation descriptors. May be {@code null}. + */ + public AccessConfig(List expose, List> allowedOperations) { + + this.expose = expose != null ? Collections.unmodifiableList(expose) : null; + this.allowedOperations = allowedOperations != null ? Collections.unmodifiableList(allowedOperations) : null; + } + + /** + * Returns the list of hierarchical path prefixes that define what context data is exposed. + * + * @return Unmodifiable list of expose path prefixes, or {@code null} if not configured. + */ + public List getExpose() { + + return expose; + } + + /** + * Returns the list of allowed operation descriptors. + * Each entry is a map with at least {@code "op"} (operation type) and {@code "paths"} keys. + * + * @return Unmodifiable list of operation descriptors, or {@code null} if not configured. + */ + public List> getAllowedOperations() { + + return allowedOperations; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java new file mode 100644 index 000000000000..2abf96831962 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2025, 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.inflow.extension.model; + +import org.wso2.carbon.identity.action.management.api.model.Action; +import org.wso2.carbon.identity.action.management.api.model.EndpointConfig; + +import java.sql.Timestamp; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * In-Flow Extension Action. + *

+ * Extends the base {@link Action} with an {@link AccessConfig} that defines what flow context data + * is exposed to the external service and which operations are allowed. + *

+ */ +public class InFlowExtensionAction extends Action { + + private final AccessConfig accessConfig; + private final Map flowTypeOverrides; + + public InFlowExtensionAction(ResponseBuilder responseBuilder) { + + super(responseBuilder); + this.accessConfig = responseBuilder.accessConfig; + this.flowTypeOverrides = responseBuilder.flowTypeOverrides != null + ? Collections.unmodifiableMap(new HashMap<>(responseBuilder.flowTypeOverrides)) + : Collections.emptyMap(); + } + + public InFlowExtensionAction(RequestBuilder requestBuilder) { + + super(requestBuilder); + this.accessConfig = requestBuilder.accessConfig; + this.flowTypeOverrides = requestBuilder.flowTypeOverrides != null + ? Collections.unmodifiableMap(new HashMap<>(requestBuilder.flowTypeOverrides)) + : Collections.emptyMap(); + } + + /** + * Returns the default access configuration for this In-Flow Extension action. + * + * @return The access config, or {@code null} if not configured. + */ + public AccessConfig getAccessConfig() { + + return accessConfig; + } + + /** + * Returns the per-flow-type access config overrides. + * Keys are flow type strings (e.g., "REGISTRATION", "LOGIN"). + * + * @return Unmodifiable map of flow type to AccessConfig overrides. + */ + public Map getFlowTypeOverrides() { + + return flowTypeOverrides; + } + + /** + * Resolves the effective access config for the given flow type. + * Returns the flow-type-specific override if present, otherwise falls back to the default access config. + * + * @param flowType The flow type (e.g., "REGISTRATION"). + * @return The resolved AccessConfig, or {@code null} if neither override nor default is configured. + */ + public AccessConfig resolveAccessConfig(String flowType) { + + if (flowType != null && flowTypeOverrides.containsKey(flowType)) { + return flowTypeOverrides.get(flowType); + } + return accessConfig; + } + + /** + * Response Builder for InFlowExtensionAction. + * Used when building from persisted data (DAO → service layer). + */ + public static class ResponseBuilder extends ActionResponseBuilder { + + private AccessConfig accessConfig; + private Map flowTypeOverrides; + + @Override + public ResponseBuilder id(String id) { + + super.id(id); + return this; + } + + @Override + public ResponseBuilder type(ActionTypes type) { + + super.type(type); + return this; + } + + @Override + public ResponseBuilder name(String name) { + + super.name(name); + return this; + } + + @Override + public ResponseBuilder description(String description) { + + super.description(description); + return this; + } + + @Override + public ResponseBuilder status(Status status) { + + super.status(status); + return this; + } + + @Override + public ResponseBuilder actionVersion(String actionVersion) { + + super.actionVersion(actionVersion); + return this; + } + + @Override + public ResponseBuilder createdAt(Timestamp createdAt) { + + super.createdAt(createdAt); + return this; + } + + @Override + public ResponseBuilder updatedAt(Timestamp updatedAt) { + + super.updatedAt(updatedAt); + return this; + } + + @Override + public ResponseBuilder endpoint(EndpointConfig endpoint) { + + super.endpoint(endpoint); + return this; + } + + @Override + public ResponseBuilder rule(org.wso2.carbon.identity.action.management.api.model.ActionRule rule) { + + super.rule(rule); + return this; + } + + public ResponseBuilder accessConfig(AccessConfig accessConfig) { + + this.accessConfig = accessConfig; + return this; + } + + public ResponseBuilder flowTypeOverrides(Map flowTypeOverrides) { + + this.flowTypeOverrides = flowTypeOverrides; + return this; + } + + @Override + public InFlowExtensionAction build() { + + return new InFlowExtensionAction(this); + } + } + + /** + * Request Builder for InFlowExtensionAction. + * Used when building from REST API request (API → service layer). + */ + public static class RequestBuilder extends ActionRequestBuilder { + + private AccessConfig accessConfig; + private Map flowTypeOverrides; + + public RequestBuilder(Action action) { + + name(action.getName()); + description(action.getDescription()); + actionVersion(action.getActionVersion()); + endpoint(action.getEndpoint()); + rule(action.getActionRule()); + } + + @Override + public RequestBuilder name(String name) { + + super.name(name); + return this; + } + + @Override + public RequestBuilder description(String description) { + + super.description(description); + return this; + } + + @Override + public RequestBuilder endpoint(EndpointConfig endpoint) { + + super.endpoint(endpoint); + return this; + } + + public RequestBuilder accessConfig(AccessConfig accessConfig) { + + this.accessConfig = accessConfig; + return this; + } + + public RequestBuilder flowTypeOverrides(Map flowTypeOverrides) { + + this.flowTypeOverrides = flowTypeOverrides; + return this; + } + + @Override + public InFlowExtensionAction build() { + + return new InFlowExtensionAction(this); + } + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java index ed14a2f04d39..323458a451b6 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java @@ -19,6 +19,7 @@ package org.wso2.carbon.identity.flow.execution.engine.internal; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; +import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; import org.wso2.carbon.identity.application.mgt.ApplicationManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.event.services.IdentityEventService; @@ -49,6 +50,7 @@ public class FlowExecutionEngineDataHolder { private FederatedAssociationManager federatedAssociationManager; private IdentityEventService identityEventService; private ActionExecutorService actionExecutorService; + private ActionManagementService actionManagementService; private List flowExecutionListeners = new ArrayList<>(); private FlowExecutionEngineDataHolder() { @@ -240,4 +242,24 @@ public void setActionExecutorService(ActionExecutorService actionExecutorService this.actionExecutorService = actionExecutorService; } + + /** + * Get the ActionManagementService instance. + * + * @return ActionManagementService instance. + */ + public ActionManagementService getActionManagementService() { + + return actionManagementService; + } + + /** + * Set the ActionManagementService instance. + * + * @param actionManagementService ActionManagementService instance. + */ + public void setActionManagementService(ActionManagementService actionManagementService) { + + this.actionManagementService = actionManagementService; + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java index 59521b9b6739..ed25143e86e7 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java @@ -31,6 +31,9 @@ import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionRequestBuilder; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionResponseProcessor; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; +import org.wso2.carbon.identity.action.management.api.service.ActionConverter; +import org.wso2.carbon.identity.action.management.api.service.ActionDTOModelResolver; +import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; import org.wso2.carbon.identity.application.mgt.ApplicationManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.event.services.IdentityEventService; @@ -39,9 +42,13 @@ import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionRequestBuilder; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionResponseProcessor; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.AccessConfigFlowUpdateInterceptor; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConverter; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionDTOModelResolver; import org.wso2.carbon.identity.flow.execution.engine.listener.FlowExecutionListener; import org.wso2.carbon.identity.flow.execution.engine.validation.InputProcessingListener; import org.wso2.carbon.identity.flow.mgt.FlowMgtService; +import org.wso2.carbon.identity.flow.mgt.FlowUpdateInterceptor; import org.wso2.carbon.identity.input.validation.mgt.services.InputValidationManagementService; import org.wso2.carbon.identity.user.profile.mgt.association.federation.FederatedAssociationManager; import org.wso2.carbon.user.core.service.RealmService; @@ -93,6 +100,16 @@ protected void activate(ComponentContext context) { bundleContext.registerService(ActionExecutionResponseProcessor.class.getName(), new InFlowExtensionResponseProcessor(), null); + // Register In-Flow Extension action management services + bundleContext.registerService(ActionConverter.class.getName(), + new InFlowExtensionActionConverter(), null); + bundleContext.registerService(ActionDTOModelResolver.class.getName(), + new InFlowExtensionActionDTOModelResolver(), null); + + // Register flow update interceptor for access config overrides + bundleContext.registerService(FlowUpdateInterceptor.class.getName(), + new AccessConfigFlowUpdateInterceptor(), null); + LOG.debug("Flow Engine service successfully activated."); } catch (Throwable e) { LOG.error("Error while initiating Flow Engine service", e); @@ -237,6 +254,25 @@ public void unsetFederatedAssociationManager(FederatedAssociationManager federat FlowExecutionEngineDataHolder.getInstance().setFederatedAssociationManager(null); } + @Reference( + name = "ActionExecutorService", + service = ActionExecutorService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetActionExecutorService" + ) + protected void setActionExecutorService(ActionExecutorService actionExecutorService) { + + LOG.debug("Setting the ActionExecutorService in the Flow Engine component."); + FlowExecutionEngineDataHolder.getInstance().setActionExecutorService(actionExecutorService); + } + + protected void unsetActionExecutorService(ActionExecutorService actionExecutorService) { + + LOG.debug("Unsetting the ActionExecutorService in the Flow Engine component."); + FlowExecutionEngineDataHolder.getInstance().setActionExecutorService(null); + } + @Reference( name = "ClaimMetadataManagementService", service = ClaimMetadataManagementService.class, diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowMgtService.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowMgtService.java index 50e370f5e1ca..b1c2ea36c13b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowMgtService.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowMgtService.java @@ -91,7 +91,17 @@ public void updateFlow(FlowDTO flowDTO, int tenantID) clearFlowResolveCache(flowDTO.getFlowType(), tenantID); GraphConfig flowConfig = new GraphBuilder().withSteps(flowDTO.getSteps()).build(); + + // Notify interceptors BEFORE persistence. Interceptors may extract and strip + // metadata from the graph (e.g., access config overrides) so that the stripped + // data is NOT persisted to executor metadata but is stored elsewhere. + for (FlowUpdateInterceptor interceptor : + FlowMgtServiceDataHolder.getInstance().getFlowUpdateInterceptors()) { + interceptor.onFlowUpdate(flowDTO.getFlowType(), flowConfig, tenantID); + } + FLOW_DAO.updateFlow(flowDTO.getFlowType(), flowConfig, tenantID, DEFAULT_FLOW_NAME); + AuditLog.AuditLogBuilder auditLogBuilder = new AuditLog.AuditLogBuilder(getInitiatorId(), LoggerUtils.getInitiatorType(getInitiatorId()), flowConfig.getId(), diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowUpdateInterceptor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowUpdateInterceptor.java new file mode 100644 index 000000000000..a418cdf2bbc4 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowUpdateInterceptor.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025, 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.mgt; + +import org.wso2.carbon.identity.flow.mgt.exception.FlowMgtFrameworkException; +import org.wso2.carbon.identity.flow.mgt.model.GraphConfig; + +/** + * Interceptor interface for flow update operations. + *

+ * Implementations can hook into the flow update lifecycle to perform additional + * processing when a flow is updated. Interceptors are invoked before the + * graph is persisted to the database, allowing them to: + *

    + *
  • Extract data from executor metadata for external storage (e.g., access config overrides + * stored as action properties instead of executor metadata).
  • + *
  • Strip extracted keys from executor metadata so they are not persisted to the + * flow executor metadata table (e.g., to avoid column size limitations).
  • + *
+ *

+ */ +public interface FlowUpdateInterceptor { + + /** + * Called after the flow graph has been built but before it is persisted to the database. + * Implementations may modify the {@code graphConfig} (e.g., stripping metadata keys) and the + * modifications will be reflected in the persisted graph. + * + * @param flowType The flow type (e.g., "REGISTRATION", "LOGIN"). + * @param graphConfig The built graph configuration containing node and executor details. + * Mutable — interceptors may modify executor metadata. + * @param tenantId The tenant ID. + * @throws FlowMgtFrameworkException If an error occurs during interception. + */ + void onFlowUpdate(String flowType, GraphConfig graphConfig, int tenantId) throws FlowMgtFrameworkException; +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceComponent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceComponent.java index d10eaa7b501d..d1cfbfc71df0 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceComponent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceComponent.java @@ -32,6 +32,7 @@ import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager; import org.wso2.carbon.identity.flow.mgt.FlowAIService; import org.wso2.carbon.identity.flow.mgt.FlowMgtService; +import org.wso2.carbon.identity.flow.mgt.FlowUpdateInterceptor; import org.wso2.carbon.identity.organization.management.service.OrganizationManager; import org.wso2.carbon.identity.organization.resource.hierarchy.traverse.service.OrgResourceResolverService; @@ -130,4 +131,20 @@ protected void unsetCompatibilitySettingsManager(CompatibilitySettingsManager co FlowMgtServiceDataHolder.getInstance().setCompatibilitySettingsManager(null); } + + @Reference( + name = "flow.update.interceptor", + service = FlowUpdateInterceptor.class, + cardinality = ReferenceCardinality.MULTIPLE, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetFlowUpdateInterceptor") + protected void setFlowUpdateInterceptor(FlowUpdateInterceptor interceptor) { + + FlowMgtServiceDataHolder.getInstance().addFlowUpdateInterceptor(interceptor); + } + + protected void unsetFlowUpdateInterceptor(FlowUpdateInterceptor interceptor) { + + FlowMgtServiceDataHolder.getInstance().removeFlowUpdateInterceptor(interceptor); + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceDataHolder.java index 22109cfcf196..7e21dde44e80 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceDataHolder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceDataHolder.java @@ -20,9 +20,14 @@ import org.wso2.carbon.identity.compatibility.settings.core.CompatibilitySettingsManager; import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager; +import org.wso2.carbon.identity.flow.mgt.FlowUpdateInterceptor; import org.wso2.carbon.identity.organization.management.service.OrganizationManager; import org.wso2.carbon.identity.organization.resource.hierarchy.traverse.service.OrgResourceResolverService; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * A singleton class to hold the data of the flow management service. */ @@ -32,6 +37,7 @@ public class FlowMgtServiceDataHolder { private OrgResourceResolverService orgResourceResolverService; private ConfigurationManager configurationManager; private CompatibilitySettingsManager compatibilitySettingsManager; + private final List flowUpdateInterceptors = new ArrayList<>(); private static final FlowMgtServiceDataHolder INSTANCE = new FlowMgtServiceDataHolder(); @@ -83,4 +89,19 @@ public void setCompatibilitySettingsManager(CompatibilitySettingsManager compati this.compatibilitySettingsManager = compatibilitySettingsManager; } + + public List getFlowUpdateInterceptors() { + + return Collections.unmodifiableList(flowUpdateInterceptors); + } + + public void addFlowUpdateInterceptor(FlowUpdateInterceptor interceptor) { + + flowUpdateInterceptors.add(interceptor); + } + + public void removeFlowUpdateInterceptor(FlowUpdateInterceptor interceptor) { + + flowUpdateInterceptors.remove(interceptor); + } } From 8dcf6eb7999e7d2dcc424cca82755e8d0e022293 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Wed, 11 Mar 2026 11:23:20 +0530 Subject: [PATCH 07/41] Enhance In-Flow Extension Model with Encryption and Path Management - Hierarchical paths with optional encryption. - Created Encryption class to hold external service's X.509 public certificate for outbound JWE encryption. - Updated InFlowExtensionAction to include encryption configuration. - Enhanced FlowExecutionEngineDataHolder to manage CertificateManagementService. - Registered CertificateManagementService in FlowExecutionEngineServiceComponent. - Added support for ACTIONS inbound protocol in IdentityKeyStoreResolverConstants. --- .../pom.xml | 10 + .../executor/InFlowExtensionExecutor.java | 53 ++- .../InFlowExtensionRequestBuilder.java | 287 ++++++++++----- .../InFlowExtensionResponseProcessor.java | 66 ++++ .../extension/executor/JWEEncryptionUtil.java | 234 +++++++++++++ .../AccessConfigFlowUpdateInterceptor.java | 109 +++++- .../InFlowExtensionActionConstants.java | 13 + .../InFlowExtensionActionConverter.java | 36 +- ...InFlowExtensionActionDTOModelResolver.java | 330 +++++++++++++++--- .../inflow/extension/model/AccessConfig.java | 136 +++++++- .../extension/model/AllowedOperation.java | 87 +++++ .../inflow/extension/model/Encryption.java | 60 ++++ .../inflow/extension/model/ExposePath.java | 64 ++++ .../model/InFlowExtensionAction.java | 27 ++ .../inflow/extension/model/OperationPath.java | 70 ++++ .../FlowExecutionEngineDataHolder.java | 22 ++ .../FlowExecutionEngineServiceComponent.java | 64 ++-- .../IdentityKeyStoreResolverConstants.java | 6 +- 18 files changed, 1477 insertions(+), 197 deletions(-) create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AllowedOperation.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/Encryption.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/ExposePath.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationPath.java diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml index 178d4f046935..952d0b0db8bd 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml @@ -115,6 +115,10 @@ org.wso2.carbon.identity.framework org.wso2.carbon.identity.user.action
+ + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.certificate.management + @@ -184,6 +188,12 @@ version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.central.log.mgt.utils; version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.certificate.management.exception; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.certificate.management.model; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.certificate.management.service; + version="${carbon.identity.package.import.version.range}", com.fasterxml.jackson.core.*; version="${com.fasterxml.jackson.annotation.version.range}", com.fasterxml.jackson.databind.*; version="${com.fasterxml.jackson.annotation.version.range}", diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java index 5b238cc749fb..39a4d520bc1e 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java @@ -38,6 +38,7 @@ import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; import org.wso2.carbon.identity.flow.mgt.model.NodeConfig; @@ -66,10 +67,6 @@ * Context updates are already applied directly to the {@link FlowExecutionContext} * by the response processor. * - * - *

Note: Access config is never read from executor metadata. It is stored exclusively - * in action properties (default and per-flow-type overrides in {@code IDN_ACTION_PROPERTIES}). - * Executor metadata only provides the {@code actionId}.

*/ public class InFlowExtensionExecutor implements Executor { @@ -81,6 +78,8 @@ public class InFlowExtensionExecutor implements Executor { public static final String EXPOSE_KEY = "expose"; public static final String ALLOWED_OPERATIONS_KEY = "allowedOperations"; public static final String PATH_TYPE_ANNOTATIONS_KEY = "pathTypeAnnotations"; + public static final String ACCESS_CONFIG_KEY = "accessConfig"; + public static final String ENCRYPTION_KEY = "encryption"; private static final String ACTION_ID_METADATA_KEY = "actionId"; @Override @@ -106,15 +105,14 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE LOG.debug("Executing In-Flow Extension action with ID: " + actionId); } - // Resolve access config exclusively from the action (with flow-type override support). - // Access config is NOT stored in executor metadata — it is stored in action properties. AccessConfig resolvedConfig = resolveAccessConfigFromAction(actionId, context); + Encryption encryption = resolveEncryptionFromAction(actionId, context); List expose; String allowedOpsJson = null; if (resolvedConfig != null && resolvedConfig.getExpose() != null) { - expose = resolvedConfig.getExpose(); + expose = resolvedConfig.getExposePaths(); } else { // No access config on action — use system defaults. expose = new ArrayList<>(HierarchicalPrefixMatcher.DEFAULT_EXPOSE); @@ -136,6 +134,17 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE flowContext.add(ALLOWED_OPERATIONS_KEY, allowedOpsJson); } + // Pass the full AccessConfig so request builder and response processor can access + // per-path encryption flags for JWE encryption/decryption. + if (resolvedConfig != null) { + flowContext.add(ACCESS_CONFIG_KEY, resolvedConfig); + } + + // Pass the Encryption config (certificate) separately. + if (encryption != null) { + flowContext.add(ENCRYPTION_KEY, encryption); + } + ActionExecutorService actionExecutorService = getActionExecutorService(); if (actionExecutorService == null) { throw new FlowEngineException("ActionExecutorService is not available."); @@ -291,7 +300,7 @@ private AccessConfig resolveAccessConfigFromAction(String actionId, FlowExecutio try { Action action = actionMgtService.getActionByActionId( - Action.ActionTypes.IN_FLOW_EXTENSION.getActionType(), + Action.ActionTypes.IN_FLOW_EXTENSION.getPathParam(), actionId, context.getTenantDomain()); if (action instanceof InFlowExtensionAction) { @@ -306,6 +315,34 @@ private AccessConfig resolveAccessConfigFromAction(String actionId, FlowExecutio return null; } + /** + * Resolve the encryption configuration (certificate) from the action. + * + * @param actionId The action ID. + * @param context The flow execution context. + * @return The Encryption config, or {@code null} if none configured. + */ + private Encryption resolveEncryptionFromAction(String actionId, FlowExecutionContext context) { + + ActionManagementService actionMgtService = getActionManagementService(); + if (actionMgtService == null) { + return null; + } + + try { + Action action = actionMgtService.getActionByActionId( + Action.ActionTypes.IN_FLOW_EXTENSION.getPathParam(), + actionId, context.getTenantDomain()); + + if (action instanceof InFlowExtensionAction) { + return ((InFlowExtensionAction) action).getEncryption(); + } + } catch (ActionMgtException e) { + LOG.error("Error retrieving encryption config for action " + actionId, e); + } + return null; + } + /** * Read a single metadata value from the current node's executor configuration. * diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java index f949217b9990..71f6a648c222 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java @@ -37,6 +37,8 @@ import org.wso2.carbon.identity.action.execution.api.model.UserStore; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionRequestBuilder; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; 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.mgt.model.NodeConfig; @@ -99,16 +101,31 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex expose = HierarchicalPrefixMatcher.DEFAULT_EXPOSE; } + // Read access config for encryption metadata. + AccessConfig accessConfig = flowContext.getValue( + InFlowExtensionExecutor.ACCESS_CONFIG_KEY, AccessConfig.class); + + // Read encryption configuration (certificate) separately. + Encryption encryption = flowContext.getValue( + InFlowExtensionExecutor.ENCRYPTION_KEY, Encryption.class); + // Build allowed operations first so REPLACE paths can augment the expose list. List allowedOperations = buildAllowedOperations( flowContext.getValue(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, String.class), - flowContext); + flowContext, accessConfig); - // Augment expose with REPLACE paths so the external service can see the current values - // it may replace. - expose = augmentExposeWithReplacePaths(expose, allowedOperations); + // Augment expose with REPLACE and REMOVE paths so the external service can see + // the current values it may replace or remove. + expose = augmentExposeWithOperationPaths(expose, allowedOperations); + + // Determine certificate PEM for outbound encryption (if any paths are encrypted). + String certificatePEM = null; + if (accessConfig != null && accessConfig.hasAnyEncryptedPath() + && encryption != null && encryption.getCertificate() != null) { + certificatePEM = encryption.getCertificate().getCertificateContent(); + } - InFlowExtensionEvent event = buildEvent(execCtx, expose); + InFlowExtensionEvent event = buildEvent(execCtx, expose, accessConfig, certificatePEM); return new ActionExecutionRequest.Builder() .actionType(ActionType.IN_FLOW_EXTENSION) @@ -120,17 +137,23 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex /** * Parse the allowed-operations JSON into typed {@link AllowedOperation} objects. - * Strips path type annotations (e.g., "[]", "[schema]") from paths before creating - * AllowedOperation objects so that {@code OperationComparator} receives clean paths. + * Handles the nested format where each entry is {@code {op, paths: [{path, encrypted}]}}. + * Groups entries by operation type and strips path type annotations. * The original annotations are stored in the FlowContext under * {@link InFlowExtensionExecutor#PATH_TYPE_ANNOTATIONS_KEY} for the response processor. + *

+ * Encryption metadata is NOT extracted from the JSON. Instead, the {@link AccessConfig} + * (already in FlowContext) is the single source of truth for per-path encryption flags. + * The response processor uses {@code AccessConfig.isOperationPathEncrypted()} directly. * * @param allowedOperationsJson The JSON string (maybe null). - * @param flowContext The FlowContext to store path annotations in. + * @param flowContext The FlowContext to store path annotations. + * @param accessConfig The access config (may be null). Reserved for future use. * @return List of AllowedOperation objects with clean (annotation-free) paths. */ private List buildAllowedOperations(String allowedOperationsJson, - FlowContext flowContext) { + FlowContext flowContext, + AccessConfig accessConfig) { if (allowedOperationsJson == null || allowedOperationsJson.isEmpty()) { LOG.debug("No allowed operations configured. Using empty list."); @@ -142,14 +165,69 @@ private List buildAllowedOperations(String allowedOperationsJs allowedOperationsJson, OPERATION_LIST_TYPE_REF); // Map to store path type annotations: cleanPath -> annotation content. - // "" means simple string array (from []), non-empty means schema (from [schema]). Map pathTypeAnnotations = new HashMap<>(); - List allowedOperations = new ArrayList<>(); + // Group entries by operation type, handling nested paths [{path, encrypted}]. + Map> groupedByOp = new HashMap<>(); for (Map config : operationConfigs) { - AllowedOperation operation = createAllowedOperationFromConfig(config, pathTypeAnnotations); - if (operation != null) { - allowedOperations.add(operation); + String opString = (String) config.get("op"); + Object pathsObj = config.get("paths"); + + if (opString == null) { + LOG.warn("Invalid allowed operation config: missing 'op'."); + continue; + } + if (!(pathsObj instanceof List)) { + LOG.warn("Invalid allowed operation config: missing or invalid 'paths'."); + continue; + } + + List pathsList = (List) pathsObj; + for (Object pathEntry : pathsList) { + String rawPath; + + if (pathEntry instanceof Map) { + // Nested format: {path, encrypted} — only extract path here. + // Encryption metadata is resolved from AccessConfig at runtime. + Map pathMap = (Map) pathEntry; + rawPath = (String) pathMap.get("path"); + } else if (pathEntry instanceof String) { + rawPath = (String) pathEntry; + } else { + LOG.warn("Invalid path entry in allowed operation."); + continue; + } + + if (rawPath == null) { + continue; + } + + // Strip annotations and accumulate paths into groups. + Matcher matcher = PATH_ANNOTATION_PATTERN.matcher(rawPath); + String cleanPath; + if (matcher.find()) { + cleanPath = rawPath.substring(0, matcher.start()); + pathTypeAnnotations.put(cleanPath, matcher.group(1)); + } else { + cleanPath = rawPath; + } + + groupedByOp.computeIfAbsent(opString.toUpperCase(Locale.ENGLISH), + k -> new ArrayList<>()).add(cleanPath); + } + } + + // Build AllowedOperation objects from grouped entries. + List allowedOperations = new ArrayList<>(); + for (Map.Entry> entry : groupedByOp.entrySet()) { + try { + Operation operation = Operation.valueOf(entry.getKey()); + AllowedOperation allowedOperation = new AllowedOperation(); + allowedOperation.setOp(operation); + allowedOperation.setPaths(entry.getValue()); + allowedOperations.add(allowedOperation); + } catch (IllegalArgumentException e) { + LOG.warn("Unknown operation type: " + entry.getKey()); } } @@ -169,75 +247,20 @@ private List buildAllowedOperations(String allowedOperationsJs } } - /** - * Create an AllowedOperation from a configuration map. - * Strips path type annotations and records them in the annotations map. - * - * @param config The raw configuration map from JSON. - * @param pathTypeAnnotations Map to record extracted annotations (cleanPath → annotation). - * @return The AllowedOperation with clean paths, or null if invalid. - */ - @SuppressWarnings("unchecked") - private AllowedOperation createAllowedOperationFromConfig(Map config, - Map pathTypeAnnotations) { - - String opString = (String) config.get("op"); - Object pathsObj = config.get("paths"); - - if (opString == null || pathsObj == null) { - LOG.warn("Invalid allowed operation config: missing 'op' or 'paths'."); - return null; - } - - Operation operation; - try { - operation = Operation.valueOf(opString.toUpperCase(Locale.ENGLISH)); - } catch (IllegalArgumentException e) { - LOG.warn("Unknown operation type: " + opString); - return null; - } - - List paths; - if (pathsObj instanceof List) { - paths = (List) pathsObj; - } else { - LOG.warn("Invalid 'paths' type in allowed operation config."); - return null; - } - - // Strip type annotations from paths and record them. - List cleanPaths = new ArrayList<>(); - for (String rawPath : paths) { - Matcher matcher = PATH_ANNOTATION_PATTERN.matcher(rawPath); - if (matcher.find()) { - String cleanPath = rawPath.substring(0, matcher.start()); - String annotationContent = matcher.group(1); // "" for [], or schema content for [schema] - cleanPaths.add(cleanPath); - pathTypeAnnotations.put(cleanPath, annotationContent); - if (LOG.isDebugEnabled()) { - LOG.debug("Path annotation extracted: " + rawPath + " -> clean: " + cleanPath + - ", annotation: [" + annotationContent + "]"); - } - } else { - cleanPaths.add(rawPath); - } - } - - AllowedOperation allowedOperation = new AllowedOperation(); - allowedOperation.setOp(operation); - allowedOperation.setPaths(cleanPaths); - return allowedOperation; - } - /** * Build the {@link InFlowExtensionEvent} from the {@link FlowExecutionContext}, * filtering data according to the expose configuration. + * Values for expose paths with {@code encrypted: true} are JWE-encrypted using the + * external service's certificate before being included in the event. * - * @param context The FlowExecutionContext (full source of truth). - * @param expose The expose prefix list controlling which data is included. + * @param context The FlowExecutionContext (full source of truth). + * @param expose The expose prefix list controlling which data is included. + * @param accessConfig The access config (may be null if no encryption). + * @param certificatePEM The external service's certificate PEM for JWE encryption (may be null). * @return The InFlowExtensionEvent. */ - private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List expose) { + private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List expose, + AccessConfig accessConfig, String certificatePEM) { InFlowExtensionEvent.Builder eventBuilder = new InFlowExtensionEvent.Builder(); @@ -264,7 +287,7 @@ private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List expose) { + private User buildUser(FlowUser flowUser, List expose, + AccessConfig accessConfig, String certificatePEM) { String userId = isExposed(HierarchicalPrefixMatcher.USER_ID_PATH, expose) ? flowUser.getUserId() : null; @@ -331,7 +358,13 @@ private User buildUser(FlowUser flowUser, List expose) { for (Map.Entry claim : claims.entrySet()) { if (!hasSpecificFilter || isExposed( HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX + claim.getKey(), expose)) { - userClaims.add(new UserClaim(claim.getKey(), claim.getValue())); + String claimValue = claim.getValue(); + // Encrypt claim value if the expose path is marked as encrypted. + String claimPath = HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX + claim.getKey(); + if (shouldEncrypt(claimPath, accessConfig, certificatePEM)) { + claimValue = encryptValue(claimValue, certificatePEM); + } + userClaims.add(new UserClaim(claim.getKey(), claimValue)); } } if (!userClaims.isEmpty()) { @@ -343,8 +376,20 @@ private User buildUser(FlowUser flowUser, List expose) { if (isExposed(HierarchicalPrefixMatcher.USER_CREDENTIALS_PREFIX, expose)) { Map credentials = flowUser.getUserCredentials(); if (credentials != null && !credentials.isEmpty()) { - - userBuilder.userCredentials(new HashMap<>(credentials)); + // Check if credentials expose path is encrypted. + if (shouldEncrypt(HierarchicalPrefixMatcher.USER_CREDENTIALS_PREFIX, accessConfig, + certificatePEM)) { + // Encrypt each credential value and store as char[] of the JWE compact string. + Map encryptedCredentials = new HashMap<>(); + for (Map.Entry entry : credentials.entrySet()) { + String plaintext = new String(entry.getValue()); + String encrypted = encryptValue(plaintext, certificatePEM); + encryptedCredentials.put(entry.getKey(), encrypted.toCharArray()); + } + userBuilder.userCredentials(encryptedCredentials); + } else { + userBuilder.userCredentials(new HashMap<>(credentials)); + } } } @@ -404,41 +449,91 @@ private boolean hasSpecificSubPathFilter(List expose, String areaPrefix) } /** - * Augment the expose list with paths from REPLACE operations. - * REPLACE operations require the current values to be visible to the external service, - * so their paths must be exposed even if not explicitly in the expose configuration. + * Augment the expose list with paths from REPLACE and REMOVE operations. + * REPLACE operations require the current values to be visible to the external service + * so it can decide what replacement to send. REMOVE operations require current values + * to be visible so the external service knows what data would be removed. * * @param expose The current expose list. * @param allowedOperations The parsed allowed operations. * @return The augmented expose list (new list if modified, original if unchanged). */ - private List augmentExposeWithReplacePaths(List expose, - List allowedOperations) { + private List augmentExposeWithOperationPaths(List expose, + List allowedOperations) { if (allowedOperations == null || allowedOperations.isEmpty()) { return expose; } - List replacePaths = new ArrayList<>(); + List additionalPaths = new ArrayList<>(); for (AllowedOperation op : allowedOperations) { - if (op.getOp() == Operation.REPLACE && op.getPaths() != null) { + if ((op.getOp() == Operation.REPLACE || op.getOp() == Operation.REMOVE) + && op.getPaths() != null) { for (String path : op.getPaths()) { if (!isExposed(path, expose)) { - replacePaths.add(path); + additionalPaths.add(path); } } } } - if (replacePaths.isEmpty()) { + if (additionalPaths.isEmpty()) { return expose; } List augmented = new ArrayList<>(expose); - augmented.addAll(replacePaths); + augmented.addAll(additionalPaths); if (LOG.isDebugEnabled()) { - LOG.debug("Augmented expose list with REPLACE paths: " + replacePaths); + LOG.debug("Augmented expose list with REPLACE/REMOVE paths: " + additionalPaths); } return augmented; } + + // ---- Encryption helpers ---- + + /** + * Determine if a value at the given path should be JWE-encrypted before sending to the external service. + *

+ * Checks both the explicit expose config and the operation paths (REPLACE / REMOVE) that may + * have been dynamically augmented into the expose list. REPLACE and REMOVE operation paths + * carry their own encryption flags in the allowedOperations config — when such a path is + * augmented into expose for visibility, its encryption flag must be honoured. + * + * @param path The expose path. + * @param accessConfig The access config with encryption flags. + * @param certificatePEM The certificate PEM (null if no encryption configured). + * @return {@code true} if the value should be encrypted. + */ + private boolean shouldEncrypt(String path, AccessConfig accessConfig, String certificatePEM) { + + if (certificatePEM == null || accessConfig == null) { + return false; + } + // Check explicit expose paths first. + if (accessConfig.isExposePathEncrypted(path)) { + return true; + } + // Also check REPLACE and REMOVE operation paths that were augmented into expose. + // These paths may have their own encryption flags in the allowedOperations config. + return accessConfig.isOperationPathEncrypted("REPLACE", path) + || accessConfig.isOperationPathEncrypted("REMOVE", path); + } + + /** + * JWE-encrypt a plaintext value using the external service's certificate. + * If encryption fails, the original value is returned and a warning is logged. + * + * @param plaintext The value to encrypt. + * @param certificatePEM The external service's certificate PEM. + * @return The JWE compact serialization string, or the original value on failure. + */ + private String encryptValue(String plaintext, String certificatePEM) { + + try { + return JWEEncryptionUtil.encrypt(plaintext, certificatePEM); + } catch (Exception e) { + LOG.warn("Failed to JWE-encrypt outbound value. Sending plaintext.", e); + return plaintext; + } + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java index 4770b229fc40..02650d7fa231 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java @@ -45,6 +45,7 @@ import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; 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; @@ -120,6 +121,11 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon pathTypeAnnotations = Collections.emptyMap(); } + // Read access config for encryption metadata. + // Uses AccessConfig.isOperationPathEncrypted() for canonical encryption checking. + AccessConfig accessConfig = flowContext.getValue( + InFlowExtensionExecutor.ACCESS_CONFIG_KEY, AccessConfig.class); + List results = new ArrayList<>(); // Get operations from the response (already filtered by ActionExecutorServiceImpl). @@ -140,6 +146,9 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon operation = normalizedOp; } + // Decrypt inbound value if this operation path is marked as encrypted in AccessConfig. + operation = decryptOperationValueIfNeeded(operation, accessConfig, tenantDomain); + results.add(processOperation(operation, execCtx, tenantDomain, pathTypeAnnotations)); } } @@ -707,4 +716,61 @@ public static class InFlowExtensionSuccess implements Success { // Marker class for successful execution. } + + // ========================= Inbound decryption ========================= + + /** + * Decrypt the operation value if the operation path has encryption enabled in the AccessConfig. + * Uses {@link AccessConfig#isOperationPathEncrypted(String, String)} for canonical checking. + * For operations with encrypted paths, checks if the value looks like a JWE compact + * serialization and decrypts it using the IS's private key. + * + * @param operation The operation to potentially decrypt. + * @param accessConfig The access config with encryption flags (may be null). + * @param tenantDomain Tenant domain for IS private key resolution. + * @return The operation with decrypted value, or the original operation if no decryption needed. + */ + private PerformableOperation decryptOperationValueIfNeeded(PerformableOperation operation, + AccessConfig accessConfig, String tenantDomain) { + + if (accessConfig == null || operation.getValue() == null) { + return operation; + } + + // Check if this operation path has encryption enabled via AccessConfig. + if (!accessConfig.isOperationPathEncrypted(operation.getOp().name(), operation.getPath())) { + return operation; + } + + // Only decrypt string values that look like JWE compact serialization. + Object value = operation.getValue(); + if (!(value instanceof String)) { + return operation; + } + + String stringValue = (String) value; + if (!JWEEncryptionUtil.isJWEEncrypted(stringValue)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Value for encrypted path " + operation.getPath() + + " is not JWE-encrypted. Using as-is."); + } + return operation; + } + + try { + String decrypted = JWEEncryptionUtil.decrypt(stringValue, tenantDomain); + PerformableOperation decryptedOp = new PerformableOperation(); + decryptedOp.setOp(operation.getOp()); + decryptedOp.setPath(operation.getPath()); + decryptedOp.setValue(decrypted); + if (LOG.isDebugEnabled()) { + LOG.debug("Successfully decrypted inbound JWE value for path: " + operation.getPath()); + } + return decryptedOp; + } catch (Exception e) { + LOG.warn("Failed to decrypt inbound JWE value for path: " + operation.getPath() + + ". Using raw value.", e); + return operation; + } + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java new file mode 100644 index 000000000000..a9ba41c0b470 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2025, 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.inflow.extension.executor; + +import com.nimbusds.jose.EncryptionMethod; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWEHeader; +import com.nimbusds.jose.JWEObject; +import com.nimbusds.jose.Payload; +import com.nimbusds.jose.crypto.RSADecrypter; +import com.nimbusds.jose.crypto.RSAEncrypter; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionException; +import org.wso2.carbon.identity.core.IdentityKeyStoreResolver; +import org.wso2.carbon.identity.core.util.IdentityKeyStoreResolverConstants; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.text.ParseException; + +/** + * Utility class for JWE encryption and decryption operations used by In-Flow Extension actions. + * + *

Outbound encryption (IS → external service): uses the external service's X.509 + * certificate public key with {@code RSA-OAEP-256} + {@code A256GCM}. This follows the same + * pattern as {@code PasswordUpdatingUser.encryptCredential()}.

+ * + *

Inbound decryption (external service → IS): uses the IS server's RSA private key + * retrieved via {@link IdentityKeyStoreResolver} with + * {@link IdentityKeyStoreResolverConstants.InboundProtocol#ACTIONS}. This follows the same + * pattern as {@code OIDCSessionManagementUtil.decryptWithRSA()}.

+ */ +public final class JWEEncryptionUtil { + + private static final Log LOG = LogFactory.getLog(JWEEncryptionUtil.class); + + /** Number of dot-separated parts in a JWE compact serialization. */ + private static final int JWE_PART_COUNT = 5; + + /** Standard PEM line length per RFC 7468. */ + private static final int PEM_LINE_LENGTH = 64; + + private JWEEncryptionUtil() { + + } + + /** + * Encrypt a plaintext string using JWE with the external service's X.509 certificate. + * Uses RSA-OAEP-256 key encryption and A256GCM content encryption. + * + * @param plaintext The plaintext JSON string to encrypt. + * @param certificatePEM The external service's X.509 certificate in PEM format. + * @return JWE compact serialization string (5-part dot-separated). + * @throws ActionExecutionException If encryption fails. + */ + public static String encrypt(String plaintext, String certificatePEM) throws ActionExecutionException { + + try { + X509Certificate certificate = parsePEMCertificate(certificatePEM); + RSAPublicKey publicKey = (RSAPublicKey) certificate.getPublicKey(); + + JWEHeader header = new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM) + .contentType("application/json") + .build(); + + JWEObject jweObject = new JWEObject(header, new Payload(plaintext)); + jweObject.encrypt(new RSAEncrypter(publicKey)); + + return jweObject.serialize(); + } catch (JOSEException e) { + throw new ActionExecutionException( + "Failed to JWE-encrypt data for In-Flow Extension action.", e); + } + } + + /** + * Decrypt a JWE compact string using the IS server's RSA private key. + * The private key is resolved via {@link IdentityKeyStoreResolver} using + * {@link IdentityKeyStoreResolverConstants.InboundProtocol#ACTIONS}. + * + * @param jweString The JWE compact serialization string. + * @param tenantDomain The tenant domain to resolve the private key. + * @return Decrypted plaintext string. + * @throws ActionExecutionException If decryption fails. + */ + public static String decrypt(String jweString, String tenantDomain) throws ActionExecutionException { + + try { + JWEObject jweObject = JWEObject.parse(jweString); + + Key privateKey = IdentityKeyStoreResolver.getInstance() + .getPrivateKey(tenantDomain, + IdentityKeyStoreResolverConstants.InboundProtocol.ACTIONS); + + RSADecrypter decrypter = new RSADecrypter((RSAPrivateKey) privateKey); + jweObject.decrypt(decrypter); + + return jweObject.getPayload().toString(); + } catch (ParseException e) { + throw new ActionExecutionException( + "Failed to parse JWE string from In-Flow Extension response.", e); + } catch (JOSEException e) { + throw new ActionExecutionException( + "Failed to decrypt JWE value from In-Flow Extension response.", e); + } catch (Exception e) { + throw new ActionExecutionException( + "Error resolving IS private key for In-Flow Extension JWE decryption.", e); + } + } + + /** + * Detect whether a string value is a JWE compact serialization. + * A JWE compact serialization has exactly 5 dot-separated Base64url-encoded parts. + * + * @param value The value to check. + * @return {@code true} if the value appears to be a JWE compact string. + */ + public static boolean isJWEEncrypted(String value) { + + if (value == null || value.isEmpty()) { + return false; + } + // Count dots — JWE compact serialization has exactly 4 dots (5 parts). + int dotCount = 0; + for (int i = 0; i < value.length(); i++) { + if (value.charAt(i) == '.') { + dotCount++; + if (dotCount > 4) { + return false; + } + } + } + return dotCount == 4; + } + + /** + * Parse a PEM-encoded X.509 certificate string into an {@link X509Certificate} object. + *

+ * Handles PEM strings that may have lost their newlines during database storage/retrieval + * (e.g., via {@code CertificateManagementDAOImpl.getStringValueFromBlob()} which strips + * line breaks). The method normalizes the PEM format before parsing to ensure the + * header/footer are on separate lines and Base64 content is properly structured. + *

+ * + * @param pem The PEM string (with or without proper line breaks). + * @return The parsed X509Certificate. + * @throws ActionExecutionException If parsing fails. + */ + public static X509Certificate parsePEMCertificate(String pem) throws ActionExecutionException { + + try { + String normalizedPEM = normalizePEM(pem); + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) factory.generateCertificate( + new ByteArrayInputStream(normalizedPEM.getBytes(StandardCharsets.UTF_8))); + } catch (ActionExecutionException e) { + throw e; + } catch (Exception e) { + throw new ActionExecutionException( + "Failed to parse PEM certificate for In-Flow Extension action.", e); + } + } + + /** + * Normalize a PEM string to ensure it has proper line structure. + *

+ * The CertificateManagementService's DAO layer reads BLOB data using + * {@code BufferedReader.readLine()} + {@code StringBuilder.append(line)}, which strips + * all newline characters. This produces a single-line string like: + * {@code -----BEGIN CERTIFICATE-----MIICxjCC...-----END CERTIFICATE-----} + * which Java's X509 parser cannot handle. + *

+ *

+ * This method extracts the Base64 body, strips all whitespace, and re-wraps it at + * 64-character lines between proper PEM header/footer markers. + *

+ * + * @param pem The PEM string (possibly with stripped newlines). + * @return A properly formatted PEM string. + * @throws ActionExecutionException If the PEM structure is invalid. + */ + private static String normalizePEM(String pem) throws ActionExecutionException { + + if (pem == null || pem.isEmpty()) { + throw new ActionExecutionException("Certificate PEM string is null or empty."); + } + + String trimmed = pem.trim(); + + // Extract the Base64 body by stripping header/footer markers. + String base64Body = trimmed + .replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replaceAll("\\s+", ""); + + if (base64Body.isEmpty()) { + throw new ActionExecutionException("Certificate PEM contains no Base64 data."); + } + + // Reconstruct proper PEM with 64-char line wrapping (RFC 7468). + StringBuilder sb = new StringBuilder(); + sb.append("-----BEGIN CERTIFICATE-----\n"); + for (int i = 0; i < base64Body.length(); i += PEM_LINE_LENGTH) { + sb.append(base64Body, i, Math.min(i + PEM_LINE_LENGTH, base64Body.length())); + sb.append('\n'); + } + sb.append("-----END CERTIFICATE-----\n"); + return sb.toString(); + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java index 4f989dfe19d4..43d0626cf597 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java @@ -28,7 +28,10 @@ import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AllowedOperation; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ExposePath; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.OperationPath; import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; import org.wso2.carbon.identity.flow.mgt.FlowUpdateInterceptor; import org.wso2.carbon.identity.flow.mgt.exception.FlowMgtFrameworkException; @@ -37,6 +40,7 @@ import org.wso2.carbon.identity.flow.mgt.model.GraphConfig; import org.wso2.carbon.identity.flow.mgt.model.NodeConfig; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -125,10 +129,10 @@ private void processNode(NodeConfig node, String flowType, String tenantDomain, // Parse the unified accessConfig JSON: {"expose": [...], "allowedOperations": [...]} Map accessConfigMap = OBJECT_MAPPER.readValue(accessConfigJson, MAP_TYPE_REF); - List expose = accessConfigMap.containsKey(EXPOSE_FIELD) - ? (List) accessConfigMap.get(EXPOSE_FIELD) : null; - List> allowedOps = accessConfigMap.containsKey(ALLOWED_OPERATIONS_FIELD) - ? (List>) accessConfigMap.get(ALLOWED_OPERATIONS_FIELD) : null; + List expose = parseExposePaths(accessConfigMap.get(EXPOSE_FIELD)); + List allowedOps = parseAllowedOperations( + accessConfigMap.get(ALLOWED_OPERATIONS_FIELD)); + // Certificate is action-level only — not overridable per flow type. AccessConfig overrideConfig = new AccessConfig(expose, allowedOps); // Retrieve the existing action to merge overrides (preserving other flow types). @@ -171,4 +175,101 @@ private void processNode(NodeConfig node, String flowType, String tenantDomain, "Error parsing access config metadata for action: " + actionId, e); } } + + /** + * Parse expose entries from the raw JSON value. + * Accepts both simple strings (no encryption) and objects ({path, encrypted}). + */ + @SuppressWarnings("unchecked") + private List parseExposePaths(Object value) { + + if (value == null) { + return null; + } + if (!(value instanceof List)) { + LOG.warn("Invalid expose value in flow override accessConfig. Expected list."); + return null; + } + + List rawList = (List) value; + List result = new ArrayList<>(); + for (Object item : rawList) { + if (item instanceof String) { + result.add(new ExposePath((String) item, false)); + } else if (item instanceof Map) { + Map map = (Map) item; + String path = (String) map.get("path"); + boolean encrypted = toBooleanSafe(map.get("encrypted")); + if (path != null) { + result.add(new ExposePath(path, encrypted)); + } + } + } + return result.isEmpty() ? null : result; + } + + /** + * Parse allowed operations from the raw JSON value. + * Accepts the nested format: [{op, paths: [{path, encrypted}]}]. + */ + @SuppressWarnings("unchecked") + private List parseAllowedOperations(Object value) { + + if (value == null) { + return null; + } + if (!(value instanceof List)) { + LOG.warn("Invalid allowedOperations value in flow override accessConfig. Expected list."); + return null; + } + + List rawList = (List) value; + List result = new ArrayList<>(); + for (Object item : rawList) { + if (item instanceof Map) { + Map map = (Map) item; + String op = (String) map.get("op"); + Object pathsObj = map.get("paths"); + if (op == null || !(pathsObj instanceof List)) { + continue; + } + List pathsList = (List) pathsObj; + List operationPaths = new ArrayList<>(); + for (Object pathItem : pathsList) { + if (pathItem instanceof Map) { + Map pathMap = (Map) pathItem; + String path = (String) pathMap.get("path"); + boolean encrypted = toBooleanSafe(pathMap.get("encrypted")); + if (path != null) { + operationPaths.add(new OperationPath(path, encrypted)); + } + } else if (pathItem instanceof String) { + operationPaths.add(new OperationPath((String) pathItem, false)); + } + } + if (!operationPaths.isEmpty()) { + result.add(new AllowedOperation(op, operationPaths)); + } + } + } + return result.isEmpty() ? null : result; + } + + /** + * Safely converts a value to boolean, handling both {@link Boolean} and {@link String} types. + * Jackson deserializes JSON {@code true} as {@link Boolean} but JSON {@code "true"} as {@link String}. + * + * @param value The value to convert. + * @return {@code true} if the value is Boolean TRUE or the string "true" (case-insensitive). + */ + private static boolean toBooleanSafe(Object value) { + + if (value instanceof Boolean) { + return (Boolean) value; + } + if (value instanceof String) { + return Boolean.parseBoolean((String) value); + } + return false; + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java index 2dbdaae25e70..fbd8c88e0b5b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java @@ -73,6 +73,19 @@ public class InFlowExtensionActionConstants { */ public static final int MAX_EXPOSE_PATHS = 50; + /** + * Property key for the external service's certificate stored in IDN_ACTION_PROPERTIES. + * During persistence the certificate object is replaced with its UUID (PRIMITIVE string); + * during retrieval the UUID is resolved back to the full Certificate object. + */ + public static final String CERTIFICATE = "CERTIFICATE"; + + /** + * Naming prefix for certificates stored via CertificateManagementService. + * Full name format: {@code "ACTIONS:"}. + */ + public static final String CERTIFICATE_NAME_PREFIX = "ACTIONS:"; + private InFlowExtensionActionConstants() { } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java index fea0f774718d..cb44c2d81a65 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java @@ -22,7 +22,11 @@ import org.wso2.carbon.identity.action.management.api.model.ActionDTO; import org.wso2.carbon.identity.action.management.api.model.ActionProperty; import org.wso2.carbon.identity.action.management.api.service.ActionConverter; +import org.wso2.carbon.identity.certificate.management.model.Certificate; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AllowedOperation; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ExposePath; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; import java.util.HashMap; @@ -31,6 +35,7 @@ import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_ALLOWED_OPERATIONS; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.CERTIFICATE; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE_PREFIX; @@ -81,6 +86,13 @@ public ActionDTO buildActionDTO(Action action) { } } + // Encryption certificate (separate from access config). + Encryption encryption = inFlowExtensionAction.getEncryption(); + if (encryption != null && encryption.getCertificate() != null) { + properties.put(CERTIFICATE, + new ActionProperty.BuilderForService(encryption.getCertificate()).build()); + } + // Per-flow-type overrides using prefixed keys. Map overrides = inFlowExtensionAction.getFlowTypeOverrides(); if (overrides != null) { @@ -115,31 +127,40 @@ public ActionDTO buildActionDTO(Action action) { public Action buildAction(ActionDTO actionDTO) { // Default access config. - List expose = (List) actionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE); - List> allowedOperations = - (List>) actionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS); + List expose = (List) actionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE); + List allowedOperations = + (List) actionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS); AccessConfig accessConfig = null; if (expose != null || allowedOperations != null) { accessConfig = new AccessConfig(expose, allowedOperations); } + // Encryption certificate (separate from access config). + Encryption encryption = null; + Object certValue = actionDTO.getPropertyValue(CERTIFICATE); + if (certValue instanceof Certificate) { + encryption = new Encryption((Certificate) certValue); + } + // Reconstruct per-flow-type overrides from prefixed keys. Map flowTypeOverrides = new HashMap<>(); if (actionDTO.getProperties() != null) { for (String propertyKey : actionDTO.getProperties().keySet()) { if (propertyKey.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX)) { String flowType = propertyKey.substring(ACCESS_CONFIG_EXPOSE_PREFIX.length()); - AccessConfig existing = flowTypeOverrides.getOrDefault(flowType, new AccessConfig(null, null)); + AccessConfig existing = flowTypeOverrides.getOrDefault(flowType, + new AccessConfig(null, null)); flowTypeOverrides.put(flowType, new AccessConfig( - (List) actionDTO.getPropertyValue(propertyKey), + (List) actionDTO.getPropertyValue(propertyKey), existing.getAllowedOperations())); } else if (propertyKey.startsWith(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX)) { String flowType = propertyKey.substring(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX.length()); - AccessConfig existing = flowTypeOverrides.getOrDefault(flowType, new AccessConfig(null, null)); + AccessConfig existing = flowTypeOverrides.getOrDefault(flowType, + new AccessConfig(null, null)); flowTypeOverrides.put(flowType, new AccessConfig( existing.getExpose(), - (List>) actionDTO.getPropertyValue(propertyKey))); + (List) actionDTO.getPropertyValue(propertyKey))); } } } @@ -155,6 +176,7 @@ public Action buildAction(ActionDTO actionDTO) { .updatedAt(actionDTO.getUpdatedAt()) .endpoint(actionDTO.getEndpoint()) .accessConfig(accessConfig) + .encryption(encryption) .flowTypeOverrides(flowTypeOverrides) .rule(actionDTO.getActionRule()) .build(); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java index ba420ce5af44..ec4ac4da1b15 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java @@ -30,6 +30,12 @@ import org.wso2.carbon.identity.action.management.api.model.ActionProperty; import org.wso2.carbon.identity.action.management.api.model.BinaryObject; import org.wso2.carbon.identity.action.management.api.service.ActionDTOModelResolver; +import org.wso2.carbon.identity.certificate.management.exception.CertificateMgtException; +import org.wso2.carbon.identity.certificate.management.model.Certificate; +import org.wso2.carbon.identity.certificate.management.service.CertificateManagementService; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AllowedOperation; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ExposePath; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.OperationPath; import java.io.IOException; import java.util.ArrayList; @@ -42,8 +48,10 @@ import static java.util.Collections.emptyList; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_ALLOWED_OPERATIONS; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.CERTIFICATE; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE_PREFIX; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.CERTIFICATE_NAME_PREFIX; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.MAX_EXPOSE_PATHS; /** @@ -65,10 +73,13 @@ public class InFlowExtensionActionDTOModelResolver implements ActionDTOModelReso private static final Log LOG = LogFactory.getLog(InFlowExtensionActionDTOModelResolver.class); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final TypeReference> STRING_LIST_TYPE_REF = - new TypeReference>() { }; - private static final TypeReference>> MAP_LIST_TYPE_REF = - new TypeReference>>() { }; + + private final CertificateManagementService certificateManagementService; + + public InFlowExtensionActionDTOModelResolver(CertificateManagementService certificateManagementService) { + + this.certificateManagementService = certificateManagementService; + } @Override public Action.ActionTypes getSupportedActionType() { @@ -85,17 +96,20 @@ public ActionDTO resolveForAddOperation(ActionDTO actionDTO, String tenantDomain Object exposeValue = actionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE); // Expose is an optional field. if (exposeValue != null) { - List validatedExpose = validateExpose(exposeValue); + List validatedExpose = validateExpose(exposeValue); properties.put(ACCESS_CONFIG_EXPOSE, createBlobProperty(validatedExpose)); } Object allowedOpsValue = actionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS); // Allowed operations is an optional field. if (allowedOpsValue != null) { - List> validatedOps = validateAllowedOperations(allowedOpsValue); + List validatedOps = validateAllowedOperations(allowedOpsValue); properties.put(ACCESS_CONFIG_ALLOWED_OPERATIONS, createBlobProperty(validatedOps)); } + // Handle certificate: store via CertificateManagementService and replace with ID. + handleCertificateAdd(actionDTO, properties, tenantDomain); + // Handle per-flow-type override properties (prefixed keys). if (actionDTO.getProperties() != null) { for (Map.Entry entry : actionDTO.getProperties().entrySet()) { @@ -103,13 +117,13 @@ public ActionDTO resolveForAddOperation(ActionDTO actionDTO, String tenantDomain if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX)) { Object overrideExpose = actionDTO.getPropertyValue(key); if (overrideExpose != null) { - List validatedOverrideExpose = validateExpose(overrideExpose); + List validatedOverrideExpose = validateExpose(overrideExpose); properties.put(key, createBlobProperty(validatedOverrideExpose)); } } else if (key.startsWith(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX)) { Object overrideOps = actionDTO.getPropertyValue(key); if (overrideOps != null) { - List> validatedOverrideOps = validateAllowedOperations(overrideOps); + List validatedOverrideOps = validateAllowedOperations(overrideOps); properties.put(key, createBlobProperty(validatedOverrideOps)); } } @@ -138,6 +152,9 @@ public ActionDTO resolveForGetOperation(ActionDTO actionDTO, String tenantDomain ((BinaryObject) actionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS)).getJSONString())); } + // Retrieve certificate by stored ID. + handleCertificateGet(actionDTO, properties, tenantDomain); + // Deserialize per-flow-type override properties (prefixed keys). if (actionDTO.getProperties() != null) { for (Map.Entry entry : actionDTO.getProperties().entrySet()) { @@ -191,17 +208,20 @@ public ActionDTO resolveForUpdateOperation(ActionDTO updatingActionDTO, ActionDT // updated the existing properties should be sent to the DAO layer. // Handle default access config properties. - List expose = getResolvedUpdatingExpose(updatingActionDTO, existingActionDTO); + List expose = getResolvedUpdatingExpose(updatingActionDTO, existingActionDTO); if (!expose.isEmpty()) { properties.put(ACCESS_CONFIG_EXPOSE, createBlobProperty(expose)); } - List> allowedOps = + List allowedOps = getResolvedUpdatingAllowedOps(updatingActionDTO, existingActionDTO); if (!allowedOps.isEmpty()) { properties.put(ACCESS_CONFIG_ALLOWED_OPERATIONS, createBlobProperty(allowedOps)); } + // Handle certificate update. + handleCertificateUpdate(updatingActionDTO, existingActionDTO, properties, tenantDomain); + // Handle per-flow-type override properties. Since DAO treats update as PUT (full replace), // we must carry forward all existing overrides that are not explicitly being updated. // First, carry forward all existing per-flow-type overrides. @@ -221,13 +241,13 @@ public ActionDTO resolveForUpdateOperation(ActionDTO updatingActionDTO, ActionDT if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX)) { Object overrideExpose = updatingActionDTO.getPropertyValue(key); if (overrideExpose != null) { - List validatedOverrideExpose = validateExpose(overrideExpose); + List validatedOverrideExpose = validateExpose(overrideExpose); properties.put(key, createBlobProperty(validatedOverrideExpose)); } } else if (key.startsWith(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX)) { Object overrideOps = updatingActionDTO.getPropertyValue(key); if (overrideOps != null) { - List> validatedOverrideOps = validateAllowedOperations(overrideOps); + List validatedOverrideOps = validateAllowedOperations(overrideOps); properties.put(key, createBlobProperty(validatedOverrideOps)); } } @@ -242,32 +262,34 @@ public ActionDTO resolveForUpdateOperation(ActionDTO updatingActionDTO, ActionDT @Override public void resolveForDeleteOperation(ActionDTO deletingActionDTO, String tenantDomain) throws ActionDTOModelResolverException { - // No-op: properties are cascade-deleted with the action in IDN_ACTION_PROPERTIES. + + // Delete the certificate if one was stored for this action. + handleCertificateDelete(deletingActionDTO, tenantDomain); } // ---- Update helpers ---- @SuppressWarnings("unchecked") - private List getResolvedUpdatingExpose(ActionDTO updatingActionDTO, ActionDTO existingActionDTO) + private List getResolvedUpdatingExpose(ActionDTO updatingActionDTO, ActionDTO existingActionDTO) throws ActionDTOModelResolverException { if (updatingActionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE) != null) { return validateExpose(updatingActionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE)); } else if (existingActionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE) != null) { - return (List) existingActionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE); + return (List) existingActionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE); } return emptyList(); } @SuppressWarnings("unchecked") - private List> getResolvedUpdatingAllowedOps(ActionDTO updatingActionDTO, - ActionDTO existingActionDTO) + private List getResolvedUpdatingAllowedOps(ActionDTO updatingActionDTO, + ActionDTO existingActionDTO) throws ActionDTOModelResolverException { if (updatingActionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS) != null) { return validateAllowedOperations(updatingActionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS)); } else if (existingActionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS) != null) { - return (List>) existingActionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS); + return (List) existingActionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS); } return emptyList(); } @@ -275,28 +297,43 @@ private List> getResolvedUpdatingAllowedOps(ActionDTO updati // ---- Validation ---- @SuppressWarnings("unchecked") - private List validateExpose(Object exposeValue) throws ActionDTOModelResolverException { + private List validateExpose(Object exposeValue) throws ActionDTOModelResolverException { if (!(exposeValue instanceof List)) { throw new ActionDTOModelResolverClientException("Invalid expose format.", - "Expose should be provided as a list of strings."); + "Expose should be provided as a list."); } List exposeList = (List) exposeValue; + List result = new ArrayList<>(); + for (Object item : exposeList) { - if (!(item instanceof String)) { + if (item instanceof String) { + // Simple string path — no encryption. + result.add(new ExposePath((String) item, false)); + } else if (item instanceof Map) { + Map map = (Map) item; + if (!map.containsKey("path") || !(map.get("path") instanceof String)) { + throw new ActionDTOModelResolverClientException("Invalid expose format.", + "Each expose entry must be a string or an object with a 'path' field."); + } + String path = (String) map.get("path"); + boolean encrypted = map.containsKey("encrypted") && toBooleanSafe(map.get("encrypted")); + result.add(new ExposePath(path, encrypted)); + } else if (item instanceof ExposePath) { + result.add((ExposePath) item); + } else { throw new ActionDTOModelResolverClientException("Invalid expose format.", - "Expose must contain only string values."); + "Each expose entry must be a string or an object with 'path' and optional 'encrypted' fields."); } } - List expose = (List) exposeValue; - validateExposeCount(expose); - validateExposePathFormat(expose); - return expose; + validateExposeCount(result); + validateExposePathFormat(result); + return result; } - private void validateExposeCount(List expose) throws ActionDTOModelResolverClientException { + private void validateExposeCount(List expose) throws ActionDTOModelResolverClientException { if (expose.size() > MAX_EXPOSE_PATHS) { throw new ActionDTOModelResolverClientException("Maximum expose paths limit exceeded.", @@ -305,10 +342,11 @@ private void validateExposeCount(List expose) throws ActionDTOModelResol } } - private void validateExposePathFormat(List expose) throws ActionDTOModelResolverClientException { + private void validateExposePathFormat(List expose) throws ActionDTOModelResolverClientException { Set seen = new HashSet<>(); - for (String path : expose) { + for (ExposePath exposePath : expose) { + String path = exposePath.getPath(); if (path == null || path.trim().isEmpty()) { throw new ActionDTOModelResolverClientException("Invalid expose path.", "Expose paths must not be null or empty."); @@ -325,7 +363,7 @@ private void validateExposePathFormat(List expose) throws ActionDTOModel } @SuppressWarnings("unchecked") - private List> validateAllowedOperations(Object allowedOpsValue) + private List validateAllowedOperations(Object allowedOpsValue) throws ActionDTOModelResolverClientException { if (!(allowedOpsValue instanceof List)) { @@ -334,23 +372,209 @@ private List> validateAllowedOperations(Object allowedOpsVal } List opsList = (List) allowedOpsValue; + List result = new ArrayList<>(); + for (Object item : opsList) { - if (!(item instanceof Map)) { + if (item instanceof AllowedOperation) { + result.add((AllowedOperation) item); + } else if (item instanceof Map) { + Map opMap = (Map) item; + if (!opMap.containsKey("op") || !(opMap.get("op") instanceof String)) { + throw new ActionDTOModelResolverClientException("Invalid allowed operation.", + "Each allowed operation must contain an 'op' field with a string value."); + } + String op = (String) opMap.get("op"); + + if (!opMap.containsKey("paths") || !(opMap.get("paths") instanceof List)) { + throw new ActionDTOModelResolverClientException("Invalid allowed operation.", + "Each allowed operation must contain a 'paths' list."); + } + + List pathsList = (List) opMap.get("paths"); + List operationPaths = new ArrayList<>(); + for (Object pathItem : pathsList) { + if (pathItem instanceof OperationPath) { + operationPaths.add((OperationPath) pathItem); + } else if (pathItem instanceof Map) { + Map pathMap = (Map) pathItem; + if (!pathMap.containsKey("path") || !(pathMap.get("path") instanceof String)) { + throw new ActionDTOModelResolverClientException("Invalid operation path.", + "Each path entry must contain a 'path' field with a string value."); + } + String path = (String) pathMap.get("path"); + boolean encrypted = pathMap.containsKey("encrypted") + && toBooleanSafe(pathMap.get("encrypted")); + operationPaths.add(new OperationPath(path, encrypted)); + } else if (pathItem instanceof String) { + // Simple string path — no encryption. + operationPaths.add(new OperationPath((String) pathItem, false)); + } else { + throw new ActionDTOModelResolverClientException("Invalid operation path format.", + "Each path entry must be a string or an object with 'path' and " + + "optional 'encrypted' fields."); + } + } + result.add(new AllowedOperation(op, operationPaths)); + } else { throw new ActionDTOModelResolverClientException("Invalid allowed operations format.", - "Each allowed operation must be an object with 'op' and 'paths' keys."); + "Each allowed operation must be an object with 'op' and 'paths' fields."); } - Map opMap = (Map) item; - if (!opMap.containsKey("op") || !(opMap.get("op") instanceof String)) { - throw new ActionDTOModelResolverClientException("Invalid allowed operation.", - "Each allowed operation must contain an 'op' field with a string value."); - } - if (!opMap.containsKey("paths") || !(opMap.get("paths") instanceof List)) { - throw new ActionDTOModelResolverClientException("Invalid allowed operation.", - "Each allowed operation must contain a 'paths' field with a list of strings."); + } + + return result; + } + + // ---- Certificate lifecycle helpers ---- + + /** + * Stores the external service's certificate via CertificateManagementService during action creation. + * The certificate PEM is replaced with the stored certificate's ID as a PRIMITIVE property. + */ + private void handleCertificateAdd(ActionDTO actionDTO, Map properties, + String tenantDomain) throws ActionDTOModelResolverException { + + Object certValue = actionDTO.getPropertyValue(CERTIFICATE); + if (certValue == null) { + return; + } + + String certificatePEM = extractCertificatePEM(certValue); + String certName = CERTIFICATE_NAME_PREFIX + actionDTO.getId(); + + try { + String certificateId = certificateManagementService.addCertificate( + new Certificate.Builder() + .name(certName) + .certificateContent(certificatePEM) + .build(), + tenantDomain); + + // Store the certificate ID as a primitive property so DAO persists just the ID. + properties.put(CERTIFICATE, + new ActionProperty.BuilderForDAO(certificateId).build()); + } catch (CertificateMgtException e) { + throw new ActionDTOModelResolverException("Error storing certificate for action: " + + actionDTO.getId(), e); + } + } + + /** + * Retrieves the certificate by its stored ID during action get operations. + * Replaces the stored ID with the full Certificate object as a service-layer property. + */ + private void handleCertificateGet(ActionDTO actionDTO, Map properties, + String tenantDomain) throws ActionDTOModelResolverException { + + Object certIdValue = actionDTO.getPropertyValue(CERTIFICATE); + if (certIdValue == null) { + return; + } + + try { + String certIdStr = certIdValue.toString(); + Certificate certificate = certificateManagementService.getCertificate( + certIdStr, tenantDomain); + properties.put(CERTIFICATE, + new ActionProperty.BuilderForService(certificate).build()); + } catch (CertificateMgtException e) { + throw new ActionDTOModelResolverException("Error retrieving certificate for action: " + + actionDTO.getId(), e); + } + } + + /** + * Handles certificate lifecycle during action update: add new, update existing, delete, or carry forward. + */ + private void handleCertificateUpdate(ActionDTO updatingActionDTO, ActionDTO existingActionDTO, + Map properties, String tenantDomain) + throws ActionDTOModelResolverException { + + Object newCertValue = updatingActionDTO.getPropertyValue(CERTIFICATE); + Object existingCertValue = existingActionDTO.getPropertyValue(CERTIFICATE); + + if (newCertValue != null && existingCertValue != null) { + // Update existing certificate. + String certificatePEM = extractCertificatePEM(newCertValue); + try { + String existingCertId = extractCertificateId(existingCertValue); + certificateManagementService.updateCertificateContent( + existingCertId, certificatePEM, tenantDomain); + // Carry forward the existing certificate ID. + properties.put(CERTIFICATE, + new ActionProperty.BuilderForDAO(existingCertId).build()); + } catch (CertificateMgtException e) { + throw new ActionDTOModelResolverException("Error updating certificate for action: " + + updatingActionDTO.getId(), e); } + } else if (newCertValue != null) { + // Add new certificate (previously had none). + handleCertificateAdd(updatingActionDTO, properties, tenantDomain); + } else if (existingCertValue != null) { + // No new certificate provided — carry forward the existing one (PUT semantics). + properties.put(CERTIFICATE, + new ActionProperty.BuilderForDAO(extractCertificateId(existingCertValue)).build()); } + // else: both null — no certificate to handle. + } - return (List>) allowedOpsValue; + /** + * Deletes the certificate from IDN_CERTIFICATE during action deletion. + */ + private void handleCertificateDelete(ActionDTO deletingActionDTO, String tenantDomain) + throws ActionDTOModelResolverException { + + Object certIdValue = deletingActionDTO.getPropertyValue(CERTIFICATE); + if (certIdValue == null) { + return; + } + + try { + String certId = certIdValue.toString(); + certificateManagementService.deleteCertificate(certId, tenantDomain); + } catch (CertificateMgtException e) { + throw new ActionDTOModelResolverException("Error deleting certificate for action: " + + deletingActionDTO.getId(), e); + } + } + + /** + * Extracts the certificate UUID from a certificate property value. + *

+ * The existing action DTO may come from the GET resolver, which replaces the stored UUID + * with the full {@link Certificate} object. This method handles both cases: + * - {@link Certificate} object: extracts the ID via {@code getId()}. + * - String: assumes it is already the UUID. + *

+ */ + private String extractCertificateId(Object certValue) { + + if (certValue instanceof Certificate) { + return ((Certificate) certValue).getId(); + } + return certValue.toString(); + } + + /** + * Extracts the PEM string from a certificate value, which may be a Certificate object, + * a Map, or a plain string. + */ + private String extractCertificatePEM(Object certValue) throws ActionDTOModelResolverClientException { + + if (certValue instanceof Certificate) { + return ((Certificate) certValue).getCertificateContent(); + } else if (certValue instanceof Map) { + Map certMap = (Map) certValue; + Object content = certMap.get("certificateContent"); + if (content instanceof String) { + return (String) content; + } + throw new ActionDTOModelResolverClientException("Invalid certificate format.", + "Certificate object must contain a 'certificateContent' field."); + } else if (certValue instanceof String) { + return (String) certValue; + } + throw new ActionDTOModelResolverClientException("Invalid certificate format.", + "Certificate must be a PEM string, a Certificate object, or a map with 'certificateContent'."); } // ---- Serialization helpers ---- @@ -368,7 +592,8 @@ private ActionProperty createBlobProperty(Object value) throws ActionDTOModelRes private ActionProperty deserializeExposeProperty(String jsonString) throws ActionDTOModelResolverException { try { - List expose = OBJECT_MAPPER.readValue(jsonString, STRING_LIST_TYPE_REF); + List expose = OBJECT_MAPPER.readValue(jsonString, + new TypeReference>() { }); return new ActionProperty.BuilderForService(expose).build(); } catch (IOException e) { throw new ActionDTOModelResolverException("Error reading expose values from storage.", e); @@ -379,10 +604,29 @@ private ActionProperty deserializeAllowedOpsProperty(String jsonString) throws ActionDTOModelResolverException { try { - List> allowedOps = OBJECT_MAPPER.readValue(jsonString, MAP_LIST_TYPE_REF); + List allowedOps = OBJECT_MAPPER.readValue(jsonString, + new TypeReference>() { }); return new ActionProperty.BuilderForService(allowedOps).build(); } catch (IOException e) { throw new ActionDTOModelResolverException("Error reading allowed operations from storage.", e); } } + + /** + * Safely converts a value to boolean, handling both {@link Boolean} and {@link String} types. + * Jackson deserializes JSON {@code true} as {@link Boolean} but JSON {@code "true"} as {@link String}. + * + * @param value The value to convert. + * @return {@code true} if the value is Boolean TRUE or the string "true" (case-insensitive). + */ + private static boolean toBooleanSafe(Object value) { + + if (value instanceof Boolean) { + return (Boolean) value; + } + if (value instanceof String) { + return Boolean.parseBoolean((String) value); + } + return false; + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java index 77164398e91d..7ef2cab4de85 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java @@ -20,7 +20,9 @@ import java.util.Collections; import java.util.List; -import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Access Configuration for In-Flow Extension actions. @@ -30,47 +32,145 @@ *

* *
    - *
  • {@code expose} – hierarchical path prefixes that control which context data is sent - * in the action execution request (e.g. {@code "/user/claims/"}, {@code "/flow/properties/"}).
  • - *
  • {@code allowedOperations} – a structured list of operation descriptors, where each entry - * specifies an operation type ({@code "op"}) and the paths it applies to ({@code "paths"}).
  • + *
  • {@code expose} – structured list of {@link ExposePath} entries, each with a hierarchical + * path prefix and an {@code encrypted} flag controlling outbound JWE encryption.
  • + *
  • {@code allowedOperations} – list of {@link AllowedOperation} entries, each grouping an + * operation type with a list of {@link OperationPath} entries carrying per-path encryption + * flags for inbound JWE encryption.
  • *
+ * + *

Note: The external service's certificate for outbound encryption is held in the + * separate {@link Encryption} model, not in AccessConfig. This follows the same separation + * pattern as {@code PasswordSharing} in the PreUpdatePassword action type.

*/ public class AccessConfig { - private final List expose; - private final List> allowedOperations; + /** + * Regex pattern to match path type annotations at the end of a path. + * Strips trailing bracket expressions like {@code []}, {@code [field1, field2]}. + * These annotations are used by the request builder for type coercion but are not + * part of the logical path that the external service sees. + */ + private static final Pattern PATH_ANNOTATION_PATTERN = Pattern.compile("\\[([^\\]]*)]$"); + + private final List expose; + private final List allowedOperations; /** * Constructs an AccessConfig with the given expose paths and allowed operations. * - * @param expose List of hierarchical path prefixes to expose. May be {@code null}. - * @param allowedOperations List of operation descriptors. May be {@code null}. + * @param expose List of expose path entries. May be {@code null}. + * @param allowedOperations List of allowed operation entries. May be {@code null}. */ - public AccessConfig(List expose, List> allowedOperations) { + public AccessConfig(List expose, List allowedOperations) { this.expose = expose != null ? Collections.unmodifiableList(expose) : null; - this.allowedOperations = allowedOperations != null ? Collections.unmodifiableList(allowedOperations) : null; + this.allowedOperations = allowedOperations != null + ? Collections.unmodifiableList(allowedOperations) : null; } /** - * Returns the list of hierarchical path prefixes that define what context data is exposed. + * Returns the list of expose path entries. * - * @return Unmodifiable list of expose path prefixes, or {@code null} if not configured. + * @return Unmodifiable list of {@link ExposePath} entries, or {@code null} if not configured. */ - public List getExpose() { + public List getExpose() { return expose; } /** - * Returns the list of allowed operation descriptors. - * Each entry is a map with at least {@code "op"} (operation type) and {@code "paths"} keys. + * Returns the flat list of expose path strings (without encryption metadata). + * Convenience method for components that only need path prefixes. * - * @return Unmodifiable list of operation descriptors, or {@code null} if not configured. + * @return List of path strings, or {@code null} if expose is not configured. */ - public List> getAllowedOperations() { + public List getExposePaths() { + + if (expose == null) { + return null; + } + return expose.stream().map(ExposePath::getPath).collect(Collectors.toList()); + } + + /** + * Returns the list of allowed operation entries. + * + * @return Unmodifiable list of {@link AllowedOperation} entries, or {@code null} if not configured. + */ + public List getAllowedOperations() { return allowedOperations; } + + /** + * Check if a given path prefix has outbound encryption enabled. + * Matches the most specific (longest) expose path that is a prefix of the given path. + * + * @param pathPrefix The path to check. + * @return {@code true} if the matching expose entry has {@code encrypted = true}. + */ + public boolean isExposePathEncrypted(String pathPrefix) { + + if (expose == null) { + return false; + } + return expose.stream() + .filter(ep -> pathPrefix.startsWith(ep.getPath())) + .reduce((a, b) -> a.getPath().length() >= b.getPath().length() ? a : b) + .map(ExposePath::isEncrypted) + .orElse(false); + } + + /** + * Check if a given operation path has inbound encryption enabled. + * Iterates the nested {@link OperationPath} entries within each {@link AllowedOperation}. + * Path type annotations (e.g. {@code []}, {@code [schema]}) are stripped from stored paths + * before comparison, since operation paths from the external service do not include annotations. + * + * @param op The operation type (e.g. "REPLACE"). + * @param path The operation path (clean, without annotations). + * @return {@code true} if the matching operation path has {@code encrypted = true}. + */ + public boolean isOperationPathEncrypted(String op, String path) { + + if (allowedOperations == null) { + return false; + } + return allowedOperations.stream() + .filter(ao -> ao.getOp().equalsIgnoreCase(op)) + .flatMap(ao -> ao.getPaths().stream()) + .filter(opPath -> path.startsWith(stripAnnotation(opPath.getPath()))) + .reduce((a, b) -> stripAnnotation(a.getPath()).length() + >= stripAnnotation(b.getPath()).length() ? a : b) + .map(OperationPath::isEncrypted) + .orElse(false); + } + + /** + * Returns whether any expose path or allowed operation path has encryption enabled. + * + * @return {@code true} if at least one path has {@code encrypted = true}. + */ + public boolean hasAnyEncryptedPath() { + + boolean hasEncryptedExpose = expose != null + && expose.stream().anyMatch(ExposePath::isEncrypted); + boolean hasEncryptedOps = allowedOperations != null + && allowedOperations.stream().anyMatch(AllowedOperation::hasAnyEncryptedPath); + return hasEncryptedExpose || hasEncryptedOps; + } + + /** + * Strip trailing path type annotations from a path string. + * For example, {@code "/properties/riskFactors[]"} becomes {@code "/properties/riskFactors"}. + * + * @param path The path that may contain trailing annotations. + * @return The path without annotations. + */ + private static String stripAnnotation(String path) { + + Matcher m = PATH_ANNOTATION_PATTERN.matcher(path); + return m.find() ? path.substring(0, m.start()) : path; + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AllowedOperation.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AllowedOperation.java new file mode 100644 index 000000000000..d040ea86afac --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AllowedOperation.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025, 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.inflow.extension.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Collections; +import java.util.List; + +/** + * Domain model for an allowed operation with structured paths. + *

+ * Each entry groups an operation type with a list of {@link OperationPath} entries, + * where each path carries its own {@code encrypted} flag. + *

+ * + *

Example JSON:

+ *
{@code
+ * {
+ *   "op": "REPLACE",
+ *   "paths": [
+ *     { "path": "/user/claims/", "encrypted": false },
+ *     { "path": "/user/credentials/password", "encrypted": true }
+ *   ]
+ * }
+ * }
+ */ +public class AllowedOperation { + + private final String op; + private final List paths; + + @JsonCreator + public AllowedOperation(@JsonProperty("op") String op, + @JsonProperty("paths") List paths) { + + this.op = op; + this.paths = paths != null ? Collections.unmodifiableList(paths) : Collections.emptyList(); + } + + /** + * Returns the operation type (e.g. {@code "ADD"}, {@code "REPLACE"}, {@code "REMOVE"}). + * + * @return The operation type string. + */ + public String getOp() { + + return op; + } + + /** + * Returns the list of {@link OperationPath} entries for this operation. + * + * @return Unmodifiable list of operation paths with encryption flags. + */ + public List getPaths() { + + return paths; + } + + /** + * Returns whether any path in this operation has encryption enabled. + * + * @return {@code true} if at least one path has {@code encrypted = true}. + */ + public boolean hasAnyEncryptedPath() { + + return paths.stream().anyMatch(OperationPath::isEncrypted); + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/Encryption.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/Encryption.java new file mode 100644 index 000000000000..a3b72ecafeaa --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/Encryption.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025, 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.inflow.extension.model; + +import org.wso2.carbon.identity.certificate.management.model.Certificate; + +/** + * Encryption configuration for In-Flow Extension actions. + *

+ * Holds the external service's X.509 public certificate used for outbound JWE encryption. + * This model is separate from {@link AccessConfig} — following the same pattern as + * {@code PasswordSharing} in the PreUpdatePassword action type. + *

+ *

+ * The IS uses this certificate to encrypt expose path values marked {@code encrypted: true} + * before sending them to the external service. For inbound encryption (Extension → IS), + * the external service must obtain the IS's public key out-of-band. + *

+ */ +public class Encryption { + + private final Certificate certificate; + + /** + * Constructs an Encryption configuration with the given certificate. + * + * @param certificate The external service's X.509 public certificate. + * May be {@code null} if encryption is not configured. + */ + public Encryption(Certificate certificate) { + + this.certificate = certificate; + } + + /** + * Returns the external service's X.509 public certificate. + * + * @return The certificate, or {@code null} if not configured. + */ + public Certificate getCertificate() { + + return certificate; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/ExposePath.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/ExposePath.java new file mode 100644 index 000000000000..04728ac59153 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/ExposePath.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025, 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.inflow.extension.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Domain model for an expose path with an optional encryption flag. + *

+ * When {@code encrypted} is {@code true}, the value at this path prefix is JWE-encrypted + * in the outbound request using the external service's public certificate before sending. + *

+ */ +public class ExposePath { + + private final String path; + private final boolean encrypted; + + @JsonCreator + public ExposePath(@JsonProperty("path") String path, + @JsonProperty("encrypted") boolean encrypted) { + + this.path = path; + this.encrypted = encrypted; + } + + /** + * Returns the hierarchical path prefix (e.g. {@code "/user/credentials/"}). + * + * @return The path string. + */ + public String getPath() { + + return path; + } + + /** + * Returns whether values at this path should be JWE-encrypted before sending + * to the external service. + * + * @return {@code true} if encryption is enabled for this path. + */ + public boolean isEncrypted() { + + return encrypted; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java index 2abf96831962..c4539d783235 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java @@ -36,12 +36,14 @@ public class InFlowExtensionAction extends Action { private final AccessConfig accessConfig; + private final Encryption encryption; private final Map flowTypeOverrides; public InFlowExtensionAction(ResponseBuilder responseBuilder) { super(responseBuilder); this.accessConfig = responseBuilder.accessConfig; + this.encryption = responseBuilder.encryption; this.flowTypeOverrides = responseBuilder.flowTypeOverrides != null ? Collections.unmodifiableMap(new HashMap<>(responseBuilder.flowTypeOverrides)) : Collections.emptyMap(); @@ -51,6 +53,7 @@ public InFlowExtensionAction(RequestBuilder requestBuilder) { super(requestBuilder); this.accessConfig = requestBuilder.accessConfig; + this.encryption = requestBuilder.encryption; this.flowTypeOverrides = requestBuilder.flowTypeOverrides != null ? Collections.unmodifiableMap(new HashMap<>(requestBuilder.flowTypeOverrides)) : Collections.emptyMap(); @@ -66,6 +69,16 @@ public AccessConfig getAccessConfig() { return accessConfig; } + /** + * Returns the encryption configuration holding the external service's certificate. + * + * @return The encryption config, or {@code null} if not configured. + */ + public Encryption getEncryption() { + + return encryption; + } + /** * Returns the per-flow-type access config overrides. * Keys are flow type strings (e.g., "REGISTRATION", "LOGIN"). @@ -99,6 +112,7 @@ public AccessConfig resolveAccessConfig(String flowType) { public static class ResponseBuilder extends ActionResponseBuilder { private AccessConfig accessConfig; + private Encryption encryption; private Map flowTypeOverrides; @Override @@ -177,6 +191,12 @@ public ResponseBuilder accessConfig(AccessConfig accessConfig) { return this; } + public ResponseBuilder encryption(Encryption encryption) { + + this.encryption = encryption; + return this; + } + public ResponseBuilder flowTypeOverrides(Map flowTypeOverrides) { this.flowTypeOverrides = flowTypeOverrides; @@ -197,6 +217,7 @@ public InFlowExtensionAction build() { public static class RequestBuilder extends ActionRequestBuilder { private AccessConfig accessConfig; + private Encryption encryption; private Map flowTypeOverrides; public RequestBuilder(Action action) { @@ -235,6 +256,12 @@ public RequestBuilder accessConfig(AccessConfig accessConfig) { return this; } + public RequestBuilder encryption(Encryption encryption) { + + this.encryption = encryption; + return this; + } + public RequestBuilder flowTypeOverrides(Map flowTypeOverrides) { this.flowTypeOverrides = flowTypeOverrides; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationPath.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationPath.java new file mode 100644 index 000000000000..bb4abaa64bbd --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationPath.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2025, 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.inflow.extension.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Domain model for an operation path with an optional encryption flag. + *

+ * Used within {@link AllowedOperation} to represent a single path on which + * the operation is allowed, along with an encryption flag indicating whether + * the IS should expect/produce JWE-encrypted values for that specific path. + *

+ */ +public class OperationPath { + + private final String path; + private final boolean encrypted; + + @JsonCreator + public OperationPath(@JsonProperty("path") String path, + @JsonProperty("encrypted") boolean encrypted) { + + this.path = path; + this.encrypted = encrypted; + } + + /** + * Returns the operation path (e.g. {@code "/user/claims/"}, {@code "/properties/riskScore"}). + * + * @return The path string. + */ + public String getPath() { + + return path; + } + + /** + * Returns whether this operation path requires JWE encryption/decryption. + *
    + *
  • ADD operations: external service sends JWE-encrypted values to the IS.
  • + *
  • REPLACE operations: both directions — outbound current value encrypted, + * inbound replacement value encrypted.
  • + *
  • REMOVE operations: outbound current value encrypted in the expose event.
  • + *
+ * + * @return {@code true} if JWE encryption is required for this path. + */ + public boolean isEncrypted() { + + return encrypted; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java index 323458a451b6..8a38b6a0bc3f 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java @@ -21,6 +21,7 @@ import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; import org.wso2.carbon.identity.application.mgt.ApplicationManagementService; +import org.wso2.carbon.identity.certificate.management.service.CertificateManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; @@ -51,6 +52,7 @@ public class FlowExecutionEngineDataHolder { private IdentityEventService identityEventService; private ActionExecutorService actionExecutorService; private ActionManagementService actionManagementService; + private CertificateManagementService certificateManagementService; private List flowExecutionListeners = new ArrayList<>(); private FlowExecutionEngineDataHolder() { @@ -262,4 +264,24 @@ public void setActionManagementService(ActionManagementService actionManagementS this.actionManagementService = actionManagementService; } + + /** + * Get the CertificateManagementService instance. + * + * @return CertificateManagementService instance. + */ + public CertificateManagementService getCertificateManagementService() { + + return certificateManagementService; + } + + /** + * Set the CertificateManagementService instance. + * + * @param certificateManagementService CertificateManagementService instance. + */ + public void setCertificateManagementService(CertificateManagementService certificateManagementService) { + + this.certificateManagementService = certificateManagementService; + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java index ed25143e86e7..cda3fcdc80cb 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java @@ -35,6 +35,7 @@ import org.wso2.carbon.identity.action.management.api.service.ActionDTOModelResolver; import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; import org.wso2.carbon.identity.application.mgt.ApplicationManagementService; +import org.wso2.carbon.identity.certificate.management.service.CertificateManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.flow.execution.engine.FlowExecutionService; @@ -104,7 +105,9 @@ protected void activate(ComponentContext context) { bundleContext.registerService(ActionConverter.class.getName(), new InFlowExtensionActionConverter(), null); bundleContext.registerService(ActionDTOModelResolver.class.getName(), - new InFlowExtensionActionDTOModelResolver(), null); + new InFlowExtensionActionDTOModelResolver( + FlowExecutionEngineDataHolder.getInstance().getCertificateManagementService()), + null); // Register flow update interceptor for access config overrides bundleContext.registerService(FlowUpdateInterceptor.class.getName(), @@ -254,25 +257,6 @@ public void unsetFederatedAssociationManager(FederatedAssociationManager federat FlowExecutionEngineDataHolder.getInstance().setFederatedAssociationManager(null); } - @Reference( - name = "ActionExecutorService", - service = ActionExecutorService.class, - cardinality = ReferenceCardinality.MANDATORY, - policy = ReferencePolicy.DYNAMIC, - unbind = "unsetActionExecutorService" - ) - protected void setActionExecutorService(ActionExecutorService actionExecutorService) { - - LOG.debug("Setting the ActionExecutorService in the Flow Engine component."); - FlowExecutionEngineDataHolder.getInstance().setActionExecutorService(actionExecutorService); - } - - protected void unsetActionExecutorService(ActionExecutorService actionExecutorService) { - - LOG.debug("Unsetting the ActionExecutorService in the Flow Engine component."); - FlowExecutionEngineDataHolder.getInstance().setActionExecutorService(null); - } - @Reference( name = "ClaimMetadataManagementService", service = ClaimMetadataManagementService.class, @@ -329,4 +313,44 @@ protected void unsetActionExecutorService(ActionExecutorService actionExecutorSe LOG.debug("Unsetting the ActionExecutorService in the Flow Engine component."); FlowExecutionEngineDataHolder.getInstance().setActionExecutorService(null); } + + @Reference( + name = "ActionManagementService", + service = ActionManagementService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetActionManagementService" + ) + protected void setActionManagementService(ActionManagementService actionManagementService) { + + LOG.debug("Setting the ActionManagementService in the Flow Engine component."); + FlowExecutionEngineDataHolder.getInstance().setActionManagementService(actionManagementService); + } + + protected void unsetActionManagementService(ActionManagementService actionManagementService) { + + LOG.debug("Unsetting the ActionManagementService in the Flow Engine component."); + FlowExecutionEngineDataHolder.getInstance().setActionManagementService(null); + } + + @Reference( + name = "CertificateManagementService", + service = CertificateManagementService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetCertificateManagementService" + ) + protected void setCertificateManagementService(CertificateManagementService certificateManagementService) { + + LOG.debug("Setting the CertificateManagementService in the Flow Engine component."); + FlowExecutionEngineDataHolder.getInstance() + .setCertificateManagementService(certificateManagementService); + } + + protected void unsetCertificateManagementService( + CertificateManagementService certificateManagementService) { + + LOG.debug("Unsetting the CertificateManagementService in the Flow Engine component."); + FlowExecutionEngineDataHolder.getInstance().setCertificateManagementService(null); + } } diff --git a/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverConstants.java b/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverConstants.java index 0a8c7abddd7e..f78fe303f584 100644 --- a/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverConstants.java +++ b/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverConstants.java @@ -48,6 +48,7 @@ public class IdentityKeyStoreResolverConstants { public static final String INBOUND_PROTOCOL_SAML = "saml"; public static final String INBOUND_PROTOCOL_WS_TRUST = "ws-trust"; public static final String INBOUND_PROTOCOL_WS_FEDERATION = "ws-federation"; + public static final String INBOUND_PROTOCOL_ACTIONS = "actions"; /** * Enums for inbound protocols. @@ -58,7 +59,8 @@ public enum InboundProtocol { OAUTH(INBOUND_PROTOCOL_OAUTH), SAML(INBOUND_PROTOCOL_SAML), WS_TRUST(INBOUND_PROTOCOL_WS_TRUST), - WS_FEDERATION(INBOUND_PROTOCOL_WS_FEDERATION); + WS_FEDERATION(INBOUND_PROTOCOL_WS_FEDERATION), + ACTIONS(INBOUND_PROTOCOL_ACTIONS); private final String protocolName; @@ -81,6 +83,8 @@ public static InboundProtocol fromString(String protocolName) { return WS_TRUST; case INBOUND_PROTOCOL_WS_FEDERATION: return WS_FEDERATION; + case INBOUND_PROTOCOL_ACTIONS: + return ACTIONS; default: return null; } From ffca806aa42df72d08cd31643ffd0d63b163324d Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Fri, 27 Mar 2026 08:47:11 +0530 Subject: [PATCH 08/41] path type annotation validations and encryption --- .../management/api/constant/ErrorMessage.java | 2 + .../api/service/ActionManagementService.java | 12 + .../impl/ActionManagementServiceImpl.java | 39 ++ .../CacheBackedActionManagementService.java | 7 + .../executor/InFlowExtensionExecutor.java | 27 +- .../InFlowExtensionRequestBuilder.java | 234 ++----- .../InFlowExtensionResponseProcessor.java | 386 ++--------- .../extension/executor/JWEEncryptionUtil.java | 142 ++--- .../executor/PathTypeAnnotationUtil.java | 393 ++++++++++++ .../AccessConfigFlowUpdateInterceptor.java | 77 +-- .../InFlowExtensionActionConstants.java | 6 +- .../InFlowExtensionActionConverter.java | 41 +- ...InFlowExtensionActionDTOModelResolver.java | 184 ++---- .../inflow/extension/model/AccessConfig.java | 128 ++-- .../extension/model/AllowedOperation.java | 87 --- .../{ExposePath.java => ContextPath.java} | 14 +- .../inflow/extension/model/OperationPath.java | 70 -- .../FlowExecutionEngineServiceComponent.java | 3 - .../InFlowExtensionRequestBuilderTest.java | 172 ++--- .../InFlowExtensionResponseProcessorTest.java | 385 +++++------ .../executor/PathTypeAnnotationUtilTest.java | 599 ++++++++++++++++++ .../engine/model/AccessConfigTest.java | 204 ++++++ .../model/InFlowExtensionEventTest.java | 126 ++++ .../model/OperationExecutionResultTest.java | 79 +++ .../src/test/resources/testng.xml | 8 + .../IdentityKeyStoreResolverConstants.java | 6 +- 26 files changed, 2069 insertions(+), 1362 deletions(-) create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtil.java delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AllowedOperation.java rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/{ExposePath.java => ContextPath.java} (75%) delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationPath.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/PathTypeAnnotationUtilTest.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/AccessConfigTest.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/OperationExecutionResultTest.java diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/constant/ErrorMessage.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/constant/ErrorMessage.java index 30d998be18d2..ef1ceaf5b84d 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/constant/ErrorMessage.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/constant/ErrorMessage.java @@ -49,6 +49,8 @@ public enum ErrorMessage { "Each attribute must be a non-empty string."), ERROR_UNSUPPORTED_ATTRIBUTE("60014", "Unsupported attribute provided.", "The attribute %s is not supported to be shared with the extension."), + ERROR_ACTION_NAME_ALREADY_EXISTS("60015", "Action name already exists.", + "An action with the name '%s' already exists for the given action type."), // Server errors. ERROR_WHILE_ADDING_ACTION("65001", "Error while adding Action.", diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java index ac233ffdb0fc..cddc2d5548ed 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java @@ -118,6 +118,7 @@ Action updateAction(String actionType, String actionId, Action action, String te /** * Update the authentication of the action endpoint. + * * @param actionType Action Type. * @param actionId Action ID. @@ -128,4 +129,15 @@ Action updateAction(String actionType, String actionId, Action action, String te */ Action updateActionEndpointAuthentication(String actionType, String actionId, Authentication authentication, String tenantDomain) throws ActionMgtException; + + /** + * Check whether the given action name is available (unique) within the specified action type. + * + * @param actionType Action Type path parameter. + * @param name Action name to check. + * @param tenantDomain Tenant domain. + * @return {@code true} if the name is available, {@code false} otherwise. + * @throws ActionMgtException If an error occurs while checking name availability. + */ + boolean isActionNameAvailable(String actionType, String name, String tenantDomain) throws ActionMgtException; } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java index 619505c374d4..974e18b02b45 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java @@ -78,6 +78,8 @@ public Action addAction(String actionType, Action action, String tenantDomain) t castedActionType, ActionManagementConfig.getInstance().getLatestVersion(castedActionType), action); // Check whether the maximum allowed actions per type is reached. validateMaxActionsPerType(resolvedActionType, tenantDomain); + // Check whether the action name is unique within the action type. + validateActionNameUniqueness(resolvedActionType, action.getName(), null, tenantId); String generatedActionId = UUID.randomUUID().toString(); ActionDTO creatingActionDTO = buildActionDTOForCreation(resolvedActionType, generatedActionId, action); @@ -161,6 +163,9 @@ public Action updateAction(String actionType, String actionId, Action action, St Action.ActionTypes castedActionType = Action.ActionTypes.valueOf(resolvedActionType); ActionValidatorFactory.getActionValidator(castedActionType).doPreUpdateActionValidations( castedActionType, resolveActionVersionAtUpdating(action, existingActionDTO), action); + if (action.getName() != null) { + validateActionNameUniqueness(resolvedActionType, action.getName(), actionId, tenantId); + } ActionDTO updatingActionDTO = buildActionDTOForUpdate(resolvedActionType, actionId, action); DAO_FACADE.updateAction(updatingActionDTO, existingActionDTO, tenantId); @@ -171,6 +176,17 @@ public Action updateAction(String actionType, String actionId, Action action, St return buildAction(resolvedActionType, updatedActionDTO); } + @Override + public boolean isActionNameAvailable(String actionType, String name, String tenantDomain) + throws ActionMgtException { + + String resolvedActionType = getActionTypeFromPath(actionType); + int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); + List actionDTOS = DAO_FACADE.getActionsByActionType(resolvedActionType, tenantId); + return actionDTOS.stream() + .noneMatch(dto -> name.equalsIgnoreCase(dto.getName())); + } + private String resolveActionVersionAtUpdating(Action updatingAction, ActionDTO existingActionDTO) { String updatingActionVersion = updatingAction.getActionVersion(); @@ -353,6 +369,29 @@ private ActionDTO checkIfActionExists(String actionType, String actionId, String return actionDTO; } + /** + * Validate that the action name is unique within the given action type. + * + * @param actionType Action type. + * @param name Action name to validate. + * @param excludeId Action ID to exclude (for update). Null for creation. + * @param tenantId Tenant ID. + * @throws ActionMgtException If a duplicate name is found. + */ + private void validateActionNameUniqueness(String actionType, String name, String excludeId, int tenantId) + throws ActionMgtException { + + List existingActions = DAO_FACADE.getActionsByActionType(actionType, tenantId); + boolean duplicateExists = existingActions.stream() + .filter(dto -> excludeId == null || !excludeId.equals(dto.getId())) + .anyMatch(dto -> name.equalsIgnoreCase(dto.getName())); + + if (duplicateExists) { + throw ActionManagementExceptionHandler.handleClientException( + ErrorMessage.ERROR_ACTION_NAME_ALREADY_EXISTS, name); + } + } + /** * For action creation operation, builds an `ActionDTO` object based on the provided action type, action ID, and * action model. This method resolves the action type and status, applies necessary transformations, and constructs diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java index 5e2e0dd0f3b6..6ab187ce2c6c 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java @@ -182,6 +182,13 @@ public Action updateActionEndpointAuthentication(String actionType, String actio return updatedAction; } + @Override + public boolean isActionNameAvailable(String actionType, String name, String tenantDomain) + throws ActionMgtException { + + return ACTION_MGT_SERVICE.isActionNameAvailable(actionType, name, tenantDomain); + } + private void updateCache(Action action, ActionCacheEntry entry, ActionTypeCacheKey cacheKey, String tenantDomain) { if (LOG.isDebugEnabled()) { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java index 39a4d520bc1e..5f4fd60307bd 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java @@ -18,8 +18,6 @@ package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionException; @@ -59,9 +57,9 @@ *
  • Extract executor metadata: {@code actionId}.
  • *
  • Resolve access config from the action (with per-flow-type override support via * {@link ActionManagementService}). Falls back to system defaults if unavailable.
  • - *
  • Build a minimal {@link FlowContext} containing only three entries: - * the full {@link FlowExecutionContext}, the expose list, and the allowed operations - * JSON. The request builder will use these to construct the filtered request.
  • + *
  • Build a minimal {@link FlowContext} containing the full {@link FlowExecutionContext}, + * the expose list, the access config, and the encryption config. + * The request builder will use these to construct the filtered request.
  • *
  • Invoke the external service via {@link ActionExecutorService}.
  • *
  • Map the {@link ActionExecutionStatus} to an {@link ExecutorResponse}. * Context updates are already applied directly to the {@link FlowExecutionContext} @@ -72,11 +70,9 @@ public class InFlowExtensionExecutor implements Executor { private static final Log LOG = LogFactory.getLog(InFlowExtensionExecutor.class); private static final String EXECUTOR_NAME = "ExtensionExecutor"; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); public static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; public static final String EXPOSE_KEY = "expose"; - public static final String ALLOWED_OPERATIONS_KEY = "allowedOperations"; public static final String PATH_TYPE_ANNOTATIONS_KEY = "pathTypeAnnotations"; public static final String ACCESS_CONFIG_KEY = "accessConfig"; public static final String ENCRYPTION_KEY = "encryption"; @@ -109,7 +105,6 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE Encryption encryption = resolveEncryptionFromAction(actionId, context); List expose; - String allowedOpsJson = null; if (resolvedConfig != null && resolvedConfig.getExpose() != null) { expose = resolvedConfig.getExposePaths(); @@ -118,24 +113,12 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE expose = new ArrayList<>(HierarchicalPrefixMatcher.DEFAULT_EXPOSE); } - if (resolvedConfig != null && resolvedConfig.getAllowedOperations() != null) { - try { - allowedOpsJson = OBJECT_MAPPER.writeValueAsString(resolvedConfig.getAllowedOperations()); - } catch (JsonProcessingException e) { - LOG.error("Failed to serialize resolved allowed operations.", e); - } - } - FlowContext flowContext = FlowContext.create() .add(FLOW_EXECUTION_CONTEXT_KEY, context) .add(EXPOSE_KEY, expose); - if (allowedOpsJson != null) { - flowContext.add(ALLOWED_OPERATIONS_KEY, allowedOpsJson); - } - - // Pass the full AccessConfig so request builder and response processor can access - // per-path encryption flags for JWE encryption/decryption. + // Pass the full AccessConfig so request builder can derive allowed operations + // from modify paths, and response processor can check encryption flags. if (resolvedConfig != null) { flowContext.add(ACCESS_CONFIG_KEY, resolvedConfig); } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java index 71f6a648c222..918d3e633db8 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java @@ -18,9 +18,6 @@ package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionRequestBuilderException; @@ -39,6 +36,7 @@ import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; 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.mgt.model.NodeConfig; @@ -47,10 +45,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * This class is responsible for building the {@link ActionExecutionRequest} for In-Flow Extension @@ -58,24 +53,13 @@ * *

    Responsibility: expose-based filtering and request construction. * It receives a {@link FlowContext} containing the full {@link FlowExecutionContext}, the expose - * list, and the allowed-operations JSON. It filters the {@link FlowExecutionContext} data according - * to the expose configuration and maps the result into the {@link InFlowExtensionEvent} model.

    + * list, and the access config. It filters the {@link FlowExecutionContext} data according + * to the expose configuration and maps the result into the {@link InFlowExtensionEvent} model. + * Modify paths from the access config are converted to a single REPLACE {@link AllowedOperation}.

    */ public class InFlowExtensionRequestBuilder implements ActionExecutionRequestBuilder { private static final Log LOG = LogFactory.getLog(InFlowExtensionRequestBuilder.class); - private static final ObjectMapper objectMapper = new ObjectMapper(); - private static final TypeReference>> OPERATION_LIST_TYPE_REF = - new TypeReference>>() { }; - - /** - * Regex pattern to match path type annotations. - * Matches a trailing bracket expression at the end of a path: - * - "[]" — denotes a string array value. - * - "[field1, field2, field3[]]" — denotes a complex object array with a schema. - * Group 1 captures the content inside the brackets (empty for simple arrays). - */ - private static final Pattern PATH_ANNOTATION_PATTERN = Pattern.compile("\\[([^\\]]*)]$"); @Override public ActionType getSupportedActionType() { @@ -109,19 +93,13 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex Encryption encryption = flowContext.getValue( InFlowExtensionExecutor.ENCRYPTION_KEY, Encryption.class); - // Build allowed operations first so REPLACE paths can augment the expose list. - List allowedOperations = buildAllowedOperations( - flowContext.getValue(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, String.class), - flowContext, accessConfig); + // Build allowed operations from modify paths. + List allowedOperations = buildAllowedOperationsFromModify( + accessConfig, flowContext); - // Augment expose with REPLACE and REMOVE paths so the external service can see - // the current values it may replace or remove. - expose = augmentExposeWithOperationPaths(expose, allowedOperations); - - // Determine certificate PEM for outbound encryption (if any paths are encrypted). + // Determine certificate PEM for outbound encryption. String certificatePEM = null; - if (accessConfig != null && accessConfig.hasAnyEncryptedPath() - && encryption != null && encryption.getCertificate() != null) { + if (accessConfig != null && encryption != null && encryption.getCertificate() != null) { certificatePEM = encryption.getCertificate().getCertificateContent(); } @@ -136,115 +114,63 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex } /** - * Parse the allowed-operations JSON into typed {@link AllowedOperation} objects. - * Handles the nested format where each entry is {@code {op, paths: [{path, encrypted}]}}. - * Groups entries by operation type and strips path type annotations. - * The original annotations are stored in the FlowContext under - * {@link InFlowExtensionExecutor#PATH_TYPE_ANNOTATIONS_KEY} for the response processor. - *

    - * Encryption metadata is NOT extracted from the JSON. Instead, the {@link AccessConfig} - * (already in FlowContext) is the single source of truth for per-path encryption flags. - * The response processor uses {@code AccessConfig.isOperationPathEncrypted()} directly. + * Build allowed operations from the modify paths in {@link AccessConfig}. + * All modify paths are mapped to a single REPLACE {@link AllowedOperation}. + * Path type annotations (e.g. {@code []} or {@code [schema]}) are stripped and stored + * in the {@link FlowContext} under {@link InFlowExtensionExecutor#PATH_TYPE_ANNOTATIONS_KEY} + * for the response processor. * - * @param allowedOperationsJson The JSON string (maybe null). - * @param flowContext The FlowContext to store path annotations. - * @param accessConfig The access config (may be null). Reserved for future use. - * @return List of AllowedOperation objects with clean (annotation-free) paths. + * @param accessConfig The access config containing modify paths (may be null). + * @param flowContext The FlowContext to store path annotations. + * @return A singleton list containing one REPLACE operation, or empty list if no modify paths. */ - private List buildAllowedOperations(String allowedOperationsJson, - FlowContext flowContext, - AccessConfig accessConfig) { + private List buildAllowedOperationsFromModify(AccessConfig accessConfig, + FlowContext flowContext) { - if (allowedOperationsJson == null || allowedOperationsJson.isEmpty()) { - LOG.debug("No allowed operations configured. Using empty list."); + if (accessConfig == null || accessConfig.getModify() == null || accessConfig.getModify().isEmpty()) { return Collections.emptyList(); } - try { - List> operationConfigs = objectMapper.readValue( - allowedOperationsJson, OPERATION_LIST_TYPE_REF); - - // Map to store path type annotations: cleanPath -> annotation content. - Map pathTypeAnnotations = new HashMap<>(); + List cleanPaths = new ArrayList<>(); + Map pathTypeAnnotations = new HashMap<>(); - // Group entries by operation type, handling nested paths [{path, encrypted}]. - Map> groupedByOp = new HashMap<>(); - for (Map config : operationConfigs) { - String opString = (String) config.get("op"); - Object pathsObj = config.get("paths"); + for (ContextPath modifyPath : accessConfig.getModify()) { + String rawPath = modifyPath.getPath(); + if (rawPath == null) { + continue; + } - if (opString == null) { - LOG.warn("Invalid allowed operation config: missing 'op'."); - continue; - } - if (!(pathsObj instanceof List)) { - LOG.warn("Invalid allowed operation config: missing or invalid 'paths'."); + String[] stripped = PathTypeAnnotationUtil.stripAnnotation(rawPath); + String cleanPath = stripped[0]; + String annotation = stripped[1]; + if (annotation != null) { + if (!PathTypeAnnotationUtil.validateAnnotationLimits(annotation)) { + LOG.warn("Annotation for path " + cleanPath + + " exceeds maximum attribute limit. Skipping path."); continue; } - - List pathsList = (List) pathsObj; - for (Object pathEntry : pathsList) { - String rawPath; - - if (pathEntry instanceof Map) { - // Nested format: {path, encrypted} — only extract path here. - // Encryption metadata is resolved from AccessConfig at runtime. - Map pathMap = (Map) pathEntry; - rawPath = (String) pathMap.get("path"); - } else if (pathEntry instanceof String) { - rawPath = (String) pathEntry; - } else { - LOG.warn("Invalid path entry in allowed operation."); - continue; - } - - if (rawPath == null) { - continue; - } - - // Strip annotations and accumulate paths into groups. - Matcher matcher = PATH_ANNOTATION_PATTERN.matcher(rawPath); - String cleanPath; - if (matcher.find()) { - cleanPath = rawPath.substring(0, matcher.start()); - pathTypeAnnotations.put(cleanPath, matcher.group(1)); - } else { - cleanPath = rawPath; - } - - groupedByOp.computeIfAbsent(opString.toUpperCase(Locale.ENGLISH), - k -> new ArrayList<>()).add(cleanPath); - } + pathTypeAnnotations.put(cleanPath, annotation); } + cleanPaths.add(cleanPath); + } - // Build AllowedOperation objects from grouped entries. - List allowedOperations = new ArrayList<>(); - for (Map.Entry> entry : groupedByOp.entrySet()) { - try { - Operation operation = Operation.valueOf(entry.getKey()); - AllowedOperation allowedOperation = new AllowedOperation(); - allowedOperation.setOp(operation); - allowedOperation.setPaths(entry.getValue()); - allowedOperations.add(allowedOperation); - } catch (IllegalArgumentException e) { - LOG.warn("Unknown operation type: " + entry.getKey()); - } - } + if (cleanPaths.isEmpty()) { + return Collections.emptyList(); + } - // Store annotations in FlowContext for the response processor. - if (!pathTypeAnnotations.isEmpty()) { - flowContext.add(InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); - } + AllowedOperation replaceOp = new AllowedOperation(); + replaceOp.setOp(Operation.REPLACE); + replaceOp.setPaths(cleanPaths); - if (LOG.isDebugEnabled()) { - LOG.debug("Built " + allowedOperations.size() + " allowed operations. " + - "Path annotations: " + pathTypeAnnotations.size()); - } - return allowedOperations; - } catch (JsonProcessingException e) { - LOG.error("Failed to parse allowed operations: " + e.getMessage(), e); - return Collections.emptyList(); + if (!pathTypeAnnotations.isEmpty()) { + flowContext.add(InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Built REPLACE allowed operation with " + cleanPaths.size() + + " modify paths. Path annotations: " + pathTypeAnnotations.size()); } + return Collections.singletonList(replaceOp); } /** @@ -448,56 +374,11 @@ private boolean hasSpecificSubPathFilter(List expose, String areaPrefix) return false; } - /** - * Augment the expose list with paths from REPLACE and REMOVE operations. - * REPLACE operations require the current values to be visible to the external service - * so it can decide what replacement to send. REMOVE operations require current values - * to be visible so the external service knows what data would be removed. - * - * @param expose The current expose list. - * @param allowedOperations The parsed allowed operations. - * @return The augmented expose list (new list if modified, original if unchanged). - */ - private List augmentExposeWithOperationPaths(List expose, - List allowedOperations) { - - if (allowedOperations == null || allowedOperations.isEmpty()) { - return expose; - } - - List additionalPaths = new ArrayList<>(); - for (AllowedOperation op : allowedOperations) { - if ((op.getOp() == Operation.REPLACE || op.getOp() == Operation.REMOVE) - && op.getPaths() != null) { - for (String path : op.getPaths()) { - if (!isExposed(path, expose)) { - additionalPaths.add(path); - } - } - } - } - - if (additionalPaths.isEmpty()) { - return expose; - } - - List augmented = new ArrayList<>(expose); - augmented.addAll(additionalPaths); - if (LOG.isDebugEnabled()) { - LOG.debug("Augmented expose list with REPLACE/REMOVE paths: " + additionalPaths); - } - return augmented; - } - // ---- Encryption helpers ---- /** - * Determine if a value at the given path should be JWE-encrypted before sending to the external service. - *

    - * Checks both the explicit expose config and the operation paths (REPLACE / REMOVE) that may - * have been dynamically augmented into the expose list. REPLACE and REMOVE operation paths - * carry their own encryption flags in the allowedOperations config — when such a path is - * augmented into expose for visibility, its encryption flag must be honoured. + * Determine if a value at the given path should be JWE-encrypted before sending to the + * external service. Only expose paths with {@code encrypted: true} trigger outbound encryption. * * @param path The expose path. * @param accessConfig The access config with encryption flags. @@ -509,14 +390,7 @@ private boolean shouldEncrypt(String path, AccessConfig accessConfig, String cer if (certificatePEM == null || accessConfig == null) { return false; } - // Check explicit expose paths first. - if (accessConfig.isExposePathEncrypted(path)) { - return true; - } - // Also check REPLACE and REMOVE operation paths that were augmented into expose. - // These paths may have their own encryption flags in the allowedOperations config. - return accessConfig.isOperationPathEncrypted("REPLACE", path) - || accessConfig.isOperationPathEncrypted("REMOVE", path); + return accessConfig.isExposePathEncrypted(path); } /** diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java index 02650d7fa231..707334fb1c9b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java @@ -36,7 +36,6 @@ import org.wso2.carbon.identity.action.execution.api.model.FailedStatus; import org.wso2.carbon.identity.action.execution.api.model.Failure; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; -import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.action.execution.api.model.SuccessStatus; @@ -56,7 +55,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; @@ -64,21 +62,19 @@ * This class is responsible for processing the response from In-Flow Extension actions. * *

    Responsibility: operation processing and applying context updates directly to - * the {@link FlowExecutionContext}. It processes operations (ADD, REMOVE, REPLACE) on flow + * the {@link FlowExecutionContext}. It processes REPLACE operations on flow * properties, user claims, and user inputs.

    * - *

    The {@code allowedOperations} list (sent to the external service in the request and enforced - * upstream by {@code ActionExecutorServiceImpl}) is the sole mechanism for gating which operations - * are permitted. This processor performs two additional validations:

    + *

    Only {@code REPLACE} operations are supported. The {@code allowedOperations} list + * (derived from modify paths and sent to the external service in the request, enforced + * upstream by {@code ActionExecutorServiceImpl}) is the sole mechanism for gating which + * operations are permitted. This processor performs additional validations:

    *
      *
    • Read-only areas: No modifications allowed to {@code /flow/} or {@code /graph/}.
    • - *
    • Claim URI validation: For ADD operations on {@code /user/claims/}, validates that - * the claim URI exists in the system via {@code ClaimMetadataManagementService}.
    • + *
    • Claim URI validation: For {@code /user/claims/} operations, validates that + * the claim URI exists in the local claim dialect and is not an identity claim.
    • *
    */ - -// TODO: Consider separating claim validation and read-only path checks into utility classes. - public class InFlowExtensionResponseProcessor implements ActionExecutionResponseProcessor { private static final Log LOG = LogFactory.getLog(InFlowExtensionResponseProcessor.class); @@ -90,9 +86,6 @@ public class InFlowExtensionResponseProcessor implements ActionExecutionResponse // Legacy prefix for backward compatibility private static final String LEGACY_USER_INPUTS_PATH_PREFIX = "/userInputs/"; - private static final char PATH_SEPARATOR = '/'; - private static final String LAST_ELEMENT_CHARACTER = "-"; - // Cache for valid claim URIs (per tenant) private Map> validClaimUrisCache = new HashMap<>(); @@ -113,8 +106,7 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon String tenantDomain = execCtx != null ? execCtx.getTenantDomain() : null; // Read path type annotations set by the request builder. - // Maps clean paths (e.g., "/properties/riskFactors") to annotation content - // ("" for string arrays, or schema content for complex object arrays). + // Maps clean paths to annotation content Map pathTypeAnnotations = flowContext.getValue( InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); if (pathTypeAnnotations == null) { @@ -122,13 +114,12 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon } // Read access config for encryption metadata. - // Uses AccessConfig.isOperationPathEncrypted() for canonical encryption checking. + // Uses AccessConfig.isModifyPathEncrypted() for canonical encryption checking. AccessConfig accessConfig = flowContext.getValue( InFlowExtensionExecutor.ACCESS_CONFIG_KEY, AccessConfig.class); List results = new ArrayList<>(); - // Get operations from the response (already filtered by ActionExecutorServiceImpl). List operations = responseContext.getActionInvocationResponse().getOperations(); @@ -198,269 +189,66 @@ private OperationExecutionResult processOperation(PerformableOperation operation ", " + USER_CLAIMS_PATH_PREFIX + ", " + USER_INPUTS_PATH_PREFIX); } - // ========================= Property operations ========================= - /** - * Handle operations on flow properties — apply directly to {@link FlowExecutionContext}. - * - *

    Supports terminal paths with nested segments. For example:

    - *
      - *
    • {@code /properties/riskScore} → flat property "riskScore"
    • - *
    • {@code /properties/risk_margin/lower_margin} → nested: sets "lower_margin" inside - * a "risk_margin" Map, auto-creating the parent Map if needed.
    • - *
    - * - *

    Value type rules (based on path type annotations from allowed operations):

    - *
      - *
    • No annotation: value must be a string (or convertible via String.valueOf).
    • - *
    • {@code []} annotation: value must be a List of strings.
    • - *
    • {@code [schema]} annotation: value must be a List of objects matching the schema.
    • - *
    + * Handle operation on flow properties — apply directly to {@link FlowExecutionContext}. * - *

    REPLACE validation: the target path must already exist in the context.

    + *

    Only flat property paths are supported (e.g., {@code /properties/riskScore}). + * The property is created if it does not already exist. Value coercion is applied + * based on path type annotations from the request builder via + * {@link PathTypeAnnotationUtil#coerceValue}.

    * * @param operation The performable operation. * @param context The FlowExecutionContext. * @param pathTypeAnnotations Path type annotations map from request builder. */ - @SuppressWarnings("unchecked") private OperationExecutionResult handlePropertyOperation(PerformableOperation operation, FlowExecutionContext context, Map pathTypeAnnotations) { - String remaining = extractNameFromPath(operation.getPath(), PROPERTIES_PATH_PREFIX); + String propertyName = extractNameFromPath(operation.getPath(), PROPERTIES_PATH_PREFIX); - if (remaining == null || remaining.isEmpty()) { + if (propertyName == null || propertyName.isEmpty()) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Invalid property path. Property name is required."); } - // Split into path segments for nested property support. - // e.g., "risk_margin/lower_margin" -> ["risk_margin", "lower_margin"] - String[] segments = remaining.split("/"); - - switch (operation.getOp()) { - case ADD: - return handlePropertyAdd(operation, context, segments, pathTypeAnnotations); - - case REPLACE: - return handlePropertyReplace(operation, context, segments, pathTypeAnnotations); - - case REMOVE: - return handlePropertyRemove(operation, context, segments); - - default: - return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Unsupported operation: " + operation.getOp()); - } - } - - /** - * Handle ADD operation on properties. Auto-creates parent Maps for nested paths. - */ - @SuppressWarnings("unchecked") - private OperationExecutionResult handlePropertyAdd(PerformableOperation operation, - FlowExecutionContext context, String[] segments, - Map pathTypeAnnotations) { - - if (operation.getValue() == null) { - return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Value is required for ADD operation."); - } - - // Coerce value based on path type annotation. - Object coercedValue = coerceValue(operation.getPath(), operation.getValue(), pathTypeAnnotations); - - if (segments.length == 1) { - // Flat property: /properties/riskScore -> setProperty("riskScore", value) - context.setProperty(segments[0], coercedValue); - } else { - // Nested property: /properties/risk_margin/lower_margin - // Auto-create parent Map(s) and set the leaf value. - setNestedProperty(context, segments, coercedValue); - } - - return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, - "Property add applied."); - } - - /** - * Handle REPLACE operation on properties. Validates that the target path exists. - */ - @SuppressWarnings("unchecked") - private OperationExecutionResult handlePropertyReplace(PerformableOperation operation, - FlowExecutionContext context, String[] segments, - Map pathTypeAnnotations) { - if (operation.getValue() == null) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Value is required for REPLACE operation."); } - // Validate that the target path exists before replacing. - if (segments.length == 1) { - if (!context.getProperties().containsKey(segments[0])) { - return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Cannot REPLACE: property '" + segments[0] + "' does not exist in context."); - } - } else { - // For nested paths, validate the full path exists. - Object existing = resolveNestedProperty(context, segments); - if (existing == null) { - return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Cannot REPLACE: nested property path '" + String.join("/", segments) + - "' does not exist in context."); - } + // Validate complex object structure against annotation schema before coercion. + if (!PathTypeAnnotationUtil.validateValueAgainstAnnotation( + operation.getPath(), operation.getValue(), pathTypeAnnotations)) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Value does not match annotation schema for path: " + operation.getPath()); } // Coerce value based on path type annotation. - Object coercedValue = coerceValue(operation.getPath(), operation.getValue(), pathTypeAnnotations); + Object coercedValue = PathTypeAnnotationUtil.coerceValue( + operation.getPath(), operation.getValue(), pathTypeAnnotations); - if (segments.length == 1) { - context.setProperty(segments[0], coercedValue); - } else { - setNestedProperty(context, segments, coercedValue); + // Enforce array item limits after coercion. + if (!PathTypeAnnotationUtil.enforceArrayItemLimit( + operation.getPath(), coercedValue, pathTypeAnnotations)) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Array value exceeds maximum item limit for path: " + operation.getPath()); } - return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, - "Property replace applied."); - } - - /** - * Handle REMOVE operation on properties. - */ - @SuppressWarnings("unchecked") - private OperationExecutionResult handlePropertyRemove(PerformableOperation operation, - FlowExecutionContext context, String[] segments) { - - if (segments.length == 1) { - context.getProperties().remove(segments[0]); - } else { - // For nested paths, remove the leaf key from the parent Map. - removeNestedProperty(context, segments); - } + context.setProperty(propertyName, coercedValue); return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, - "Property removed."); - } - - /** - * Set a value at a nested path in the properties map, auto-creating parent Maps. - * e.g., segments=["risk_margin", "lower_margin"], value="20" - * Creates: properties.risk_margin = { lower_margin: "20" } - */ - @SuppressWarnings("unchecked") - private void setNestedProperty(FlowExecutionContext context, String[] segments, Object value) { - - Map current = context.getProperties(); - - // Navigate/create parent maps for all segments except the last. - for (int i = 0; i < segments.length - 1; i++) { - Object child = current.get(segments[i]); - if (child instanceof Map) { - current = (Map) child; - } else { - // Auto-create parent Map. - Map newMap = new HashMap<>(); - current.put(segments[i], newMap); - current = newMap; - } - } - - // Set the leaf value. - current.put(segments[segments.length - 1], value); - } - - /** - * Resolve a nested property path, returning the leaf value or null if any segment is missing. - */ - @SuppressWarnings("unchecked") - private Object resolveNestedProperty(FlowExecutionContext context, String[] segments) { - - Map current = context.getProperties(); - - for (int i = 0; i < segments.length - 1; i++) { - Object child = current.get(segments[i]); - if (child instanceof Map) { - current = (Map) child; - } else { - return null; - } - } - - return current.get(segments[segments.length - 1]); - } - - /** - * Remove a leaf key from a nested property path. - */ - @SuppressWarnings("unchecked") - private void removeNestedProperty(FlowExecutionContext context, String[] segments) { - - Map current = context.getProperties(); - - for (int i = 0; i < segments.length - 1; i++) { - Object child = current.get(segments[i]); - if (child instanceof Map) { - current = (Map) child; - } else { - return; // Parent doesn't exist, nothing to remove. - } - } - - current.remove(segments[segments.length - 1]); + "Property replace applied."); } /** - * Coerce a value based on path type annotations. + * Handle REPLACE operation on user claims — validate and apply directly to {@link FlowUser}. * + *

    Validates that the claim URI:

    *
      - *
    • No annotation: value is coerced to String via String.valueOf().
    • - *
    • "" annotation (from []): value is expected to be a List; each element is coerced to String.
    • - *
    • Non-empty annotation (from [schema]): value is expected to be a List of objects; - * passed through as-is (schema validation can be added later).
    • + *
    • Exists in the local claim dialect (via {@link ClaimMetadataManagementService}).
    • + *
    • Is not an identity claim ({@code http://wso2.org/claims/identity/}).
    • *
    - * - * @param path The operation path. - * @param value The raw value from the operation. - * @param pathTypeAnnotations Path type annotations map. - * @return The coerced value. - */ - @SuppressWarnings("unchecked") - private Object coerceValue(String path, Object value, - Map pathTypeAnnotations) { - - String annotation = pathTypeAnnotations.get(path); - - if (annotation == null) { - // No annotation: coerce to String. - return String.valueOf(value); - } - - if (annotation.isEmpty()) { - // [] annotation: expect a List of strings. - if (value instanceof List) { - List rawList = (List) value; - List stringList = new ArrayList<>(); - for (Object item : rawList) { - stringList.add(String.valueOf(item)); - } - return stringList; - } - // Single value — wrap in a list. - List singleList = new ArrayList<>(); - singleList.add(String.valueOf(value)); - return singleList; - } - - // [schema] annotation: pass through as-is for now. - // TODO: Add schema validation for complex object arrays. - return value; - } - - // ========================= User claim operations ========================= - - /** - * Handle operations on user claims — validate and apply directly to {@link FlowUser}. + *

    The value is always stringified via {@code String.valueOf()}.

    */ private OperationExecutionResult handleUserClaimOperation(PerformableOperation operation, FlowExecutionContext context, String tenantDomain) { @@ -472,12 +260,16 @@ private OperationExecutionResult handleUserClaimOperation(PerformableOperation o "Invalid claim path. Claim URI is required."); } - // For ADD operations, validate that the claim URI exists in system configuration. - if (operation.getOp() == Operation.ADD) { - if (!isValidClaimUri(claimUri, tenantDomain)) { - return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Invalid claim URI. Claim must be configured in the system: " + claimUri); - } + // Reject identity claims — these are system-managed and not user-modifiable. + if (claimUri.startsWith(PathTypeAnnotationUtil.IDENTITY_CLAIM_URI_PREFIX)) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Identity claims cannot be modified via extensions: " + claimUri); + } + + // Validate claim exists in local claim dialect. + if (!isValidClaimUri(claimUri, tenantDomain)) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Invalid claim URI. Claim must be configured in the local claim dialect: " + claimUri); } FlowUser user = context.getFlowUser(); @@ -486,32 +278,19 @@ private OperationExecutionResult handleUserClaimOperation(PerformableOperation o "No FlowUser in FlowExecutionContext. Cannot apply user claim operation."); } - switch (operation.getOp()) { - case ADD: - case REPLACE: - if (operation.getValue() == null) { - return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Value is required for " + operation.getOp() + " operation."); - } - user.addClaim(claimUri, String.valueOf(operation.getValue())); - return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, - "User claim " + operation.getOp().name().toLowerCase(Locale.ENGLISH) + " applied."); - - case REMOVE: - if (user.getClaims() != null) { - user.getClaims().remove(claimUri); - } - return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, - "User claim removed."); - - default: - return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Unsupported operation: " + operation.getOp()); + if (operation.getValue() == null) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Value is required for REPLACE operation."); } + + user.addClaim(claimUri, String.valueOf(operation.getValue())); + return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, + "User claim replace applied."); } /** - * Handle operations on user inputs — apply directly to {@link FlowExecutionContext}. + * Handle REPLACE operation on user inputs — apply directly to {@link FlowExecutionContext}. + * The value is always stringified via {@code String.valueOf()}. */ private OperationExecutionResult handleUserInputOperation(PerformableOperation operation, FlowExecutionContext context) { @@ -523,30 +302,19 @@ private OperationExecutionResult handleUserInputOperation(PerformableOperation o "Invalid user input path. Input name is required."); } - switch (operation.getOp()) { - case ADD: - case REPLACE: - if (operation.getValue() == null) { - return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Value is required for " + operation.getOp() + " operation."); - } - context.addUserInputData(inputName, String.valueOf(operation.getValue())); - return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, - "User input " + operation.getOp().name().toLowerCase(Locale.ENGLISH) + " applied."); - - case REMOVE: - context.getUserInputData().remove(inputName); - return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, - "User input removed."); - - default: - return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Unsupported operation: " + operation.getOp()); + if (operation.getValue() == null) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Value is required for REPLACE operation."); } + + context.addUserInputData(inputName, String.valueOf(operation.getValue())); + return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, + "User input replace applied."); } + //TODO: These validations can be removed once the attribute executor introduced. /** - * Validate if a claim URI exists in system configuration. + * Validate if a claim URI exists in the local claim dialect. */ private boolean isValidClaimUri(String claimUri, String tenantDomain) { @@ -610,28 +378,9 @@ private String extractNameFromPath(String path, String prefix) { remaining = remaining.substring(0, remaining.length() - 1); } - // Handle array index notation (e.g., /properties/items/0). - int lastSlash = remaining.lastIndexOf(PATH_SEPARATOR); - if (lastSlash > 0) { - String possibleIndex = remaining.substring(lastSlash + 1); - if (LAST_ELEMENT_CHARACTER.equals(possibleIndex) || isNumeric(possibleIndex)) { - return remaining.substring(0, lastSlash); - } - } - return remaining; } - private boolean isNumeric(String str) { - - try { - Integer.parseInt(str); - return true; - } catch (NumberFormatException e) { - return false; - } - } - @Override public ActionExecutionStatus processErrorResponse(FlowContext flowContext, ActionExecutionResponseContext responseContext) @@ -721,7 +470,7 @@ public static class InFlowExtensionSuccess implements Success { /** * Decrypt the operation value if the operation path has encryption enabled in the AccessConfig. - * Uses {@link AccessConfig#isOperationPathEncrypted(String, String)} for canonical checking. + * Uses {@link AccessConfig#isModifyPathEncrypted(String)} for canonical checking. * For operations with encrypted paths, checks if the value looks like a JWE compact * serialization and decrypts it using the IS's private key. * @@ -737,16 +486,13 @@ private PerformableOperation decryptOperationValueIfNeeded(PerformableOperation return operation; } - // Check if this operation path has encryption enabled via AccessConfig. - if (!accessConfig.isOperationPathEncrypted(operation.getOp().name(), operation.getPath())) { + // Check if this operation path has encryption enabled via modify paths in AccessConfig. + if (!accessConfig.isModifyPathEncrypted(operation.getPath())) { return operation; } // Only decrypt string values that look like JWE compact serialization. Object value = operation.getValue(); - if (!(value instanceof String)) { - return operation; - } String stringValue = (String) value; if (!JWEEncryptionUtil.isJWEEncrypted(stringValue)) { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java index a9ba41c0b470..bfe9813199e5 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java @@ -29,9 +29,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.base.MultitenantConstants; +import org.wso2.carbon.core.util.KeyStoreManager; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionException; -import org.wso2.carbon.identity.core.IdentityKeyStoreResolver; -import org.wso2.carbon.identity.core.util.IdentityKeyStoreResolverConstants; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; @@ -41,6 +42,8 @@ import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.text.ParseException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * Utility class for JWE encryption and decryption operations used by In-Flow Extension actions. @@ -50,9 +53,8 @@ * pattern as {@code PasswordUpdatingUser.encryptCredential()}.

    * *

    Inbound decryption (external service → IS): uses the IS server's RSA private key - * retrieved via {@link IdentityKeyStoreResolver} with - * {@link IdentityKeyStoreResolverConstants.InboundProtocol#ACTIONS}. This follows the same - * pattern as {@code OIDCSessionManagementUtil.decryptWithRSA()}.

    + * retrieved via {@link KeyStoreManager} for the tenant. This follows the same + * pattern as {@code UserAssertionUtils.getPrivateKey()}.

    */ public final class JWEEncryptionUtil { @@ -64,6 +66,9 @@ public final class JWEEncryptionUtil { /** Standard PEM line length per RFC 7468. */ private static final int PEM_LINE_LENGTH = 64; + /** Cache of resolved private keys, keyed by tenant ID. */ + private static final Map PRIVATE_KEYS = new ConcurrentHashMap<>(); + private JWEEncryptionUtil() { } @@ -99,8 +104,7 @@ public static String encrypt(String plaintext, String certificatePEM) throws Act /** * Decrypt a JWE compact string using the IS server's RSA private key. - * The private key is resolved via {@link IdentityKeyStoreResolver} using - * {@link IdentityKeyStoreResolverConstants.InboundProtocol#ACTIONS}. + * The private key is resolved via {@link KeyStoreManager} for the tenant. * * @param jweString The JWE compact serialization string. * @param tenantDomain The tenant domain to resolve the private key. @@ -112,9 +116,7 @@ public static String decrypt(String jweString, String tenantDomain) throws Actio try { JWEObject jweObject = JWEObject.parse(jweString); - Key privateKey = IdentityKeyStoreResolver.getInstance() - .getPrivateKey(tenantDomain, - IdentityKeyStoreResolverConstants.InboundProtocol.ACTIONS); + Key privateKey = getPrivateKey(tenantDomain); RSADecrypter decrypter = new RSADecrypter((RSAPrivateKey) privateKey); jweObject.decrypt(decrypter); @@ -126,12 +128,52 @@ public static String decrypt(String jweString, String tenantDomain) throws Actio } catch (JOSEException e) { throw new ActionExecutionException( "Failed to decrypt JWE value from In-Flow Extension response.", e); + } catch (ActionExecutionException e) { + throw e; } catch (Exception e) { throw new ActionExecutionException( "Error resolving IS private key for In-Flow Extension JWE decryption.", e); } } + /** + * Retrieve the IS server's RSA private key for the given tenant. + * Uses {@link KeyStoreManager} directly, bypassing IdentityKeyStoreResolver, + * to avoid requiring a protocol-specific keystore mapping. + * + * @param tenantDomain The tenant domain. + * @return The RSA private key. + * @throws ActionExecutionException If retrieval fails. + */ + private static Key getPrivateKey(String tenantDomain) throws ActionExecutionException { + + int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); + String cacheKey = String.valueOf(tenantId); + Key cachedKey = PRIVATE_KEYS.get(cacheKey); + if (cachedKey != null) { + return cachedKey; + } + + try { + IdentityTenantUtil.initializeRegistry(tenantId); + KeyStoreManager keyStoreManager = KeyStoreManager.getInstance(tenantId); + Key privateKey; + + if (MultitenantConstants.SUPER_TENANT_DOMAIN_NAME.equals(tenantDomain)) { + privateKey = keyStoreManager.getDefaultPrivateKey(); + } else { + String tenantKeyStoreName = tenantDomain.trim().replace(".", "-") + ".jks"; + privateKey = keyStoreManager.getPrivateKey(tenantKeyStoreName, tenantDomain); + } + + PRIVATE_KEYS.put(cacheKey, privateKey); + return privateKey; + } catch (Exception e) { + throw new ActionExecutionException( + "Error retrieving private key for tenant: " + tenantDomain, e); + } + } + /** * Detect whether a string value is a JWE compact serialization. * A JWE compact serialization has exactly 5 dot-separated Base64url-encoded parts. @@ -158,77 +200,31 @@ public static boolean isJWEEncrypted(String value) { } /** - * Parse a PEM-encoded X.509 certificate string into an {@link X509Certificate} object. - *

    - * Handles PEM strings that may have lost their newlines during database storage/retrieval - * (e.g., via {@code CertificateManagementDAOImpl.getStringValueFromBlob()} which strips - * line breaks). The method normalizes the PEM format before parsing to ensure the - * header/footer are on separate lines and Base64 content is properly structured. - *

    + * Parse a Base64-encoded PEM X.509 certificate string into an {@link X509Certificate} object. + * Expects the input to be a fully Base64-encoded string of a standard PEM file. * - * @param pem The PEM string (with or without proper line breaks). + * @param base64EncodedPem The Base64 encoded PEM string. * @return The parsed X509Certificate. - * @throws ActionExecutionException If parsing fails. + * @throws ActionExecutionException If parsing or decoding fails. */ - public static X509Certificate parsePEMCertificate(String pem) throws ActionExecutionException { + public static X509Certificate parsePEMCertificate(String base64EncodedPem) throws ActionExecutionException { - try { - String normalizedPEM = normalizePEM(pem); - CertificateFactory factory = CertificateFactory.getInstance("X.509"); - return (X509Certificate) factory.generateCertificate( - new ByteArrayInputStream(normalizedPEM.getBytes(StandardCharsets.UTF_8))); - } catch (ActionExecutionException e) { - throw e; - } catch (Exception e) { - throw new ActionExecutionException( - "Failed to parse PEM certificate for In-Flow Extension action.", e); - } - } - - /** - * Normalize a PEM string to ensure it has proper line structure. - *

    - * The CertificateManagementService's DAO layer reads BLOB data using - * {@code BufferedReader.readLine()} + {@code StringBuilder.append(line)}, which strips - * all newline characters. This produces a single-line string like: - * {@code -----BEGIN CERTIFICATE-----MIICxjCC...-----END CERTIFICATE-----} - * which Java's X509 parser cannot handle. - *

    - *

    - * This method extracts the Base64 body, strips all whitespace, and re-wraps it at - * 64-character lines between proper PEM header/footer markers. - *

    - * - * @param pem The PEM string (possibly with stripped newlines). - * @return A properly formatted PEM string. - * @throws ActionExecutionException If the PEM structure is invalid. - */ - private static String normalizePEM(String pem) throws ActionExecutionException { - - if (pem == null || pem.isEmpty()) { - throw new ActionExecutionException("Certificate PEM string is null or empty."); + if (base64EncodedPem == null || base64EncodedPem.trim().isEmpty()) { + throw new ActionExecutionException("Certificate string is null or empty."); } - String trimmed = pem.trim(); - - // Extract the Base64 body by stripping header/footer markers. - String base64Body = trimmed - .replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replaceAll("\\s+", ""); + try { + byte[] decodedPemBytes = java.util.Base64.getDecoder().decode(base64EncodedPem.trim()); - if (base64Body.isEmpty()) { - throw new ActionExecutionException("Certificate PEM contains no Base64 data."); - } + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(decodedPemBytes)); - // Reconstruct proper PEM with 64-char line wrapping (RFC 7468). - StringBuilder sb = new StringBuilder(); - sb.append("-----BEGIN CERTIFICATE-----\n"); - for (int i = 0; i < base64Body.length(); i += PEM_LINE_LENGTH) { - sb.append(base64Body, i, Math.min(i + PEM_LINE_LENGTH, base64Body.length())); - sb.append('\n'); + } catch (IllegalArgumentException e) { + throw new ActionExecutionException( + "Failed to decode certificate: Input is not valid Base64.", e); + } catch (Exception e) { + throw new ActionExecutionException( + "Failed to parse the decoded PEM certificate for In-Flow Extension action.", e); } - sb.append("-----END CERTIFICATE-----\n"); - return sb.toString(); } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtil.java new file mode 100644 index 000000000000..6e63a3108bee --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtil.java @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2025, 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.inflow.extension.executor; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class for path type annotation parsing, stripping, and value coercion. + * + *

    Path type annotations use a trailing brace expression at the end of a modify path + * to declare the expected data type for that path. The unified format uses curly braces:

    + *
      + *
    • {@code /properties/risk-factor{String}} — primary data type.
    • + *
    • {@code /properties/risk-factors{[String]}} — multivalued primary (array of type).
    • + *
    • {@code /properties/risk{risk: Float, factor: String}} — complex object with schema.
    • + *
    • {@code /properties/risk{[risk: Float, factor: String]}} — multivalued complex object array.
    • + *
    + * + *

    This class provides methods to strip annotations from paths and coerce incoming values + * based on the stored annotations.

    + */ +public final class PathTypeAnnotationUtil { + + /** + * Regex pattern to match a trailing curly brace annotation at the end of a path. + * Captures the content inside the braces (Group 1). + * Examples: {@code {String}}, {@code {[String]}}, {@code {risk: Float, factor: String}}. + */ + static final Pattern ANNOTATION_PATTERN = Pattern.compile("\\{([^}]*)}$"); + + /** Claim URI prefix for the WSO2 local claim dialect. */ + static final String LOCAL_CLAIM_DIALECT_PREFIX = "http://wso2.org/claims/"; + + /** Claim URI prefix for WSO2 identity claims (subset of local claims, not user-modifiable). */ + static final String IDENTITY_CLAIM_URI_PREFIX = "http://wso2.org/claims/identity/"; + + /** Reusable ObjectMapper for parsing JSON-string values received for complex-typed paths. */ + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private PathTypeAnnotationUtil() { + + // Utility class, no instantiation. + } + + /** + * Strip a trailing path type annotation from a raw path. + * + * @param rawPath The raw path potentially containing a trailing {@code {annotation}}. + * @return A two-element array: {@code [cleanPath, annotation]}. + * If no annotation is found, annotation element is {@code null}. + */ + public static String[] stripAnnotation(String rawPath) { + + if (rawPath == null) { + return new String[]{null, null}; + } + + Matcher matcher = ANNOTATION_PATTERN.matcher(rawPath); + if (matcher.find()) { + String cleanPath = rawPath.substring(0, matcher.start()); + String annotation = matcher.group(1); + return new String[]{cleanPath, annotation}; + } + return new String[]{rawPath, null}; + } + + /** + * Coerce a value based on path type annotations. + * + *

    Annotation interpretation:

    + *
      + *
    • {@code null} (no annotation): value is coerced to String via {@code String.valueOf()}.
    • + *
    • Starts with {@code [} and contains {@code :} (e.g., {@code [risk: Float]}): + * complex object array — value is passed through as-is.
    • + *
    • Starts with {@code [} without {@code :} (e.g., {@code [String]}): + * multivalued primary type — value is expected to be a List; each element coerced to String. + * A single value is wrapped into a list.
    • + *
    • Contains {@code :} (e.g., {@code risk: Float, factor: String}): + * complex object — value is passed through as-is.
    • + *
    • Any other annotation (e.g., {@code String}, {@code Integer}): + * primary type — value is coerced to String via {@code String.valueOf()}.
    • + *
    + * + * @param path The operation path (used as lookup key in annotations map). + * @param value The raw value from the operation. + * @param pathTypeAnnotations Map from clean path to annotation content (may be empty). + * @return The coerced value. + */ + @SuppressWarnings("unchecked") + public static Object coerceValue(String path, Object value, + Map pathTypeAnnotations) { + + String annotation = pathTypeAnnotations.get(path); + + if (annotation == null) { + // No annotation: coerce to String. + return String.valueOf(value); + } + + // Check for multivalued annotation: starts with [ + if (annotation.startsWith("[")) { + String inner = annotation.substring(1, annotation.length() - 1); + if (inner.contains(":")) { + // Complex object array (e.g., [risk: Float, factor: String]): + // parse JSON string if needed, then pass through. + return tryParseJsonString(value); + } + // Multivalued primary type (e.g., [String], [Integer]): coerce to List. + // Parse JSON string first in case the value arrived as "[\"a\",\"b\"]". + Object resolvedList = tryParseJsonString(value); + if (resolvedList instanceof List) { + List rawList = (List) resolvedList; + List stringList = new ArrayList<>(); + for (Object item : rawList) { + stringList.add(String.valueOf(item)); + } + return stringList; + } + // Single value — wrap in a list. + List singleList = new ArrayList<>(); + singleList.add(String.valueOf(value)); + return singleList; + } + + // Check for complex object annotation: contains ":" + if (annotation.contains(":")) { + // Complex object (e.g., risk: Float, factor: String): + // parse JSON string if needed, then pass through. + return tryParseJsonString(value); + } + + // Primary type annotation (e.g., String, Integer, Boolean): coerce to String. + return String.valueOf(value); + } + + /** Maximum number of attributes allowed in a complex object annotation. */ + static final int MAX_ATTRIBUTES_PER_OBJECT = 10; + + /** Maximum number of items allowed in an array (primary or complex object array). */ + static final int MAX_ARRAY_ITEMS = 10; + + /** + * Validate that a complex object annotation does not exceed the maximum attribute count. + * Should be called on the raw annotation content (inside braces) before stripping. + * + * @param annotation The annotation content (e.g., {@code "risk: Float, factor: String"} + * or {@code "[risk: Float, factor: String]"}). May be {@code null}. + * @return {@code true} if the annotation is valid (within limits or not a complex annotation). + */ + public static boolean validateAnnotationLimits(String annotation) { + + if (annotation == null || annotation.isEmpty()) { + return true; + } + + String inner = annotation; + // Unwrap array brackets if present. + if (inner.startsWith("[") && inner.endsWith("]")) { + inner = inner.substring(1, inner.length() - 1); + } + + // Only validate complex annotations (those with attribute definitions containing ':'). + if (!inner.contains(":")) { + return true; + } + + return parseAnnotationAttributes(inner).size() <= MAX_ATTRIBUTES_PER_OBJECT; + } + + /** + * Validate a complex object value against its path type annotation schema. + * + *

    Validates:

    + *
      + *
    • Value is a Map with attribute names matching the annotation schema.
    • + *
    • Only one nesting level: attributes must be primary types or primary arrays.
    • + *
    • Attribute count does not exceed {@link #MAX_ATTRIBUTES_PER_OBJECT}.
    • + *
    • Array attributes do not exceed {@link #MAX_ARRAY_ITEMS} items.
    • + *
    + * + *

    For non-complex annotations (primary types, primary arrays), this method returns + * {@code true} without further validation since those are handled by coercion.

    + * + * @param path The operation path. + * @param value The value to validate. + * @param pathTypeAnnotations Map from clean path to annotation content. + * @return {@code true} if the value is valid against the annotation, {@code false} otherwise. + */ + @SuppressWarnings("unchecked") + public static boolean validateValueAgainstAnnotation(String path, Object value, + Map pathTypeAnnotations) { + + String annotation = pathTypeAnnotations.get(path); + if (annotation == null) { + return true; + } + + boolean isArray = annotation.startsWith("["); + String inner = isArray ? annotation.substring(1, annotation.length() - 1) : annotation; + + // Only validate complex annotations (those with attribute definitions). + if (!inner.contains(":")) { + return true; + } + + Map schema = parseAnnotationAttributes(inner); + + // Parse JSON string if the value arrived as serialized JSON + // (e.g., after JWE decryption or when the external service stringifies before encrypting). + Object resolvedValue = tryParseJsonString(value); + + if (isArray) { + // Complex object array: validate each item. + if (!(resolvedValue instanceof List)) { + return false; + } + List items = (List) resolvedValue; + if (items.size() > MAX_ARRAY_ITEMS) { + return false; + } + for (Object item : items) { + if (!validateSingleComplexObject(item, schema)) { + return false; + } + } + return true; + } + + // Single complex object. + return validateSingleComplexObject(resolvedValue, schema); + } + + /** + * Enforce array item limits on a value. Applies to both primary arrays and complex object arrays. + * + * @param path The operation path. + * @param value The value to check. + * @param pathTypeAnnotations Map from clean path to annotation content. + * @return {@code true} if the value is within array item limits, {@code false} otherwise. + */ + @SuppressWarnings("unchecked") + public static boolean enforceArrayItemLimit(String path, Object value, + Map pathTypeAnnotations) { + + String annotation = pathTypeAnnotations.get(path); + if (annotation == null || !annotation.startsWith("[")) { + return true; + } + + if (!(value instanceof List)) { + return true; + } + + return ((List) value).size() <= MAX_ARRAY_ITEMS; + } + + /** + * Attempt to parse a value as JSON if it is a String that starts with {@code [} or {@code {}. + * This handles values that arrive as serialized JSON strings — for example, after JWE + * decryption, or when an external service serialises a complex object/array before encrypting it. + * + *

    If the value is not a String, does not start with {@code [} or {@code {}, or cannot be + * parsed as valid JSON, the original value is returned unchanged.

    + * + * @param value The value to inspect. + * @return The parsed JSON structure (List or Map), or the original value if not applicable. + */ + private static Object tryParseJsonString(Object value) { + + if (!(value instanceof String)) { + return value; + } + String str = ((String) value).trim(); + if (!str.startsWith("[") && !str.startsWith("{")) { + return value; + } + try { + return OBJECT_MAPPER.readValue(str, Object.class); + } catch (IOException ignored) { + return value; + } + } + + /** + * Parse complex annotation attributes into a map of attribute name to type. + * Handles array types indicated by trailing {@code []}. + * + * @param inner The inner annotation content (e.g., {@code "risk: Float, factor: String"}). + * @return Map from attribute name to type string (e.g., {@code "Float"} or {@code "String[]"}). + */ + private static Map parseAnnotationAttributes(String inner) { + + Map attributes = new HashMap<>(); + String[] parts = inner.split(","); + for (String part : parts) { + String trimmed = part.trim(); + if (trimmed.isEmpty()) { + continue; + } + int colonIndex = trimmed.indexOf(':'); + if (colonIndex > 0) { + String name = trimmed.substring(0, colonIndex).trim(); + String type = trimmed.substring(colonIndex + 1).trim(); + attributes.put(name, type); + } + } + return attributes; + } + + /** + * Validate a single complex object value against a schema. + * Ensures value is a Map, keys match schema names, attribute count within limits, + * and nested values are only primary types or primary arrays (single nesting level). + * + * @param value The value to validate (expected to be a Map). + * @param schema The annotation schema (attribute name to type). + * @return {@code true} if valid. + */ + @SuppressWarnings("unchecked") + private static boolean validateSingleComplexObject(Object value, Map schema) { + + if (!(value instanceof Map)) { + return false; + } + + Map map = (Map) value; + + // Attribute count limit. + if (map.size() > MAX_ATTRIBUTES_PER_OBJECT) { + return false; + } + + // All keys in the value must be defined in the schema. + for (String key : map.keySet()) { + if (!schema.containsKey(key)) { + return false; + } + } + + // Validate single nesting level: each attribute value must be primary or primary array. + for (Map.Entry entry : map.entrySet()) { + String type = schema.get(entry.getKey()); + Object attrValue = entry.getValue(); + + if (type != null && type.endsWith("[]")) { + // Array attribute: validate it's a list of primitives within item limit. + if (!(attrValue instanceof List)) { + return false; + } + List list = (List) attrValue; + if (list.size() > MAX_ARRAY_ITEMS) { + return false; + } + for (Object item : list) { + if (item instanceof Map || item instanceof List) { + return false; + } + } + } else { + // Primary attribute: must not be a nested structure. + if (attrValue instanceof Map || attrValue instanceof List) { + return false; + } + } + } + + return true; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java index 43d0626cf597..0f49ba756b45 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java @@ -28,10 +28,8 @@ import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AllowedOperation; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ExposePath; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.OperationPath; import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; import org.wso2.carbon.identity.flow.mgt.FlowUpdateInterceptor; import org.wso2.carbon.identity.flow.mgt.exception.FlowMgtFrameworkException; @@ -56,7 +54,7 @@ *
  • Iterates graph nodes looking for In-Flow Extension executors with an * {@code accessConfig} metadata entry.
  • *
  • Parses the unified {@code accessConfig} JSON object - * ({@code {"expose": [...], "allowedOperations": [...]}}).
  • + * ({@code {"expose": [...], "modify": [...]}}). *
  • Saves the parsed data as per-flow-type override properties on the corresponding action * via {@code ActionManagementService.updateAction()}.
  • *
  • Strips the {@code accessConfig} key from executor metadata so it is NOT persisted @@ -68,12 +66,13 @@ public class AccessConfigFlowUpdateInterceptor implements FlowUpdateInterceptor private static final Log LOG = LogFactory.getLog(AccessConfigFlowUpdateInterceptor.class); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final String IN_FLOW_EXTENSION_EXECUTOR_NAME = "ExtensionExecutor"; private static final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; private static final String ACTION_ID_METADATA_KEY = "actionId"; private static final String EXPOSE_FIELD = "expose"; - private static final String ALLOWED_OPERATIONS_FIELD = "allowedOperations"; + private static final String MODIFY_FIELD = "modify"; @Override public void onFlowUpdate(String flowType, GraphConfig graphConfig, int tenantId) @@ -106,7 +105,8 @@ private void processNode(NodeConfig node, String flowType, String tenantDomain, ActionManagementService actionMgtService) throws FlowMgtFrameworkException { ExecutorDTO executorConfig = node.getExecutorConfig(); - if (executorConfig == null || executorConfig.getMetadata() == null) { + if (executorConfig == null || !executorConfig.getName().equals(IN_FLOW_EXTENSION_EXECUTOR_NAME) + ||executorConfig.getMetadata() == null) { return; } @@ -126,14 +126,13 @@ private void processNode(NodeConfig node, String flowType, String tenantDomain, metadata.remove(ACCESS_CONFIG_METADATA_KEY); try { - // Parse the unified accessConfig JSON: {"expose": [...], "allowedOperations": [...]} + // Parse the unified accessConfig JSON: {"expose": [...], "modify": [...]} Map accessConfigMap = OBJECT_MAPPER.readValue(accessConfigJson, MAP_TYPE_REF); - List expose = parseExposePaths(accessConfigMap.get(EXPOSE_FIELD)); - List allowedOps = parseAllowedOperations( - accessConfigMap.get(ALLOWED_OPERATIONS_FIELD)); + List expose = parseContextPaths(accessConfigMap.get(EXPOSE_FIELD)); + List modify = parseContextPaths(accessConfigMap.get(MODIFY_FIELD)); // Certificate is action-level only — not overridable per flow type. - AccessConfig overrideConfig = new AccessConfig(expose, allowedOps); + AccessConfig overrideConfig = new AccessConfig(expose, modify); // Retrieve the existing action to merge overrides (preserving other flow types). Action currentAction = actionMgtService.getActionByActionId( @@ -181,7 +180,7 @@ private void processNode(NodeConfig node, String flowType, String tenantDomain, * Accepts both simple strings (no encryption) and objects ({path, encrypted}). */ @SuppressWarnings("unchecked") - private List parseExposePaths(Object value) { + private List parseContextPaths(Object value) { if (value == null) { return null; @@ -192,64 +191,18 @@ private List parseExposePaths(Object value) { } List rawList = (List) value; - List result = new ArrayList<>(); + List result = new ArrayList<>(); for (Object item : rawList) { - if (item instanceof String) { - result.add(new ExposePath((String) item, false)); - } else if (item instanceof Map) { + if (item instanceof Map) { Map map = (Map) item; String path = (String) map.get("path"); boolean encrypted = toBooleanSafe(map.get("encrypted")); if (path != null) { - result.add(new ExposePath(path, encrypted)); + result.add(new ContextPath(path, encrypted)); } } - } - return result.isEmpty() ? null : result; - } - - /** - * Parse allowed operations from the raw JSON value. - * Accepts the nested format: [{op, paths: [{path, encrypted}]}]. - */ - @SuppressWarnings("unchecked") - private List parseAllowedOperations(Object value) { - - if (value == null) { - return null; - } - if (!(value instanceof List)) { - LOG.warn("Invalid allowedOperations value in flow override accessConfig. Expected list."); - return null; - } + else { - List rawList = (List) value; - List result = new ArrayList<>(); - for (Object item : rawList) { - if (item instanceof Map) { - Map map = (Map) item; - String op = (String) map.get("op"); - Object pathsObj = map.get("paths"); - if (op == null || !(pathsObj instanceof List)) { - continue; - } - List pathsList = (List) pathsObj; - List operationPaths = new ArrayList<>(); - for (Object pathItem : pathsList) { - if (pathItem instanceof Map) { - Map pathMap = (Map) pathItem; - String path = (String) pathMap.get("path"); - boolean encrypted = toBooleanSafe(pathMap.get("encrypted")); - if (path != null) { - operationPaths.add(new OperationPath(path, encrypted)); - } - } else if (pathItem instanceof String) { - operationPaths.add(new OperationPath((String) pathItem, false)); - } - } - if (!operationPaths.isEmpty()) { - result.add(new AllowedOperation(op, operationPaths)); - } } } return result.isEmpty() ? null : result; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java index fbd8c88e0b5b..15dfd2451913 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java @@ -33,7 +33,7 @@ public class InFlowExtensionActionConstants { * Property key for the allowed operations stored in IDN_ACTION_PROPERTIES. * Stored as a BLOB (JSON-serialized list of operation descriptors). */ - public static final String ACCESS_CONFIG_ALLOWED_OPERATIONS = "ACCESS_CONFIG_ALLOWED_OPERATIONS"; + public static final String ACCESS_CONFIG_MODIFY = "ACCESS_CONFIG_MODIFY"; /** * Separator used between property key and flow type in override keys. @@ -51,8 +51,8 @@ public class InFlowExtensionActionConstants { * Prefix for flow-type-specific allowed operations override properties. * Full key format: "ACCESS_CONFIG_ALLOWED_OPERATIONS:<FLOW_TYPE>" */ - public static final String ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX = - ACCESS_CONFIG_ALLOWED_OPERATIONS + OVERRIDE_KEY_SEPARATOR; + public static final String ACCESS_CONFIG_MODIFY_PREFIX = + ACCESS_CONFIG_MODIFY + OVERRIDE_KEY_SEPARATOR; /** * Metadata key used in executor metadata (flow configuration) to pass a unified access config diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java index cb44c2d81a65..6cdc9204318a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java @@ -24,17 +24,16 @@ import org.wso2.carbon.identity.action.management.api.service.ActionConverter; import org.wso2.carbon.identity.certificate.management.model.Certificate; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AllowedOperation; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ExposePath; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; import java.util.HashMap; import java.util.List; import java.util.Map; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_ALLOWED_OPERATIONS; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY_PREFIX; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.CERTIFICATE; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE_PREFIX; @@ -57,7 +56,7 @@ public Action.ActionTypes getSupportedActionType() { /** * Converts an {@link InFlowExtensionAction} to an {@link ActionDTO} for persistence. - * Maps the access config fields (expose, allowedOperations) and flow type overrides + * Maps the access config fields (expose, modify) and flow type overrides * into the DTO's properties map using prefixed keys. * * @param action The InFlowExtensionAction to convert. @@ -80,9 +79,9 @@ public ActionDTO buildActionDTO(Action action) { properties.put(ACCESS_CONFIG_EXPOSE, new ActionProperty.BuilderForService(accessConfig.getExpose()).build()); } - if (accessConfig.getAllowedOperations() != null) { - properties.put(ACCESS_CONFIG_ALLOWED_OPERATIONS, - new ActionProperty.BuilderForService(accessConfig.getAllowedOperations()).build()); + if (accessConfig.getModify() != null) { + properties.put(ACCESS_CONFIG_MODIFY, + new ActionProperty.BuilderForService(accessConfig.getModify()).build()); } } @@ -103,9 +102,9 @@ public ActionDTO buildActionDTO(Action action) { properties.put(ACCESS_CONFIG_EXPOSE_PREFIX + flowType, new ActionProperty.BuilderForService(overrideConfig.getExpose()).build()); } - if (overrideConfig.getAllowedOperations() != null) { - properties.put(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX + flowType, - new ActionProperty.BuilderForService(overrideConfig.getAllowedOperations()).build()); + if (overrideConfig.getModify() != null) { + properties.put(ACCESS_CONFIG_MODIFY_PREFIX + flowType, + new ActionProperty.BuilderForService(overrideConfig.getModify()).build()); } } } @@ -127,13 +126,13 @@ public ActionDTO buildActionDTO(Action action) { public Action buildAction(ActionDTO actionDTO) { // Default access config. - List expose = (List) actionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE); - List allowedOperations = - (List) actionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS); + List expose = (List) actionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE); + List modify = + (List) actionDTO.getPropertyValue(ACCESS_CONFIG_MODIFY); AccessConfig accessConfig = null; - if (expose != null || allowedOperations != null) { - accessConfig = new AccessConfig(expose, allowedOperations); + if (expose != null || modify != null) { + accessConfig = new AccessConfig(expose, modify); } // Encryption certificate (separate from access config). @@ -152,15 +151,15 @@ public Action buildAction(ActionDTO actionDTO) { AccessConfig existing = flowTypeOverrides.getOrDefault(flowType, new AccessConfig(null, null)); flowTypeOverrides.put(flowType, new AccessConfig( - (List) actionDTO.getPropertyValue(propertyKey), - existing.getAllowedOperations())); - } else if (propertyKey.startsWith(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX)) { - String flowType = propertyKey.substring(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX.length()); + (List) actionDTO.getPropertyValue(propertyKey), + existing.getModify())); + } else if (propertyKey.startsWith(ACCESS_CONFIG_MODIFY_PREFIX)) { + String flowType = propertyKey.substring(ACCESS_CONFIG_MODIFY_PREFIX.length()); AccessConfig existing = flowTypeOverrides.getOrDefault(flowType, new AccessConfig(null, null)); flowTypeOverrides.put(flowType, new AccessConfig( existing.getExpose(), - (List) actionDTO.getPropertyValue(propertyKey))); + (List) actionDTO.getPropertyValue(propertyKey))); } } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java index ec4ac4da1b15..c8063a7b5b4b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java @@ -33,9 +33,7 @@ import org.wso2.carbon.identity.certificate.management.exception.CertificateMgtException; import org.wso2.carbon.identity.certificate.management.model.Certificate; import org.wso2.carbon.identity.certificate.management.service.CertificateManagementService; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AllowedOperation; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ExposePath; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.OperationPath; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; import java.io.IOException; import java.util.ArrayList; @@ -46,8 +44,8 @@ import java.util.Set; import static java.util.Collections.emptyList; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_ALLOWED_OPERATIONS; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY_PREFIX; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.CERTIFICATE; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE_PREFIX; @@ -58,11 +56,11 @@ * ActionDTOModelResolver implementation for In-Flow Extension actions. *

    * Responsible for validating and transforming access config properties (expose paths and - * allowed operations) between the service layer representation and the DAO layer BLOB format. + * modify paths) between the service layer representation and the DAO layer BLOB format. *

    * *
      - *
    • Add operation: Validates expose paths and allowed operations, then serializes + *
    • Add operation: Validates expose paths and modify paths, then serializes * them to JSON {@link BinaryObject}s for BLOB storage in IDN_ACTION_PROPERTIES.
    • *
    • Get operation: Deserializes BLOBs back to service-layer list objects.
    • *
    • Update operation: Validates updated values or preserves existing ones (PUT semantics).
    • @@ -73,6 +71,8 @@ public class InFlowExtensionActionDTOModelResolver implements ActionDTOModelReso private static final Log LOG = LogFactory.getLog(InFlowExtensionActionDTOModelResolver.class); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final TypeReference> CONTEXT_PATH_LIST_TYPE_REF = + new TypeReference>() { }; private final CertificateManagementService certificateManagementService; @@ -96,15 +96,15 @@ public ActionDTO resolveForAddOperation(ActionDTO actionDTO, String tenantDomain Object exposeValue = actionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE); // Expose is an optional field. if (exposeValue != null) { - List validatedExpose = validateExpose(exposeValue); + List validatedExpose = validateExpose(exposeValue); properties.put(ACCESS_CONFIG_EXPOSE, createBlobProperty(validatedExpose)); } - Object allowedOpsValue = actionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS); - // Allowed operations is an optional field. - if (allowedOpsValue != null) { - List validatedOps = validateAllowedOperations(allowedOpsValue); - properties.put(ACCESS_CONFIG_ALLOWED_OPERATIONS, createBlobProperty(validatedOps)); + Object modifyValue = actionDTO.getPropertyValue(ACCESS_CONFIG_MODIFY); + // Modify is an optional field. + if (modifyValue != null) { + List validatedModify = validateExpose(modifyValue); + properties.put(ACCESS_CONFIG_MODIFY, createBlobProperty(validatedModify)); } // Handle certificate: store via CertificateManagementService and replace with ID. @@ -117,14 +117,14 @@ public ActionDTO resolveForAddOperation(ActionDTO actionDTO, String tenantDomain if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX)) { Object overrideExpose = actionDTO.getPropertyValue(key); if (overrideExpose != null) { - List validatedOverrideExpose = validateExpose(overrideExpose); + List validatedOverrideExpose = validateExpose(overrideExpose); properties.put(key, createBlobProperty(validatedOverrideExpose)); } - } else if (key.startsWith(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX)) { - Object overrideOps = actionDTO.getPropertyValue(key); - if (overrideOps != null) { - List validatedOverrideOps = validateAllowedOperations(overrideOps); - properties.put(key, createBlobProperty(validatedOverrideOps)); + } else if (key.startsWith(ACCESS_CONFIG_MODIFY_PREFIX)) { + Object overrideModify = actionDTO.getPropertyValue(key); + if (overrideModify != null) { + List validatedOverrideModify = validateExpose(overrideModify); + properties.put(key, createBlobProperty(validatedOverrideModify)); } } } @@ -147,9 +147,9 @@ public ActionDTO resolveForGetOperation(ActionDTO actionDTO, String tenantDomain ((BinaryObject) actionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE)).getJSONString())); } - if (actionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS) != null) { - properties.put(ACCESS_CONFIG_ALLOWED_OPERATIONS, deserializeAllowedOpsProperty( - ((BinaryObject) actionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS)).getJSONString())); + if (actionDTO.getPropertyValue(ACCESS_CONFIG_MODIFY) != null) { + properties.put(ACCESS_CONFIG_MODIFY, deserializeExposeProperty( + ((BinaryObject) actionDTO.getPropertyValue(ACCESS_CONFIG_MODIFY)).getJSONString())); } // Retrieve certificate by stored ID. @@ -163,9 +163,9 @@ public ActionDTO resolveForGetOperation(ActionDTO actionDTO, String tenantDomain && actionDTO.getPropertyValue(key) != null) { properties.put(key, deserializeExposeProperty( ((BinaryObject) actionDTO.getPropertyValue(key)).getJSONString())); - } else if (key.startsWith(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX) + } else if (key.startsWith(ACCESS_CONFIG_MODIFY_PREFIX) && actionDTO.getPropertyValue(key) != null) { - properties.put(key, deserializeAllowedOpsProperty( + properties.put(key, deserializeExposeProperty( ((BinaryObject) actionDTO.getPropertyValue(key)).getJSONString())); } } @@ -208,15 +208,14 @@ public ActionDTO resolveForUpdateOperation(ActionDTO updatingActionDTO, ActionDT // updated the existing properties should be sent to the DAO layer. // Handle default access config properties. - List expose = getResolvedUpdatingExpose(updatingActionDTO, existingActionDTO); + List expose = getResolvedUpdatingExpose(updatingActionDTO, existingActionDTO); if (!expose.isEmpty()) { properties.put(ACCESS_CONFIG_EXPOSE, createBlobProperty(expose)); } - List allowedOps = - getResolvedUpdatingAllowedOps(updatingActionDTO, existingActionDTO); - if (!allowedOps.isEmpty()) { - properties.put(ACCESS_CONFIG_ALLOWED_OPERATIONS, createBlobProperty(allowedOps)); + List modify = getResolvedUpdatingModify(updatingActionDTO, existingActionDTO); + if (!modify.isEmpty()) { + properties.put(ACCESS_CONFIG_MODIFY, createBlobProperty(modify)); } // Handle certificate update. @@ -229,7 +228,7 @@ public ActionDTO resolveForUpdateOperation(ActionDTO updatingActionDTO, ActionDT for (Map.Entry entry : existingActionDTO.getProperties().entrySet()) { String key = entry.getKey(); if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX) - || key.startsWith(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX)) { + || key.startsWith(ACCESS_CONFIG_MODIFY_PREFIX)) { properties.put(key, createBlobProperty(existingActionDTO.getPropertyValue(key))); } } @@ -241,14 +240,14 @@ public ActionDTO resolveForUpdateOperation(ActionDTO updatingActionDTO, ActionDT if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX)) { Object overrideExpose = updatingActionDTO.getPropertyValue(key); if (overrideExpose != null) { - List validatedOverrideExpose = validateExpose(overrideExpose); + List validatedOverrideExpose = validateExpose(overrideExpose); properties.put(key, createBlobProperty(validatedOverrideExpose)); } - } else if (key.startsWith(ACCESS_CONFIG_ALLOWED_OPERATIONS_PREFIX)) { - Object overrideOps = updatingActionDTO.getPropertyValue(key); - if (overrideOps != null) { - List validatedOverrideOps = validateAllowedOperations(overrideOps); - properties.put(key, createBlobProperty(validatedOverrideOps)); + } else if (key.startsWith(ACCESS_CONFIG_MODIFY_PREFIX)) { + Object overrideModify = updatingActionDTO.getPropertyValue(key); + if (overrideModify != null) { + List validatedOverrideModify = validateExpose(overrideModify); + properties.put(key, createBlobProperty(validatedOverrideModify)); } } } @@ -270,26 +269,26 @@ public void resolveForDeleteOperation(ActionDTO deletingActionDTO, String tenant // ---- Update helpers ---- @SuppressWarnings("unchecked") - private List getResolvedUpdatingExpose(ActionDTO updatingActionDTO, ActionDTO existingActionDTO) + private List getResolvedUpdatingExpose(ActionDTO updatingActionDTO, ActionDTO existingActionDTO) throws ActionDTOModelResolverException { if (updatingActionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE) != null) { return validateExpose(updatingActionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE)); } else if (existingActionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE) != null) { - return (List) existingActionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE); + return (List) existingActionDTO.getPropertyValue(ACCESS_CONFIG_EXPOSE); } return emptyList(); } @SuppressWarnings("unchecked") - private List getResolvedUpdatingAllowedOps(ActionDTO updatingActionDTO, - ActionDTO existingActionDTO) + private List getResolvedUpdatingModify(ActionDTO updatingActionDTO, + ActionDTO existingActionDTO) throws ActionDTOModelResolverException { - if (updatingActionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS) != null) { - return validateAllowedOperations(updatingActionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS)); - } else if (existingActionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS) != null) { - return (List) existingActionDTO.getPropertyValue(ACCESS_CONFIG_ALLOWED_OPERATIONS); + if (updatingActionDTO.getPropertyValue(ACCESS_CONFIG_MODIFY) != null) { + return validateExpose(updatingActionDTO.getPropertyValue(ACCESS_CONFIG_MODIFY)); + } else if (existingActionDTO.getPropertyValue(ACCESS_CONFIG_MODIFY) != null) { + return (List) existingActionDTO.getPropertyValue(ACCESS_CONFIG_MODIFY); } return emptyList(); } @@ -297,7 +296,7 @@ private List getResolvedUpdatingAllowedOps(ActionDTO updatingA // ---- Validation ---- @SuppressWarnings("unchecked") - private List validateExpose(Object exposeValue) throws ActionDTOModelResolverException { + private List validateExpose(Object exposeValue) throws ActionDTOModelResolverException { if (!(exposeValue instanceof List)) { throw new ActionDTOModelResolverClientException("Invalid expose format.", @@ -305,12 +304,12 @@ private List validateExpose(Object exposeValue) throws ActionDTOMode } List exposeList = (List) exposeValue; - List result = new ArrayList<>(); + List result = new ArrayList<>(); for (Object item : exposeList) { if (item instanceof String) { // Simple string path — no encryption. - result.add(new ExposePath((String) item, false)); + result.add(new ContextPath((String) item, false)); } else if (item instanceof Map) { Map map = (Map) item; if (!map.containsKey("path") || !(map.get("path") instanceof String)) { @@ -319,9 +318,9 @@ private List validateExpose(Object exposeValue) throws ActionDTOMode } String path = (String) map.get("path"); boolean encrypted = map.containsKey("encrypted") && toBooleanSafe(map.get("encrypted")); - result.add(new ExposePath(path, encrypted)); - } else if (item instanceof ExposePath) { - result.add((ExposePath) item); + result.add(new ContextPath(path, encrypted)); + } else if (item instanceof ContextPath) { + result.add((ContextPath) item); } else { throw new ActionDTOModelResolverClientException("Invalid expose format.", "Each expose entry must be a string or an object with 'path' and optional 'encrypted' fields."); @@ -329,11 +328,11 @@ private List validateExpose(Object exposeValue) throws ActionDTOMode } validateExposeCount(result); - validateExposePathFormat(result); + validateContextPathFormat(result); return result; } - private void validateExposeCount(List expose) throws ActionDTOModelResolverClientException { + private void validateExposeCount(List expose) throws ActionDTOModelResolverClientException { if (expose.size() > MAX_EXPOSE_PATHS) { throw new ActionDTOModelResolverClientException("Maximum expose paths limit exceeded.", @@ -342,10 +341,10 @@ private void validateExposeCount(List expose) throws ActionDTOModelR } } - private void validateExposePathFormat(List expose) throws ActionDTOModelResolverClientException { + private void validateContextPathFormat(List expose) throws ActionDTOModelResolverClientException { Set seen = new HashSet<>(); - for (ExposePath exposePath : expose) { + for (ContextPath exposePath : expose) { String path = exposePath.getPath(); if (path == null || path.trim().isEmpty()) { throw new ActionDTOModelResolverClientException("Invalid expose path.", @@ -362,68 +361,6 @@ private void validateExposePathFormat(List expose) throws ActionDTOM } } - @SuppressWarnings("unchecked") - private List validateAllowedOperations(Object allowedOpsValue) - throws ActionDTOModelResolverClientException { - - if (!(allowedOpsValue instanceof List)) { - throw new ActionDTOModelResolverClientException("Invalid allowed operations format.", - "Allowed operations should be provided as a list."); - } - - List opsList = (List) allowedOpsValue; - List result = new ArrayList<>(); - - for (Object item : opsList) { - if (item instanceof AllowedOperation) { - result.add((AllowedOperation) item); - } else if (item instanceof Map) { - Map opMap = (Map) item; - if (!opMap.containsKey("op") || !(opMap.get("op") instanceof String)) { - throw new ActionDTOModelResolverClientException("Invalid allowed operation.", - "Each allowed operation must contain an 'op' field with a string value."); - } - String op = (String) opMap.get("op"); - - if (!opMap.containsKey("paths") || !(opMap.get("paths") instanceof List)) { - throw new ActionDTOModelResolverClientException("Invalid allowed operation.", - "Each allowed operation must contain a 'paths' list."); - } - - List pathsList = (List) opMap.get("paths"); - List operationPaths = new ArrayList<>(); - for (Object pathItem : pathsList) { - if (pathItem instanceof OperationPath) { - operationPaths.add((OperationPath) pathItem); - } else if (pathItem instanceof Map) { - Map pathMap = (Map) pathItem; - if (!pathMap.containsKey("path") || !(pathMap.get("path") instanceof String)) { - throw new ActionDTOModelResolverClientException("Invalid operation path.", - "Each path entry must contain a 'path' field with a string value."); - } - String path = (String) pathMap.get("path"); - boolean encrypted = pathMap.containsKey("encrypted") - && toBooleanSafe(pathMap.get("encrypted")); - operationPaths.add(new OperationPath(path, encrypted)); - } else if (pathItem instanceof String) { - // Simple string path — no encryption. - operationPaths.add(new OperationPath((String) pathItem, false)); - } else { - throw new ActionDTOModelResolverClientException("Invalid operation path format.", - "Each path entry must be a string or an object with 'path' and " - + "optional 'encrypted' fields."); - } - } - result.add(new AllowedOperation(op, operationPaths)); - } else { - throw new ActionDTOModelResolverClientException("Invalid allowed operations format.", - "Each allowed operation must be an object with 'op' and 'paths' fields."); - } - } - - return result; - } - // ---- Certificate lifecycle helpers ---- /** @@ -592,26 +529,13 @@ private ActionProperty createBlobProperty(Object value) throws ActionDTOModelRes private ActionProperty deserializeExposeProperty(String jsonString) throws ActionDTOModelResolverException { try { - List expose = OBJECT_MAPPER.readValue(jsonString, - new TypeReference>() { }); + List expose = OBJECT_MAPPER.readValue(jsonString, CONTEXT_PATH_LIST_TYPE_REF); return new ActionProperty.BuilderForService(expose).build(); } catch (IOException e) { throw new ActionDTOModelResolverException("Error reading expose values from storage.", e); } } - private ActionProperty deserializeAllowedOpsProperty(String jsonString) - throws ActionDTOModelResolverException { - - try { - List allowedOps = OBJECT_MAPPER.readValue(jsonString, - new TypeReference>() { }); - return new ActionProperty.BuilderForService(allowedOps).build(); - } catch (IOException e) { - throw new ActionDTOModelResolverException("Error reading allowed operations from storage.", e); - } - } - /** * Safely converts a value to boolean, handling both {@link Boolean} and {@link String} types. * Jackson deserializes JSON {@code true} as {@link Boolean} but JSON {@code "true"} as {@link String}. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java index 7ef2cab4de85..2fcf3165a3e4 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java @@ -18,63 +18,58 @@ package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.PathTypeAnnotationUtil; + import java.util.Collections; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; /** * Access Configuration for In-Flow Extension actions. *

      * Defines which parts of the flow context are exposed to the external service - * and what operations the service is allowed to perform. + * and which paths the service is allowed to modify. *

      * *
        - *
      • {@code expose} – structured list of {@link ExposePath} entries, each with a hierarchical + *
      • {@code expose} – structured list of {@link ContextPath} entries, each with a hierarchical * path prefix and an {@code encrypted} flag controlling outbound JWE encryption.
      • - *
      • {@code allowedOperations} – list of {@link AllowedOperation} entries, each grouping an - * operation type with a list of {@link OperationPath} entries carrying per-path encryption - * flags for inbound JWE encryption.
      • + *
      • {@code modify} – structured list of {@link ContextPath} entries defining which paths the + * external service can change. All modifications map to REPLACE operations internally. + * The {@code encrypted} flag on modify paths controls inbound JWE encryption (the external + * service encrypts values, IS decrypts with its private key).
      • *
      * + *

      Expose and modify are independent: expose controls what data is sent to the external service, + * while modify controls what data the external service is allowed to change. A path can appear + * in both lists with independent encryption flags.

      + * *

      Note: The external service's certificate for outbound encryption is held in the - * separate {@link Encryption} model, not in AccessConfig. This follows the same separation - * pattern as {@code PasswordSharing} in the PreUpdatePassword action type.

      + * separate {@link Encryption} model, not in AccessConfig.

      */ public class AccessConfig { - /** - * Regex pattern to match path type annotations at the end of a path. - * Strips trailing bracket expressions like {@code []}, {@code [field1, field2]}. - * These annotations are used by the request builder for type coercion but are not - * part of the logical path that the external service sees. - */ - private static final Pattern PATH_ANNOTATION_PATTERN = Pattern.compile("\\[([^\\]]*)]$"); - - private final List expose; - private final List allowedOperations; + private final List expose; + private final List modify; /** - * Constructs an AccessConfig with the given expose paths and allowed operations. + * Constructs an AccessConfig with the given expose and modify paths. * - * @param expose List of expose path entries. May be {@code null}. - * @param allowedOperations List of allowed operation entries. May be {@code null}. + * @param expose List of expose path entries. May be {@code null}. + * @param modify List of modify path entries. May be {@code null}. */ - public AccessConfig(List expose, List allowedOperations) { + public AccessConfig(List expose, List modify) { this.expose = expose != null ? Collections.unmodifiableList(expose) : null; - this.allowedOperations = allowedOperations != null - ? Collections.unmodifiableList(allowedOperations) : null; + this.modify = modify != null ? Collections.unmodifiableList(modify) : null; } /** * Returns the list of expose path entries. * - * @return Unmodifiable list of {@link ExposePath} entries, or {@code null} if not configured. + * @return Unmodifiable list of {@link ContextPath} entries, or {@code null} if not configured. */ - public List getExpose() { + public List getExpose() { return expose; } @@ -90,17 +85,31 @@ public List getExposePaths() { if (expose == null) { return null; } - return expose.stream().map(ExposePath::getPath).collect(Collectors.toList()); + return expose.stream().map(ContextPath::getPath).collect(Collectors.toList()); + } + + /** + * Returns the list of modify path entries. + * + * @return Unmodifiable list of {@link ContextPath} entries, or {@code null} if not configured. + */ + public List getModify() { + + return modify; } /** - * Returns the list of allowed operation entries. + * Returns the flat list of modify path strings (without encryption metadata). + * Convenience method for components that only need path strings. * - * @return Unmodifiable list of {@link AllowedOperation} entries, or {@code null} if not configured. + * @return List of path strings, or {@code null} if modify is not configured. */ - public List getAllowedOperations() { + public List getModifyPaths() { - return allowedOperations; + if (modify == null) { + return null; + } + return modify.stream().map(ContextPath::getPath).collect(Collectors.toList()); } /** @@ -118,59 +127,28 @@ public boolean isExposePathEncrypted(String pathPrefix) { return expose.stream() .filter(ep -> pathPrefix.startsWith(ep.getPath())) .reduce((a, b) -> a.getPath().length() >= b.getPath().length() ? a : b) - .map(ExposePath::isEncrypted) + .map(ContextPath::isEncrypted) .orElse(false); } /** - * Check if a given operation path has inbound encryption enabled. - * Iterates the nested {@link OperationPath} entries within each {@link AllowedOperation}. - * Path type annotations (e.g. {@code []}, {@code [schema]}) are stripped from stored paths - * before comparison, since operation paths from the external service do not include annotations. + * Check if a given modify path has inbound encryption enabled. + * Matches the most specific (longest) modify path that is a prefix of the given path. * - * @param op The operation type (e.g. "REPLACE"). - * @param path The operation path (clean, without annotations). - * @return {@code true} if the matching operation path has {@code encrypted = true}. + * @param pathPrefix The path to check (clean, without annotations). + * @return {@code true} if the matching modify entry has {@code encrypted = true}. */ - public boolean isOperationPathEncrypted(String op, String path) { + public boolean isModifyPathEncrypted(String pathPrefix) { - if (allowedOperations == null) { + if (modify == null) { return false; } - return allowedOperations.stream() - .filter(ao -> ao.getOp().equalsIgnoreCase(op)) - .flatMap(ao -> ao.getPaths().stream()) - .filter(opPath -> path.startsWith(stripAnnotation(opPath.getPath()))) - .reduce((a, b) -> stripAnnotation(a.getPath()).length() - >= stripAnnotation(b.getPath()).length() ? a : b) - .map(OperationPath::isEncrypted) + return modify.stream() + .filter(mp -> pathPrefix.startsWith(PathTypeAnnotationUtil.stripAnnotation(mp.getPath())[0])) + .reduce((a, b) -> PathTypeAnnotationUtil.stripAnnotation(a.getPath())[0].length() + >= PathTypeAnnotationUtil.stripAnnotation(b.getPath())[0].length() ? a : b) + .map(ContextPath::isEncrypted) .orElse(false); } - /** - * Returns whether any expose path or allowed operation path has encryption enabled. - * - * @return {@code true} if at least one path has {@code encrypted = true}. - */ - public boolean hasAnyEncryptedPath() { - - boolean hasEncryptedExpose = expose != null - && expose.stream().anyMatch(ExposePath::isEncrypted); - boolean hasEncryptedOps = allowedOperations != null - && allowedOperations.stream().anyMatch(AllowedOperation::hasAnyEncryptedPath); - return hasEncryptedExpose || hasEncryptedOps; - } - - /** - * Strip trailing path type annotations from a path string. - * For example, {@code "/properties/riskFactors[]"} becomes {@code "/properties/riskFactors"}. - * - * @param path The path that may contain trailing annotations. - * @return The path without annotations. - */ - private static String stripAnnotation(String path) { - - Matcher m = PATH_ANNOTATION_PATTERN.matcher(path); - return m.find() ? path.substring(0, m.start()) : path; - } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AllowedOperation.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AllowedOperation.java deleted file mode 100644 index d040ea86afac..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AllowedOperation.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2025, 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.inflow.extension.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.Collections; -import java.util.List; - -/** - * Domain model for an allowed operation with structured paths. - *

      - * Each entry groups an operation type with a list of {@link OperationPath} entries, - * where each path carries its own {@code encrypted} flag. - *

      - * - *

      Example JSON:

      - *
      {@code
      - * {
      - *   "op": "REPLACE",
      - *   "paths": [
      - *     { "path": "/user/claims/", "encrypted": false },
      - *     { "path": "/user/credentials/password", "encrypted": true }
      - *   ]
      - * }
      - * }
      - */ -public class AllowedOperation { - - private final String op; - private final List paths; - - @JsonCreator - public AllowedOperation(@JsonProperty("op") String op, - @JsonProperty("paths") List paths) { - - this.op = op; - this.paths = paths != null ? Collections.unmodifiableList(paths) : Collections.emptyList(); - } - - /** - * Returns the operation type (e.g. {@code "ADD"}, {@code "REPLACE"}, {@code "REMOVE"}). - * - * @return The operation type string. - */ - public String getOp() { - - return op; - } - - /** - * Returns the list of {@link OperationPath} entries for this operation. - * - * @return Unmodifiable list of operation paths with encryption flags. - */ - public List getPaths() { - - return paths; - } - - /** - * Returns whether any path in this operation has encryption enabled. - * - * @return {@code true} if at least one path has {@code encrypted = true}. - */ - public boolean hasAnyEncryptedPath() { - - return paths.stream().anyMatch(OperationPath::isEncrypted); - } -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/ExposePath.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/ContextPath.java similarity index 75% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/ExposePath.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/ContextPath.java index 04728ac59153..06b8445a0c4d 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/ExposePath.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/ContextPath.java @@ -22,20 +22,22 @@ import com.fasterxml.jackson.annotation.JsonProperty; /** - * Domain model for an expose path with an optional encryption flag. + * Domain model for a context path with an optional encryption flag. + * Used for both expose and modify path entries in {@link AccessConfig}. *

      - * When {@code encrypted} is {@code true}, the value at this path prefix is JWE-encrypted - * in the outbound request using the external service's public certificate before sending. + * When {@code encrypted} is {@code true}, the value at this path prefix is JWE-encrypted: + * for expose paths, outbound values are encrypted with the external service's certificate; + * for modify paths, inbound values from the external service are encrypted with IS's key. *

      */ -public class ExposePath { +public class ContextPath { private final String path; private final boolean encrypted; @JsonCreator - public ExposePath(@JsonProperty("path") String path, - @JsonProperty("encrypted") boolean encrypted) { + public ContextPath(@JsonProperty("path") String path, + @JsonProperty("encrypted") boolean encrypted) { this.path = path; this.encrypted = encrypted; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationPath.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationPath.java deleted file mode 100644 index bb4abaa64bbd..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationPath.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2025, 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.inflow.extension.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Domain model for an operation path with an optional encryption flag. - *

      - * Used within {@link AllowedOperation} to represent a single path on which - * the operation is allowed, along with an encryption flag indicating whether - * the IS should expect/produce JWE-encrypted values for that specific path. - *

      - */ -public class OperationPath { - - private final String path; - private final boolean encrypted; - - @JsonCreator - public OperationPath(@JsonProperty("path") String path, - @JsonProperty("encrypted") boolean encrypted) { - - this.path = path; - this.encrypted = encrypted; - } - - /** - * Returns the operation path (e.g. {@code "/user/claims/"}, {@code "/properties/riskScore"}). - * - * @return The path string. - */ - public String getPath() { - - return path; - } - - /** - * Returns whether this operation path requires JWE encryption/decryption. - *
        - *
      • ADD operations: external service sends JWE-encrypted values to the IS.
      • - *
      • REPLACE operations: both directions — outbound current value encrypted, - * inbound replacement value encrypted.
      • - *
      • REMOVE operations: outbound current value encrypted in the expose event.
      • - *
      - * - * @return {@code true} if JWE encryption is required for this path. - */ - public boolean isEncrypted() { - - return encrypted; - } -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java index cda3fcdc80cb..4cde31beb6d4 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java @@ -94,14 +94,12 @@ protected void activate(ComponentContext context) { bundleContext.registerService(FlowExecutionListener.class.getName(), new InputProcessingListener(), null); - // Register In-Flow Extension executor and its action execution services bundleContext.registerService(Executor.class.getName(), new InFlowExtensionExecutor(), null); bundleContext.registerService(ActionExecutionRequestBuilder.class.getName(), new InFlowExtensionRequestBuilder(), null); bundleContext.registerService(ActionExecutionResponseProcessor.class.getName(), new InFlowExtensionResponseProcessor(), null); - // Register In-Flow Extension action management services bundleContext.registerService(ActionConverter.class.getName(), new InFlowExtensionActionConverter(), null); bundleContext.registerService(ActionDTOModelResolver.class.getName(), @@ -109,7 +107,6 @@ protected void activate(ComponentContext context) { FlowExecutionEngineDataHolder.getInstance().getCertificateManagementService()), null); - // Register flow update interceptor for access config overrides bundleContext.registerService(FlowUpdateInterceptor.class.getName(), new AccessConfigFlowUpdateInterceptor(), null); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java index 92d06ad41b78..e6fd5242239a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java @@ -34,6 +34,8 @@ import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionEvent; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionRequestBuilder; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; 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.mgt.model.NodeConfig; @@ -125,18 +127,19 @@ public void testBuildRequestUsesDefaultExposeWhenExposeIsNull() assertNotNull(event.getUserInputs()); } - // ========================= buildAllowedOperations ========================= + // ========================= buildAllowedOperations (from modify) ========================= @Test - public void testBuildRequestWithValidAllowedOperations() + public void testBuildRequestWithValidModifyPaths() throws ActionExecutionRequestBuilderException { FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); - String allowedOpsJson = "[{\"op\":\"ADD\",\"paths\":[\"/properties/riskScore\"]}]"; + AccessConfig accessConfig = new AccessConfig(null, + Arrays.asList(new ContextPath("/properties/riskScore", false))); FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, allowedOpsJson) + .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) .add(InFlowExtensionExecutor.EXPOSE_KEY, Arrays.asList("/user/", "/properties/", "/input/", "/flow/", "/graph/")); @@ -146,12 +149,12 @@ public void testBuildRequestWithValidAllowedOperations() List ops = request.getAllowedOperations(); assertNotNull(ops); assertEquals(ops.size(), 1); - assertEquals(ops.get(0).getOp(), Operation.ADD); + assertEquals(ops.get(0).getOp(), Operation.REPLACE); assertTrue(ops.get(0).getPaths().contains("/properties/riskScore")); } @Test - public void testBuildRequestWithNullAllowedOperationsJson() + public void testBuildRequestWithNoAccessConfig() throws ActionExecutionRequestBuilderException { FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); @@ -168,13 +171,15 @@ public void testBuildRequestWithNullAllowedOperationsJson() } @Test - public void testBuildRequestWithMalformedAllowedOperationsJson() + public void testBuildRequestWithEmptyModifyPaths() throws ActionExecutionRequestBuilderException { FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + AccessConfig accessConfig = new AccessConfig(null, Arrays.asList()); + FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, "not-valid-json") + .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); @@ -185,45 +190,6 @@ public void testBuildRequestWithMalformedAllowedOperationsJson() assertTrue(ops.isEmpty()); } - @Test - public void testBuildRequestSkipsInvalidOperationConfig() - throws ActionExecutionRequestBuilderException { - - FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); - // Missing "op" key — should be skipped. - String json = "[{\"paths\":[\"/properties/score\"]},{\"op\":\"ADD\",\"paths\":[\"/properties/level\"]}]"; - - FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) - .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); - - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); - - List ops = request.getAllowedOperations(); - assertEquals(ops.size(), 1); - assertEquals(ops.get(0).getOp(), Operation.ADD); - } - - @Test - public void testBuildRequestSkipsUnknownOperationType() - throws ActionExecutionRequestBuilderException { - - FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); - String json = "[{\"op\":\"PATCH\",\"paths\":[\"/properties/score\"]}]"; - - FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) - .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); - - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); - - assertTrue(request.getAllowedOperations().isEmpty()); - } - // ========================= Path annotation stripping ========================= @Test @@ -232,26 +198,27 @@ public void testPathAnnotationStrippingSimpleArray() throws ActionExecutionRequestBuilderException { FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); - String json = "[{\"op\":\"ADD\",\"paths\":[\"/properties/riskFactors[]\"]}]"; + AccessConfig accessConfig = new AccessConfig(null, + Arrays.asList(new ContextPath("/properties/riskFactors{[String]}", false))); FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) + .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); - // AllowedOperation should have clean path (without []). + // AllowedOperation should have clean path (without {[String]}). AllowedOperation op = request.getAllowedOperations().get(0); assertTrue(op.getPaths().contains("/properties/riskFactors")); - assertFalse(op.getPaths().contains("/properties/riskFactors[]")); + assertFalse(op.getPaths().contains("/properties/riskFactors{[String]}")); // Annotations should be stored in FlowContext. Map annotations = flowContext.getValue( InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); assertNotNull(annotations); - assertEquals(annotations.get("/properties/riskFactors"), ""); + assertEquals(annotations.get("/properties/riskFactors"), "[String]"); } @Test @@ -260,11 +227,12 @@ public void testPathAnnotationStrippingSchemaAnnotation() throws ActionExecutionRequestBuilderException { FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); - String json = "[{\"op\":\"ADD\",\"paths\":[\"/properties/items[name,count]\"]}]"; + AccessConfig accessConfig = new AccessConfig(null, + Arrays.asList(new ContextPath("/properties/items{name: String, count: Integer}", false))); FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) + .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); @@ -272,11 +240,11 @@ public void testPathAnnotationStrippingSchemaAnnotation() AllowedOperation op = request.getAllowedOperations().get(0); assertTrue(op.getPaths().contains("/properties/items")); - assertFalse(op.getPaths().contains("/properties/items[name,count]")); + assertFalse(op.getPaths().contains("/properties/items{name: String, count: Integer}")); Map annotations = flowContext.getValue( InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); - assertEquals(annotations.get("/properties/items"), "name,count"); + assertEquals(annotations.get("/properties/items"), "name: String, count: Integer"); } @Test @@ -285,11 +253,12 @@ public void testPathWithoutAnnotationNotStoredInAnnotationsMap() throws ActionExecutionRequestBuilderException { FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); - String json = "[{\"op\":\"ADD\",\"paths\":[\"/properties/riskScore\"]}]"; + AccessConfig accessConfig = new AccessConfig(null, + Arrays.asList(new ContextPath("/properties/riskScore", false))); FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) + .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); @@ -307,12 +276,14 @@ public void testMultipleAnnotatedPaths() throws ActionExecutionRequestBuilderException { FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); - String json = "[{\"op\":\"ADD\",\"paths\":" + - "[\"/properties/riskScore\",\"/properties/riskFactors[]\",\"/properties/items[name,count]\"]}]"; + AccessConfig accessConfig = new AccessConfig(null, Arrays.asList( + new ContextPath("/properties/riskScore", false), + new ContextPath("/properties/riskFactors{[String]}", false), + new ContextPath("/properties/items{name: String, count: Integer}", false))); FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) + .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); @@ -327,91 +298,66 @@ public void testMultipleAnnotatedPaths() Map annotations = flowContext.getValue( InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); assertEquals(annotations.size(), 2); - assertEquals(annotations.get("/properties/riskFactors"), ""); - assertEquals(annotations.get("/properties/items"), "name,count"); + assertEquals(annotations.get("/properties/riskFactors"), "[String]"); + assertEquals(annotations.get("/properties/items"), "name: String, count: Integer"); } - // ========================= REPLACE paths auto-expose ========================= + // ========================= Expose and modify independence ========================= @Test - public void testReplacePathsAreAutoExposed() + public void testModifyPathsDoNotAffectExpose() throws ActionExecutionRequestBuilderException { FlowExecutionContext execCtx = createFullFlowExecutionContext(); - // Expose only /input/ — /properties/ not exposed. - // But REPLACE on /properties/riskScore should auto-expose it. + // Expose only /input/ and /flow/ — /properties/ is NOT exposed. List expose = Arrays.asList("/input/", "/flow/"); - String json = "[{\"op\":\"REPLACE\",\"paths\":[\"/properties/riskScore\"]}]"; + // Modify path targets /properties/riskScore — but this should NOT auto-expose it. + AccessConfig accessConfig = new AccessConfig(null, + Arrays.asList(new ContextPath("/properties/riskScore", false))); - // Pre-set the property so it appears in the event if exposed. execCtx.setProperty("riskScore", "50"); FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) + .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) .add(InFlowExtensionExecutor.EXPOSE_KEY, expose); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); - // The event should contain properties because REPLACE path auto-exposed it. - InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); - assertNotNull(event.getFlowProperties()); - assertTrue(event.getFlowProperties().containsKey("riskScore")); - assertEquals(event.getFlowProperties().get("riskScore"), "50"); - } - - @Test - public void testReplacePathAlreadyExposedNoAugmentation() - throws ActionExecutionRequestBuilderException { - - FlowExecutionContext execCtx = createFullFlowExecutionContext(); - // /properties/ already exposed. - List expose = Arrays.asList("/properties/", "/input/", "/flow/"); - String json = "[{\"op\":\"REPLACE\",\"paths\":[\"/properties/riskScore\"]}]"; - - execCtx.setProperty("riskScore", "50"); - - FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) - .add(InFlowExtensionExecutor.EXPOSE_KEY, expose); - - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + // Modify paths should produce REPLACE allowed operation. + List ops = request.getAllowedOperations(); + assertEquals(ops.size(), 1); + assertEquals(ops.get(0).getOp(), Operation.REPLACE); - // Should still work — no errors, properties exposed. + // Properties should NOT be in event — expose and modify are independent. InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); - assertNotNull(event.getFlowProperties()); - assertTrue(event.getFlowProperties().containsKey("riskScore")); + assertTrue(event.getFlowProperties() == null || event.getFlowProperties().isEmpty()); } @Test - public void testOnlyReplacePathsAreAutoExposedNotAddPaths() + public void testMultipleModifyPathsProduceSingleReplaceOperation() throws ActionExecutionRequestBuilderException { FlowExecutionContext execCtx = createFullFlowExecutionContext(); - List expose = Arrays.asList("/input/", "/flow/"); - // ADD on /properties/riskLevel should NOT be auto-exposed. - // REPLACE on /properties/riskScore SHOULD be auto-exposed. - String json = "[{\"op\":\"ADD\",\"paths\":[\"/properties/riskLevel\"]}," + - "{\"op\":\"REPLACE\",\"paths\":[\"/properties/riskScore\"]}]"; - - execCtx.setProperty("riskScore", "50"); + AccessConfig accessConfig = new AccessConfig(null, Arrays.asList( + new ContextPath("/properties/riskScore", false), + new ContextPath("/properties/riskLevel", false), + new ContextPath("/user/claims/http://wso2.org/claims/email", false))); FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ALLOWED_OPERATIONS_KEY, json) - .add(InFlowExtensionExecutor.EXPOSE_KEY, expose); + .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) + .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); - // Properties should be in event due to REPLACE auto-expose. - InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); - assertNotNull(event.getFlowProperties()); - // riskScore exposed via REPLACE, riskLevel only configured for ADD (not auto-exposed). - assertTrue(event.getFlowProperties().containsKey("riskScore")); + // All modify paths should be grouped into a single REPLACE operation. + List ops = request.getAllowedOperations(); + assertEquals(ops.size(), 1); + assertEquals(ops.get(0).getOp(), Operation.REPLACE); + assertEquals(ops.get(0).getPaths().size(), 3); } // ========================= Expose filtering ========================= diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java index 96e6d6088425..db83a4804e71 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java @@ -56,7 +56,6 @@ import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; @@ -107,45 +106,60 @@ public void testGetSupportedActionType() { assertEquals(responseProcessor.getSupportedActionType(), ActionType.IN_FLOW_EXTENSION); } - // ========================= processSuccessResponse — Property ADD ========================= + // ========================= processSuccessResponse — Property REPLACE ========================= @Test - public void testPropertyAddFlat() throws ActionExecutionResponseProcessorException { + public void testPropertyReplaceFlatExists() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); + execCtx.setProperty("riskScore", "50"); - PerformableOperation addOp = createOperation(Operation.ADD, "/properties/riskScore", "75"); - ActionExecutionStatus status = executeSuccessResponse(execCtx, addOp, Collections.emptyMap()); + PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/riskScore", "80"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, Collections.emptyMap()); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - assertEquals(execCtx.getProperties().get("riskScore"), "75"); + assertEquals(execCtx.getProperties().get("riskScore"), "80"); } @Test - public void testPropertyAddNested() throws ActionExecutionResponseProcessorException { + public void testPropertyReplaceCreatesIfMissing() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); + // riskScore not set — REPLACE should auto-create it. - PerformableOperation addOp = createOperation(Operation.ADD, "/properties/risk/level", "HIGH"); - ActionExecutionStatus status = executeSuccessResponse(execCtx, addOp, Collections.emptyMap()); + PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/riskScore", "80"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, Collections.emptyMap()); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + assertEquals(execCtx.getProperties().get("riskScore"), "80"); + } + + @Test + public void testPropertyReplaceCoercesToStringByDefault() + throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + // Integer value with no annotation → coerced to String. + PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/riskScore", 75); + ActionExecutionStatus status = executeSuccessResponse( + execCtx, replaceOp, Collections.emptyMap()); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - Object riskMap = execCtx.getProperties().get("risk"); - assertNotNull(riskMap); - assertTrue(riskMap instanceof Map); - assertEquals(((Map) riskMap).get("level"), "HIGH"); + assertEquals(execCtx.getProperties().get("riskScore"), "75"); } @Test - public void testPropertyAddWithArrayAnnotation() throws ActionExecutionResponseProcessorException { + public void testPropertyReplaceWithMultivaluedAnnotation() + throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); Map annotations = new HashMap<>(); - annotations.put("/properties/riskFactors", ""); + annotations.put("/properties/riskFactors", "[String]"); List factors = Arrays.asList("ip_mismatch", "new_device"); - PerformableOperation addOp = createOperation(Operation.ADD, "/properties/riskFactors", factors); - ActionExecutionStatus status = executeSuccessResponse(execCtx, addOp, annotations); + PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/riskFactors", factors); + ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, annotations); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); Object stored = execCtx.getProperties().get("riskFactors"); @@ -154,224 +168,254 @@ public void testPropertyAddWithArrayAnnotation() throws ActionExecutionResponseP } @Test - public void testPropertyAddWithSchemaAnnotation() throws ActionExecutionResponseProcessorException { + public void testPropertyReplaceMultivaluedSingleValueWrapped() + throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); Map annotations = new HashMap<>(); - annotations.put("/properties/items", "name,count"); + annotations.put("/properties/tags", "[String]"); - List> items = new ArrayList<>(); - Map item = new HashMap<>(); - item.put("name", "item1"); - item.put("count", 5); - items.add(item); - - PerformableOperation addOp = createOperation(Operation.ADD, "/properties/items", items); - ActionExecutionStatus status = executeSuccessResponse(execCtx, addOp, annotations); + // Single value with [String] annotation → wrapped in a list. + PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/tags", "singleTag"); + executeSuccessResponse(execCtx, replaceOp, annotations); - assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - // Schema annotation → value passed through as-is. - Object stored = execCtx.getProperties().get("items"); + Object stored = execCtx.getProperties().get("tags"); assertTrue(stored instanceof List); + assertEquals(((List) stored).size(), 1); + assertEquals(((List) stored).get(0), "singleTag"); } @Test - public void testPropertyAddWithoutAnnotationCoercesToString() + public void testPropertyReplaceWithComplexAnnotation() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); + Map annotations = new HashMap<>(); + annotations.put("/properties/item", "name: String, count: Integer"); - // Integer value with no annotation → coerced to String. - PerformableOperation addOp = createOperation(Operation.ADD, "/properties/riskScore", 75); - ActionExecutionStatus status = executeSuccessResponse(execCtx, addOp, Collections.emptyMap()); + Map item = new HashMap<>(); + item.put("name", "item1"); + item.put("count", 5); + + PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/item", item); + ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, annotations); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - assertEquals(execCtx.getProperties().get("riskScore"), "75"); + // Complex annotation → value passed through as-is. + Object stored = execCtx.getProperties().get("item"); + assertTrue(stored instanceof Map); } @Test - public void testPropertyAddNullValue() throws ActionExecutionResponseProcessorException { + public void testPropertyReplaceWithPrimaryTypeAnnotation() + throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); + Map annotations = new HashMap<>(); + annotations.put("/properties/score", "Integer"); - PerformableOperation addOp = createOperation(Operation.ADD, "/properties/riskScore", null); - // Should still succeed (status is SUCCESS overall) but the individual operation should fail. - ActionExecutionStatus status = executeSuccessResponse(execCtx, addOp, Collections.emptyMap()); + // Integer annotation → coerced to String. + PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/score", 95); + executeSuccessResponse(execCtx, replaceOp, annotations); - assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - // Property should NOT be set since value is null. - assertFalse(execCtx.getProperties().containsKey("riskScore")); + assertEquals(execCtx.getProperties().get("score"), "95"); } @Test - public void testPropertyAddArrayAnnotationSingleValueWrapped() - throws ActionExecutionResponseProcessorException { + public void testPropertyReplaceNullValue() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); - Map annotations = new HashMap<>(); - annotations.put("/properties/tags", ""); + execCtx.setProperty("score", "50"); - // Single value with [] annotation → wrapped in a list. - PerformableOperation addOp = createOperation(Operation.ADD, "/properties/tags", "singleTag"); - executeSuccessResponse(execCtx, addOp, annotations); + PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/score", null); + ActionExecutionStatus status = executeSuccessResponse( + execCtx, replaceOp, Collections.emptyMap()); - Object stored = execCtx.getProperties().get("tags"); - assertTrue(stored instanceof List); - assertEquals(((List) stored).size(), 1); - assertEquals(((List) stored).get(0), "singleTag"); + // Null value should fail for REPLACE. + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + // Original value should remain. + assertEquals(execCtx.getProperties().get("score"), "50"); } - // ========================= processSuccessResponse — Property REPLACE ========================= - @Test - public void testPropertyReplaceFlatExists() throws ActionExecutionResponseProcessorException { + public void testPropertyReplaceEmptyPropertyName() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); - execCtx.setProperty("riskScore", "50"); - PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/riskScore", "80"); - ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, Collections.emptyMap()); + PerformableOperation op = createOperation(Operation.REPLACE, "/properties/", "value"); + ActionExecutionStatus status = executeSuccessResponse(execCtx, op, Collections.emptyMap()); + // Empty property name should fail. assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - assertEquals(execCtx.getProperties().get("riskScore"), "80"); } @Test - public void testPropertyReplaceFlatDoesNotExist() throws ActionExecutionResponseProcessorException { + public void testPropertyReplaceComplexObjectInvalidSchema() + throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); - // riskScore not set — REPLACE should fail. + Map annotations = new HashMap<>(); + annotations.put("/properties/data", "risk: Float, factor: String"); - PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/riskScore", "80"); - ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, Collections.emptyMap()); + // Value has an unknown attribute not in the schema. + Map value = new HashMap<>(); + value.put("risk", 0.85); + value.put("unknown", "bad"); - // Overall status is SUCCESS but the property should not be set. + PerformableOperation op = createOperation(Operation.REPLACE, "/properties/data", value); + ActionExecutionStatus status = executeSuccessResponse(execCtx, op, annotations); + + // Should succeed overall (logged as failure for this operation) but property not set. assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - assertFalse(execCtx.getProperties().containsKey("riskScore")); + assertNull(execCtx.getProperties().get("data")); } @Test - public void testPropertyReplaceNestedExists() throws ActionExecutionResponseProcessorException { + public void testPropertyReplaceArrayExceedsItemLimit() + throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); - Map riskMap = new HashMap<>(); - riskMap.put("score", "50"); - execCtx.setProperty("risk", riskMap); + Map annotations = new HashMap<>(); + annotations.put("/properties/tags", "[String]"); - PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/risk/score", "80"); - ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, Collections.emptyMap()); + // Create a list with 11 items (exceeds max 10). + List bigList = new ArrayList<>(); + for (int i = 0; i < 11; i++) { + bigList.add("item" + i); + } + PerformableOperation op = createOperation(Operation.REPLACE, "/properties/tags", bigList); + ActionExecutionStatus status = executeSuccessResponse(execCtx, op, annotations); + + // Should succeed overall but property not set due to array limit. assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - @SuppressWarnings("unchecked") - Map updatedRisk = (Map) execCtx.getProperties().get("risk"); - assertEquals(updatedRisk.get("score"), "80"); + assertNull(execCtx.getProperties().get("tags")); } @Test - public void testPropertyReplaceNestedParentMissing() throws ActionExecutionResponseProcessorException { + public void testPropertyReplaceComplexArrayExceedsItemLimit() + throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); - // No "risk" property set. + Map annotations = new HashMap<>(); + annotations.put("/properties/risks", "[risk: Float]"); - PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/risk/score", "80"); - ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, Collections.emptyMap()); + List> items = new ArrayList<>(); + for (int i = 0; i < 11; i++) { + Map item = new HashMap<>(); + item.put("risk", (float) i); + items.add(item); + } - // Should fail — nested path doesn't exist. + PerformableOperation op = createOperation(Operation.REPLACE, "/properties/risks", items); + ActionExecutionStatus status = executeSuccessResponse(execCtx, op, annotations); + + // Should succeed overall but property not set due to item limit on complex array. assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - assertFalse(execCtx.getProperties().containsKey("risk")); + assertNull(execCtx.getProperties().get("risks")); } - // ========================= processSuccessResponse — Property REMOVE ========================= + // ========================= processSuccessResponse — User claim REPLACE ========================= @Test - public void testPropertyRemoveFlat() throws ActionExecutionResponseProcessorException { + public void testUserClaimReplace() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); - execCtx.setProperty("riskScore", "50"); + execCtx.getFlowUser().addClaim("http://wso2.org/claims/email", "old@example.com"); - PerformableOperation removeOp = createOperation(Operation.REMOVE, "/properties/riskScore", null); - executeSuccessResponse(execCtx, removeOp, Collections.emptyMap()); + PerformableOperation claimOp = createOperation( + Operation.REPLACE, "/user/claims/http://wso2.org/claims/email", "new@example.com"); + executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); - assertFalse(execCtx.getProperties().containsKey("riskScore")); + assertEquals(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/email"), "new@example.com"); } @Test - public void testPropertyRemoveNested() throws ActionExecutionResponseProcessorException { + public void testUserClaimReplaceCreatesNewClaim() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); - Map riskMap = new HashMap<>(); - riskMap.put("score", "50"); - riskMap.put("level", "MEDIUM"); - execCtx.setProperty("risk", riskMap); - PerformableOperation removeOp = createOperation(Operation.REMOVE, "/properties/risk/score", null); - executeSuccessResponse(execCtx, removeOp, Collections.emptyMap()); + PerformableOperation claimOp = createOperation( + Operation.REPLACE, "/user/claims/http://wso2.org/claims/country", "US"); + executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); - @SuppressWarnings("unchecked") - Map updatedRisk = (Map) execCtx.getProperties().get("risk"); - assertFalse(updatedRisk.containsKey("score")); - assertTrue(updatedRisk.containsKey("level")); + assertEquals(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/country"), "US"); } @Test - public void testPropertyRemoveNestedParentMissing() throws ActionExecutionResponseProcessorException { + public void testUserClaimReplaceStringifiesValue() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); - // No "risk" property — remove on nested path should be graceful (no error). - PerformableOperation removeOp = createOperation(Operation.REMOVE, "/properties/risk/score", null); - ActionExecutionStatus status = executeSuccessResponse(execCtx, removeOp, Collections.emptyMap()); - assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - } + // Numeric value should be stringified. + PerformableOperation claimOp = createOperation( + Operation.REPLACE, "/user/claims/http://wso2.org/claims/country", 42); + executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); - // ========================= processSuccessResponse — User claim operations ========================= + assertEquals(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/country"), "42"); + } @Test - public void testUserClaimAdd() throws ActionExecutionResponseProcessorException { + public void testUserClaimReplaceIdentityClaimRejected() + throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); + // Identity claim should be rejected. PerformableOperation claimOp = createOperation( - Operation.ADD, "/user/claims/http://wso2.org/claims/country", "US"); - executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); + Operation.REPLACE, "/user/claims/http://wso2.org/claims/identity/accountLocked", "true"); + ActionExecutionStatus status = executeSuccessResponse( + execCtx, claimOp, Collections.emptyMap()); - assertEquals(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/country"), "US"); + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + // Claim should NOT be set. + assertNull(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/identity/accountLocked")); } @Test - public void testUserClaimReplace() throws ActionExecutionResponseProcessorException { + public void testUserClaimReplaceNonExistentClaimRejected() + throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); - execCtx.getFlowUser().addClaim("http://wso2.org/claims/email", "old@example.com"); + // Claim not in the mocked local claim list should be rejected. PerformableOperation claimOp = createOperation( - Operation.REPLACE, "/user/claims/http://wso2.org/claims/email", "new@example.com"); - executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); + Operation.REPLACE, + "/user/claims/http://wso2.org/claims/nonexistent", "value"); + ActionExecutionStatus status = executeSuccessResponse( + execCtx, claimOp, Collections.emptyMap()); - assertEquals(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/email"), "new@example.com"); + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + assertNull(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/nonexistent")); } @Test - public void testUserClaimRemove() throws ActionExecutionResponseProcessorException { + public void testUserClaimReplaceNonLocalDialectRejected() + throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); - execCtx.getFlowUser().addClaim("http://wso2.org/claims/email", "test@example.com"); + // Non-local dialect claim should be rejected. PerformableOperation claimOp = createOperation( - Operation.REMOVE, "/user/claims/http://wso2.org/claims/email", null); - executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); + Operation.REPLACE, + "/user/claims/urn:ietf:params:scim:schemas:core:2.0:User:name.givenName", "John"); + ActionExecutionStatus status = executeSuccessResponse( + execCtx, claimOp, Collections.emptyMap()); - assertFalse(execCtx.getFlowUser().getClaims().containsKey("http://wso2.org/claims/email")); + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + assertNull(execCtx.getFlowUser().getClaims().get( + "urn:ietf:params:scim:schemas:core:2.0:User:name.givenName")); } @Test - public void testUserClaimAddNullValue() throws ActionExecutionResponseProcessorException { + public void testUserClaimReplaceNullValue() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); PerformableOperation claimOp = createOperation( - Operation.ADD, "/user/claims/http://wso2.org/claims/email", null); - ActionExecutionStatus status = executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); + Operation.REPLACE, "/user/claims/http://wso2.org/claims/email", null); + ActionExecutionStatus status = executeSuccessResponse( + execCtx, claimOp, Collections.emptyMap()); // Operation should fail — null value. assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); @@ -379,49 +423,51 @@ public void testUserClaimAddNullValue() throws ActionExecutionResponseProcessorE } @Test - public void testUserClaimAddEmptyClaimUri() throws ActionExecutionResponseProcessorException { + public void testUserClaimReplaceEmptyClaimUri() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); - PerformableOperation claimOp = createOperation(Operation.ADD, "/user/claims/", "value"); - ActionExecutionStatus status = executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); + PerformableOperation claimOp = createOperation(Operation.REPLACE, "/user/claims/", "value"); + ActionExecutionStatus status = executeSuccessResponse( + execCtx, claimOp, Collections.emptyMap()); // Should fail — empty claim URI. assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); } @Test - public void testUserClaimAddNoFlowUser() throws ActionExecutionResponseProcessorException { + public void testUserClaimReplaceNoFlowUser() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); execCtx.setFlowUser(null); PerformableOperation claimOp = createOperation( - Operation.ADD, "/user/claims/http://wso2.org/claims/email", "test@email.com"); - ActionExecutionStatus status = executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); + Operation.REPLACE, "/user/claims/http://wso2.org/claims/email", "test@email.com"); + ActionExecutionStatus status = executeSuccessResponse( + execCtx, claimOp, Collections.emptyMap()); // Should fail — no FlowUser. assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); } - // ========================= processSuccessResponse — User input operations ========================= + // ========================= processSuccessResponse — User input REPLACE ========================= @Test - public void testUserInputAdd() throws ActionExecutionResponseProcessorException { + public void testUserInputReplace() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); + execCtx.addUserInputData("consent", "false"); - PerformableOperation inputOp = createOperation(Operation.ADD, "/input/consent", "true"); + PerformableOperation inputOp = createOperation(Operation.REPLACE, "/input/consent", "true"); executeSuccessResponse(execCtx, inputOp, Collections.emptyMap()); assertEquals(execCtx.getUserInputData().get("consent"), "true"); } @Test - public void testUserInputReplace() throws ActionExecutionResponseProcessorException { + public void testUserInputReplaceCreatesNew() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); - execCtx.addUserInputData("consent", "false"); PerformableOperation inputOp = createOperation(Operation.REPLACE, "/input/consent", "true"); executeSuccessResponse(execCtx, inputOp, Collections.emptyMap()); @@ -430,15 +476,19 @@ public void testUserInputReplace() throws ActionExecutionResponseProcessorExcept } @Test - public void testUserInputRemove() throws ActionExecutionResponseProcessorException { + public void testUserInputReplaceNullValue() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); execCtx.addUserInputData("consent", "true"); - PerformableOperation inputOp = createOperation(Operation.REMOVE, "/input/consent", null); - executeSuccessResponse(execCtx, inputOp, Collections.emptyMap()); + PerformableOperation inputOp = createOperation(Operation.REPLACE, "/input/consent", null); + ActionExecutionStatus status = executeSuccessResponse( + execCtx, inputOp, Collections.emptyMap()); - assertFalse(execCtx.getUserInputData().containsKey("consent")); + // Null value should fail. + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + // Original value should remain. + assertEquals(execCtx.getUserInputData().get("consent"), "true"); } // ========================= processSuccessResponse — Read-only paths ========================= @@ -448,7 +498,7 @@ public void testReadOnlyFlowPath() throws ActionExecutionResponseProcessorExcept FlowExecutionContext execCtx = createFlowExecutionContext(); - PerformableOperation op = createOperation(Operation.ADD, "/flow/tenantDomain", "newValue"); + PerformableOperation op = createOperation(Operation.REPLACE, "/flow/tenantDomain", "newValue"); ActionExecutionStatus status = executeSuccessResponse(execCtx, op, Collections.emptyMap()); // Operation should fail but overall status is SUCCESS. @@ -460,7 +510,7 @@ public void testReadOnlyGraphPath() throws ActionExecutionResponseProcessorExcep FlowExecutionContext execCtx = createFlowExecutionContext(); - PerformableOperation op = createOperation(Operation.ADD, "/graph/currentNode/id", "newId"); + PerformableOperation op = createOperation(Operation.REPLACE, "/graph/currentNode/id", "newId"); ActionExecutionStatus status = executeSuccessResponse(execCtx, op, Collections.emptyMap()); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); @@ -473,7 +523,7 @@ public void testUnknownPathPrefix() throws ActionExecutionResponseProcessorExcep FlowExecutionContext execCtx = createFlowExecutionContext(); - PerformableOperation op = createOperation(Operation.ADD, "/unknown/path", "value"); + PerformableOperation op = createOperation(Operation.REPLACE, "/unknown/path", "value"); ActionExecutionStatus status = executeSuccessResponse(execCtx, op, Collections.emptyMap()); // Unknown path → operation fails, but overall status is SUCCESS. @@ -488,7 +538,7 @@ public void testLegacyUserInputsPathNormalized() throws ActionExecutionResponseP FlowExecutionContext execCtx = createFlowExecutionContext(); // Legacy /userInputs/ path should be normalized to /input/. - PerformableOperation op = createOperation(Operation.ADD, "/userInputs/legacyField", "value"); + PerformableOperation op = createOperation(Operation.REPLACE, "/userInputs/legacyField", "value"); executeSuccessResponse(execCtx, op, Collections.emptyMap()); assertEquals(execCtx.getUserInputData().get("legacyField"), "value"); @@ -503,9 +553,9 @@ public void testMultipleOperationsMixedResults() throws ActionExecutionResponseP execCtx.setProperty("existingProp", "old"); List operations = new ArrayList<>(); - operations.add(createOperation(Operation.ADD, "/properties/newProp", "newValue")); + operations.add(createOperation(Operation.REPLACE, "/properties/newProp", "newValue")); operations.add(createOperation(Operation.REPLACE, "/properties/existingProp", "updated")); - operations.add(createOperation(Operation.ADD, "/flow/readonly", "fail")); // This should fail. + operations.add(createOperation(Operation.REPLACE, "/flow/readonly", "fail")); // This should fail. ActionExecutionStatus status = executeSuccessResponse(execCtx, operations, Collections.emptyMap()); @@ -575,55 +625,6 @@ public void testProcessErrorResponse() throws ActionExecutionResponseProcessorEx assertEquals(status.getResponse().getErrorDescription(), "Database connection failed"); } - // ========================= processSuccessResponse — Invalid property path ========================= - - @Test - public void testPropertyAddEmptyPropertyName() throws ActionExecutionResponseProcessorException { - - FlowExecutionContext execCtx = createFlowExecutionContext(); - - PerformableOperation op = createOperation(Operation.ADD, "/properties/", "value"); - ActionExecutionStatus status = executeSuccessResponse(execCtx, op, Collections.emptyMap()); - - assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - } - - // ========================= processSuccessResponse — Three-level nesting ========================= - - @Test - public void testPropertyAddThreeLevelNesting() throws ActionExecutionResponseProcessorException { - - FlowExecutionContext execCtx = createFlowExecutionContext(); - - PerformableOperation addOp = createOperation( - Operation.ADD, "/properties/deep/nested/value", "deepValue"); - executeSuccessResponse(execCtx, addOp, Collections.emptyMap()); - - @SuppressWarnings("unchecked") - Map deep = (Map) execCtx.getProperties().get("deep"); - assertNotNull(deep); - @SuppressWarnings("unchecked") - Map nested = (Map) deep.get("nested"); - assertNotNull(nested); - assertEquals(nested.get("value"), "deepValue"); - } - - @Test - public void testPropertyReplaceNullValue() throws ActionExecutionResponseProcessorException { - - FlowExecutionContext execCtx = createFlowExecutionContext(); - execCtx.setProperty("score", "50"); - - PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/score", null); - ActionExecutionStatus status = executeSuccessResponse( - execCtx, replaceOp, Collections.emptyMap()); - - // Null value should fail for REPLACE. - assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - // Original value should remain. - assertEquals(execCtx.getProperties().get("score"), "50"); - } - // ========================= Helper methods ========================= private FlowExecutionContext createFlowExecutionContext() { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/PathTypeAnnotationUtilTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/PathTypeAnnotationUtilTest.java new file mode 100644 index 000000000000..8f176fe058a8 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/PathTypeAnnotationUtilTest.java @@ -0,0 +1,599 @@ +/* + * Copyright (c) 2025, 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.executor; + +import org.testng.annotations.Test; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.PathTypeAnnotationUtil; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +/** + * Unit tests for {@link PathTypeAnnotationUtil}. + */ +public class PathTypeAnnotationUtilTest { + + // ========================= stripAnnotation ========================= + + @Test + public void testStripAnnotationPrimaryType() { + + String[] result = PathTypeAnnotationUtil.stripAnnotation("/properties/riskFactor{String}"); + assertEquals(result[0], "/properties/riskFactor"); + assertEquals(result[1], "String"); + } + + @Test + public void testStripAnnotationMultivaluedPrimary() { + + String[] result = PathTypeAnnotationUtil.stripAnnotation("/properties/riskFactors{[String]}"); + assertEquals(result[0], "/properties/riskFactors"); + assertEquals(result[1], "[String]"); + } + + @Test + public void testStripAnnotationComplexObject() { + + String[] result = PathTypeAnnotationUtil.stripAnnotation( + "/properties/risk{risk: Float, factor: String}"); + assertEquals(result[0], "/properties/risk"); + assertEquals(result[1], "risk: Float, factor: String"); + } + + @Test + public void testStripAnnotationMultivaluedComplex() { + + String[] result = PathTypeAnnotationUtil.stripAnnotation( + "/properties/risks{[risk: Float, factor: String]}"); + assertEquals(result[0], "/properties/risks"); + assertEquals(result[1], "[risk: Float, factor: String]"); + } + + @Test + public void testStripAnnotationNoAnnotation() { + + String[] result = PathTypeAnnotationUtil.stripAnnotation("/properties/riskScore"); + assertEquals(result[0], "/properties/riskScore"); + assertNull(result[1]); + } + + @Test + public void testStripAnnotationNullInput() { + + String[] result = PathTypeAnnotationUtil.stripAnnotation(null); + assertNull(result[0]); + assertNull(result[1]); + } + + @Test + public void testStripAnnotationEmptyBraces() { + + String[] result = PathTypeAnnotationUtil.stripAnnotation("/properties/field{}"); + assertEquals(result[0], "/properties/field"); + assertEquals(result[1], ""); + } + + @Test + public void testStripAnnotationIntegerType() { + + String[] result = PathTypeAnnotationUtil.stripAnnotation("/properties/count{Integer}"); + assertEquals(result[0], "/properties/count"); + assertEquals(result[1], "Integer"); + } + + // ========================= coerceValue ========================= + + @Test + public void testCoerceValueNoAnnotation() { + + Object result = PathTypeAnnotationUtil.coerceValue( + "/properties/score", 42, Collections.emptyMap()); + assertEquals(result, "42"); + } + + @Test + public void testCoerceValuePrimaryTypeAnnotation() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/score", "Integer"); + + Object result = PathTypeAnnotationUtil.coerceValue("/properties/score", 95, annotations); + assertEquals(result, "95"); + } + + @Test + public void testCoerceValueMultivaluedPrimaryList() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/tags", "[String]"); + + List input = Arrays.asList("tag1", "tag2", "tag3"); + Object result = PathTypeAnnotationUtil.coerceValue("/properties/tags", input, annotations); + + assertTrue(result instanceof List); + @SuppressWarnings("unchecked") + List list = (List) result; + assertEquals(list.size(), 3); + assertEquals(list.get(0), "tag1"); + } + + @Test + public void testCoerceValueMultivaluedPrimarySingleValue() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/tags", "[String]"); + + Object result = PathTypeAnnotationUtil.coerceValue("/properties/tags", "singleTag", annotations); + + assertTrue(result instanceof List); + @SuppressWarnings("unchecked") + List list = (List) result; + assertEquals(list.size(), 1); + assertEquals(list.get(0), "singleTag"); + } + + @Test + public void testCoerceValueComplexObjectAnnotation() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/risk", "risk: Float, factor: String"); + + Map complexValue = new HashMap<>(); + complexValue.put("risk", 0.85); + complexValue.put("factor", "ip_mismatch"); + + Object result = PathTypeAnnotationUtil.coerceValue("/properties/risk", complexValue, annotations); + // Complex annotation — passed through as-is. + assertTrue(result instanceof Map); + assertEquals(result, complexValue); + } + + @Test + public void testCoerceValueComplexArrayAnnotation() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/risks", "[risk: Float, factor: String]"); + + List> items = Arrays.asList( + new HashMap() {{ put("risk", 0.5); put("factor", "a"); }}, + new HashMap() {{ put("risk", 0.8); put("factor", "b"); }} + ); + + Object result = PathTypeAnnotationUtil.coerceValue("/properties/risks", items, annotations); + // Complex array annotation — passed through as-is. + assertTrue(result instanceof List); + assertEquals(result, items); + } + + @Test + public void testCoerceValueBooleanPrimaryType() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/active", "Boolean"); + + Object result = PathTypeAnnotationUtil.coerceValue("/properties/active", true, annotations); + assertEquals(result, "true"); + } + + @Test + public void testCoerceValueStringValue() { + + Object result = PathTypeAnnotationUtil.coerceValue( + "/properties/name", "test", Collections.emptyMap()); + assertEquals(result, "test"); + } + + // ========================= Constants ========================= + + @Test + public void testLocalClaimDialectPrefix() { + + // Verify the identity claim URI is a sub-prefix of the local claim dialect. + assertTrue("http://wso2.org/claims/identity/".startsWith("http://wso2.org/claims/")); + } + + @Test + public void testIdentityClaimUriPrefix() { + + // Verify identity claim prefix is distinct from the general local claim prefix. + assertTrue("http://wso2.org/claims/identity/".length() > "http://wso2.org/claims/".length()); + } + + // ========================= validateAnnotationLimits ========================= + + @Test + public void testValidateAnnotationLimitsNull() { + + assertTrue(PathTypeAnnotationUtil.validateAnnotationLimits(null)); + } + + @Test + public void testValidateAnnotationLimitsEmpty() { + + assertTrue(PathTypeAnnotationUtil.validateAnnotationLimits("")); + } + + @Test + public void testValidateAnnotationLimitsPrimaryType() { + + assertTrue(PathTypeAnnotationUtil.validateAnnotationLimits("String")); + } + + @Test + public void testValidateAnnotationLimitsPrimaryArray() { + + assertTrue(PathTypeAnnotationUtil.validateAnnotationLimits("[String]")); + } + + @Test + public void testValidateAnnotationLimitsComplexWithinLimit() { + + assertTrue(PathTypeAnnotationUtil.validateAnnotationLimits("risk: Float, factor: String")); + } + + @Test + public void testValidateAnnotationLimitsComplexArrayWithinLimit() { + + assertTrue(PathTypeAnnotationUtil.validateAnnotationLimits("[risk: Float, factor: String]")); + } + + @Test + public void testValidateAnnotationLimitsExceedsMax() { + + // Build an annotation with 11 attributes (exceeds MAX_ATTRIBUTES_PER_OBJECT = 10). + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 11; i++) { + if (i > 0) sb.append(", "); + sb.append("attr").append(i).append(": String"); + } + assertFalse(PathTypeAnnotationUtil.validateAnnotationLimits(sb.toString())); + } + + @Test + public void testValidateAnnotationLimitsExactlyAtMax() { + + // Build an annotation with exactly 10 attributes. + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; i++) { + if (i > 0) sb.append(", "); + sb.append("attr").append(i).append(": String"); + } + assertTrue(PathTypeAnnotationUtil.validateAnnotationLimits(sb.toString())); + } + + // ========================= validateValueAgainstAnnotation ========================= + + @Test + public void testValidateValueNoAnnotation() { + + assertTrue(PathTypeAnnotationUtil.validateValueAgainstAnnotation( + "/properties/score", 42, Collections.emptyMap())); + } + + @Test + public void testValidateValuePrimaryAnnotation() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/score", "Integer"); + assertTrue(PathTypeAnnotationUtil.validateValueAgainstAnnotation( + "/properties/score", 42, annotations)); + } + + @Test + public void testValidateValueComplexObjectValid() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/risk", "risk: Float, factor: String"); + + Map value = new HashMap<>(); + value.put("risk", 0.85); + value.put("factor", "ip_mismatch"); + + assertTrue(PathTypeAnnotationUtil.validateValueAgainstAnnotation( + "/properties/risk", value, annotations)); + } + + @Test + public void testValidateValueComplexObjectUnknownAttribute() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/risk", "risk: Float, factor: String"); + + Map value = new HashMap<>(); + value.put("risk", 0.85); + value.put("unknown", "bad"); + + assertFalse(PathTypeAnnotationUtil.validateValueAgainstAnnotation( + "/properties/risk", value, annotations)); + } + + @Test + public void testValidateValueComplexObjectNotAMap() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/risk", "risk: Float, factor: String"); + + assertFalse(PathTypeAnnotationUtil.validateValueAgainstAnnotation( + "/properties/risk", "not a map", annotations)); + } + + @Test + public void testValidateValueComplexObjectNestedMap() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/risk", "risk: Float, factor: String"); + + Map nested = new HashMap<>(); + nested.put("deep", "value"); + + Map value = new HashMap<>(); + value.put("risk", 0.85); + value.put("factor", nested); + + assertFalse(PathTypeAnnotationUtil.validateValueAgainstAnnotation( + "/properties/risk", value, annotations)); + } + + @Test + public void testValidateValueComplexArrayValid() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/risks", "[risk: Float, factor: String]"); + + List> items = Arrays.asList( + new HashMap() {{ put("risk", 0.5); put("factor", "a"); }}, + new HashMap() {{ put("risk", 0.8); put("factor", "b"); }} + ); + + assertTrue(PathTypeAnnotationUtil.validateValueAgainstAnnotation( + "/properties/risks", items, annotations)); + } + + @Test + public void testValidateValueComplexArrayExceedsItemLimit() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/risks", "[risk: Float]"); + + List> items = new java.util.ArrayList<>(); + for (int i = 0; i < 11; i++) { + Map item = new HashMap<>(); + item.put("risk", (float) i); + items.add(item); + } + + assertFalse(PathTypeAnnotationUtil.validateValueAgainstAnnotation( + "/properties/risks", items, annotations)); + } + + @Test + public void testValidateValueComplexObjectWithArrayAttribute() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/data", "tags: String[], score: Float"); + + Map value = new HashMap<>(); + value.put("tags", Arrays.asList("a", "b", "c")); + value.put("score", 0.9); + + assertTrue(PathTypeAnnotationUtil.validateValueAgainstAnnotation( + "/properties/data", value, annotations)); + } + + @Test + public void testValidateValueComplexObjectArrayAttrExceedsLimit() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/data", "tags: String[]"); + + List bigList = new java.util.ArrayList<>(); + for (int i = 0; i < 11; i++) { + bigList.add("item" + i); + } + + Map value = new HashMap<>(); + value.put("tags", bigList); + + assertFalse(PathTypeAnnotationUtil.validateValueAgainstAnnotation( + "/properties/data", value, annotations)); + } + + // ========================= enforceArrayItemLimit ========================= + + @Test + public void testEnforceArrayItemLimitNoAnnotation() { + + assertTrue(PathTypeAnnotationUtil.enforceArrayItemLimit( + "/properties/score", Arrays.asList(1, 2, 3), Collections.emptyMap())); + } + + @Test + public void testEnforceArrayItemLimitWithinLimit() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/tags", "[String]"); + + assertTrue(PathTypeAnnotationUtil.enforceArrayItemLimit( + "/properties/tags", Arrays.asList("a", "b", "c"), annotations)); + } + + @Test + public void testEnforceArrayItemLimitExceedsLimit() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/tags", "[String]"); + + List bigList = new java.util.ArrayList<>(); + for (int i = 0; i < 11; i++) { + bigList.add("item" + i); + } + + assertFalse(PathTypeAnnotationUtil.enforceArrayItemLimit( + "/properties/tags", bigList, annotations)); + } + + @Test + public void testEnforceArrayItemLimitNonArrayAnnotation() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/score", "Integer"); + + assertTrue(PathTypeAnnotationUtil.enforceArrayItemLimit( + "/properties/score", 42, annotations)); + } + + @Test + public void testEnforceArrayItemLimitNonListValue() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/tags", "[String]"); + + assertTrue(PathTypeAnnotationUtil.enforceArrayItemLimit( + "/properties/tags", "singleValue", annotations)); + } + + // ========================= JSON string parsing ========================= + + @Test + public void testValidateValueComplexArrayFromJsonString() + throws Exception { + + // This is the exact failing case: after JWE decryption the value is a JSON string. + Map annotations = new HashMap<>(); + annotations.put("/properties/riskFactors", "[factor: String, is-critical: Boolean]"); + + String jsonString = "[{\"factor\":\"no_risk_factors_detected\",\"is-critical\":false}]"; + + assertTrue(PathTypeAnnotationUtil.validateValueAgainstAnnotation( + "/properties/riskFactors", jsonString, annotations)); + } + + @Test + public void testValidateValueComplexArrayFromJsonStringMultipleItems() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/risks", "[risk: Float, factor: String]"); + + String jsonString = "[{\"risk\":0.5,\"factor\":\"ip_mismatch\"}" + + ",{\"risk\":0.8,\"factor\":\"high_risk_email\"}]"; + + assertTrue(PathTypeAnnotationUtil.validateValueAgainstAnnotation( + "/properties/risks", jsonString, annotations)); + } + + @Test + public void testValidateValueComplexObjectFromJsonString() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/risk", "risk: Float, factor: String"); + + String jsonString = "{\"risk\":0.85,\"factor\":\"ip_mismatch\"}"; + + assertTrue(PathTypeAnnotationUtil.validateValueAgainstAnnotation( + "/properties/risk", jsonString, annotations)); + } + + @Test + public void testValidateValueComplexArrayFromJsonStringUnknownAttribute() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/risks", "[risk: Float, factor: String]"); + + // JSON string with an attribute not in the schema. + String jsonString = "[{\"risk\":0.5,\"unknown\":\"bad\"}]"; + + assertFalse(PathTypeAnnotationUtil.validateValueAgainstAnnotation( + "/properties/risks", jsonString, annotations)); + } + + @Test + public void testCoerceValueComplexArrayFromJsonString() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/riskFactors", "[factor: String, is-critical: Boolean]"); + + String jsonString = "[{\"factor\":\"no_risk_factors_detected\",\"is-critical\":false}]"; + + Object result = PathTypeAnnotationUtil.coerceValue( + "/properties/riskFactors", jsonString, annotations); + + // Should be parsed into a List, not returned as a plain string. + assertTrue(result instanceof List); + @SuppressWarnings("unchecked") + List list = (List) result; + assertEquals(list.size(), 1); + assertTrue(list.get(0) instanceof Map); + } + + @Test + public void testCoerceValueMultivaluedPrimaryFromJsonString() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/tags", "[String]"); + + String jsonString = "[\"tag1\",\"tag2\",\"tag3\"]"; + + Object result = PathTypeAnnotationUtil.coerceValue( + "/properties/tags", jsonString, annotations); + + assertTrue(result instanceof List); + @SuppressWarnings("unchecked") + List list = (List) result; + assertEquals(list.size(), 3); + assertEquals(list.get(0), "tag1"); + } + + @Test + public void testCoerceValueComplexObjectFromJsonString() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/risk", "risk: Float, factor: String"); + + String jsonString = "{\"risk\":0.85,\"factor\":\"ip_mismatch\"}"; + + Object result = PathTypeAnnotationUtil.coerceValue( + "/properties/risk", jsonString, annotations); + + // Should be parsed into a Map, not returned as a plain string. + assertTrue(result instanceof Map); + @SuppressWarnings("unchecked") + Map map = (Map) result; + assertEquals(map.get("factor"), "ip_mismatch"); + } + + @Test + public void testCoerceValueNonJsonStringIsNotParsed() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/score", "Integer"); + + // A plain string with no JSON structure should be coerced to String normally. + Object result = PathTypeAnnotationUtil.coerceValue( + "/properties/score", "42", annotations); + assertEquals(result, "42"); + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/AccessConfigTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/AccessConfigTest.java new file mode 100644 index 000000000000..92dc6bc0651a --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/AccessConfigTest.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2025, 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.model; + +import org.testng.annotations.Test; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +/** + * Unit tests for {@link AccessConfig} and {@link ContextPath}. + */ +public class AccessConfigTest { + + @Test + public void testAccessConfigWithNullLists() { + + AccessConfig config = new AccessConfig(null, null); + assertNull(config.getExpose()); + assertNull(config.getModify()); + assertNull(config.getExposePaths()); + assertNull(config.getModifyPaths()); + } + + @Test + public void testAccessConfigWithEmptyLists() { + + AccessConfig config = new AccessConfig(Collections.emptyList(), Collections.emptyList()); + assertNotNull(config.getExpose()); + assertTrue(config.getExpose().isEmpty()); + assertNotNull(config.getModify()); + assertTrue(config.getModify().isEmpty()); + assertNotNull(config.getExposePaths()); + assertTrue(config.getExposePaths().isEmpty()); + assertNotNull(config.getModifyPaths()); + assertTrue(config.getModifyPaths().isEmpty()); + } + + @Test + public void testGetExposePaths() { + + List exposePaths = Arrays.asList( + new ContextPath("/user/claims/", true), + new ContextPath("/user/credentials/", false) + ); + AccessConfig config = new AccessConfig(exposePaths, null); + + List paths = config.getExposePaths(); + assertEquals(paths.size(), 2); + assertEquals(paths.get(0), "/user/claims/"); + assertEquals(paths.get(1), "/user/credentials/"); + } + + @Test + public void testGetModifyPaths() { + + List modifyPaths = Arrays.asList( + new ContextPath("/properties/riskScore", false), + new ContextPath("/user/claims/", true) + ); + AccessConfig config = new AccessConfig(null, modifyPaths); + + List paths = config.getModifyPaths(); + assertEquals(paths.size(), 2); + assertEquals(paths.get(0), "/properties/riskScore"); + assertEquals(paths.get(1), "/user/claims/"); + } + + @Test + public void testIsExposePathEncryptedMatchesLongestPrefix() { + + List exposePaths = Arrays.asList( + new ContextPath("/user/", false), + new ContextPath("/user/claims/", true) + ); + AccessConfig config = new AccessConfig(exposePaths, null); + + assertTrue(config.isExposePathEncrypted("/user/claims/email")); + assertFalse(config.isExposePathEncrypted("/user/username")); + } + + @Test + public void testIsExposePathEncryptedNoMatch() { + + List exposePaths = Collections.singletonList( + new ContextPath("/user/claims/", true) + ); + AccessConfig config = new AccessConfig(exposePaths, null); + + assertFalse(config.isExposePathEncrypted("/properties/riskScore")); + } + + @Test + public void testIsExposePathEncryptedWithNullExpose() { + + AccessConfig config = new AccessConfig(null, null); + assertFalse(config.isExposePathEncrypted("/user/claims/email")); + } + + @Test + public void testIsModifyPathEncryptedMatchesLongestPrefix() { + + List modifyPaths = Arrays.asList( + new ContextPath("/user/", false), + new ContextPath("/user/credentials/", true) + ); + AccessConfig config = new AccessConfig(null, modifyPaths); + + assertTrue(config.isModifyPathEncrypted("/user/credentials/password")); + assertFalse(config.isModifyPathEncrypted("/user/username")); + } + + @Test + public void testIsModifyPathEncryptedWithNullModify() { + + AccessConfig config = new AccessConfig(null, null); + assertFalse(config.isModifyPathEncrypted("/user/credentials/password")); + } + + @Test + public void testIsModifyPathEncryptedWithAnnotatedPaths() { + + List modifyPaths = Arrays.asList( + new ContextPath("/properties/risk{risk: Float, factor: String}", true), + new ContextPath("/properties/tags{[String]}", false) + ); + AccessConfig config = new AccessConfig(null, modifyPaths); + + // Clean operation path should match annotated modify path after stripping. + assertTrue(config.isModifyPathEncrypted("/properties/risk")); + assertFalse(config.isModifyPathEncrypted("/properties/tags")); + assertFalse(config.isModifyPathEncrypted("/properties/unknown")); + } + + @Test + public void testContextPathGetters() { + + ContextPath path = new ContextPath("/user/claims/email", true); + assertEquals(path.getPath(), "/user/claims/email"); + assertTrue(path.isEncrypted()); + + ContextPath unencrypted = new ContextPath("/properties/", false); + assertEquals(unencrypted.getPath(), "/properties/"); + assertFalse(unencrypted.isEncrypted()); + } + + @Test + public void testExposeListIsUnmodifiable() { + + List exposePaths = Arrays.asList( + new ContextPath("/user/claims/", true) + ); + AccessConfig config = new AccessConfig(exposePaths, null); + + try { + config.getExpose().add(new ContextPath("/hack/", false)); + // If no exception thrown, fail the test + assertTrue(false, "Expected UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // Expected + } + } + + @Test + public void testModifyListIsUnmodifiable() { + + List modifyPaths = Arrays.asList( + new ContextPath("/user/claims/", true) + ); + AccessConfig config = new AccessConfig(null, modifyPaths); + + try { + config.getModify().add(new ContextPath("/hack/", false)); + assertTrue(false, "Expected UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // Expected + } + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java new file mode 100644 index 000000000000..dab31deca53c --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2025, 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.model; + +import org.testng.annotations.Test; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionEvent; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +/** + * Unit tests for {@link InFlowExtensionEvent}. + */ +public class InFlowExtensionEventTest { + + @Test + public void testBuilderWithAllFields() { + + Map userInputs = new HashMap<>(); + userInputs.put("email", "test@example.com"); + + Map flowProperties = new HashMap<>(); + flowProperties.put("riskScore", 85); + + InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() + .flowType("REGISTRATION") + .currentNodeId("node-1") + .userInputs(userInputs) + .flowProperties(flowProperties) + .build(); + + assertEquals(event.getFlowType(), "REGISTRATION"); + assertEquals(event.getCurrentNodeId(), "node-1"); + assertEquals(event.getUserInputs().get("email"), "test@example.com"); + assertEquals(event.getFlowProperties().get("riskScore"), 85); + } + + @Test + public void testBuilderWithNullMaps() { + + InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() + .flowType("LOGIN") + .currentNodeId("node-2") + .userInputs(null) + .flowProperties(null) + .build(); + + assertEquals(event.getFlowType(), "LOGIN"); + assertNotNull(event.getUserInputs()); + assertTrue(event.getUserInputs().isEmpty()); + assertNotNull(event.getFlowProperties()); + assertTrue(event.getFlowProperties().isEmpty()); + } + + @Test + public void testUserInputsAreUnmodifiable() { + + Map userInputs = new HashMap<>(); + userInputs.put("field", "value"); + + InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() + .userInputs(userInputs) + .build(); + + try { + event.getUserInputs().put("hack", "value"); + assertTrue(false, "Expected UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // Expected — map is unmodifiable + } + } + + @Test + public void testFlowPropertiesAreUnmodifiable() { + + Map flowProperties = new HashMap<>(); + flowProperties.put("key", "value"); + + InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() + .flowProperties(flowProperties) + .build(); + + try { + event.getFlowProperties().put("hack", "value"); + assertTrue(false, "Expected UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // Expected — map is unmodifiable + } + } + + @Test + public void testBuilderDoesNotShareMapReferences() { + + Map userInputs = new HashMap<>(); + userInputs.put("email", "original@test.com"); + + InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() + .userInputs(userInputs) + .build(); + + // Mutating the original map should not affect the event + userInputs.put("email", "modified@test.com"); + assertEquals(event.getUserInputs().get("email"), "original@test.com"); + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/OperationExecutionResultTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/OperationExecutionResultTest.java new file mode 100644 index 000000000000..60441c28b020 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/OperationExecutionResultTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025, 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.model; + +import org.testng.annotations.Test; +import org.wso2.carbon.identity.action.execution.api.model.Operation; +import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.OperationExecutionResult; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +/** + * Unit tests for {@link OperationExecutionResult}. + */ +public class OperationExecutionResultTest { + + private PerformableOperation createOperation(Operation op, String path, Object value) { + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(op); + operation.setPath(path); + operation.setValue(value); + return operation; + } + + @Test + public void testSuccessResult() { + + PerformableOperation operation = createOperation( + Operation.REPLACE, "/user/claims/email", "test@example.com"); + OperationExecutionResult result = new OperationExecutionResult( + operation, OperationExecutionResult.Status.SUCCESS, "Claim updated"); + + assertEquals(result.getOperation(), operation); + assertEquals(result.getStatus(), OperationExecutionResult.Status.SUCCESS); + assertEquals(result.getMessage(), "Claim updated"); + } + + @Test + public void testFailureResult() { + + PerformableOperation operation = createOperation( + Operation.REPLACE, "/user/credentials/password", "newPass"); + OperationExecutionResult result = new OperationExecutionResult( + operation, OperationExecutionResult.Status.FAILURE, "Path not allowed"); + + assertEquals(result.getStatus(), OperationExecutionResult.Status.FAILURE); + assertEquals(result.getMessage(), "Path not allowed"); + assertNotNull(result.getOperation()); + } + + @Test + public void testStatusEnumValues() { + + OperationExecutionResult.Status[] values = OperationExecutionResult.Status.values(); + assertEquals(values.length, 2); + assertEquals(OperationExecutionResult.Status.valueOf("SUCCESS"), + OperationExecutionResult.Status.SUCCESS); + assertEquals(OperationExecutionResult.Status.valueOf("FAILURE"), + OperationExecutionResult.Status.FAILURE); + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml index 47e0b300668a..028dc97b4e70 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml @@ -33,9 +33,17 @@ + + + + + + + + diff --git a/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverConstants.java b/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverConstants.java index f78fe303f584..0a8c7abddd7e 100644 --- a/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverConstants.java +++ b/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverConstants.java @@ -48,7 +48,6 @@ public class IdentityKeyStoreResolverConstants { public static final String INBOUND_PROTOCOL_SAML = "saml"; public static final String INBOUND_PROTOCOL_WS_TRUST = "ws-trust"; public static final String INBOUND_PROTOCOL_WS_FEDERATION = "ws-federation"; - public static final String INBOUND_PROTOCOL_ACTIONS = "actions"; /** * Enums for inbound protocols. @@ -59,8 +58,7 @@ public enum InboundProtocol { OAUTH(INBOUND_PROTOCOL_OAUTH), SAML(INBOUND_PROTOCOL_SAML), WS_TRUST(INBOUND_PROTOCOL_WS_TRUST), - WS_FEDERATION(INBOUND_PROTOCOL_WS_FEDERATION), - ACTIONS(INBOUND_PROTOCOL_ACTIONS); + WS_FEDERATION(INBOUND_PROTOCOL_WS_FEDERATION); private final String protocolName; @@ -83,8 +81,6 @@ public static InboundProtocol fromString(String protocolName) { return WS_TRUST; case INBOUND_PROTOCOL_WS_FEDERATION: return WS_FEDERATION; - case INBOUND_PROTOCOL_ACTIONS: - return ACTIONS; default: return null; } From 72425ce3bce7f336fbe6d755f3524a92237646ea Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Mon, 30 Mar 2026 01:00:53 +0530 Subject: [PATCH 09/41] Adding action executor service to flow execution engine service component and data holder --- .../FlowExecutionEngineServiceComponent.java | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java index 4cde31beb6d4..e32c2c758ca2 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java @@ -263,15 +263,13 @@ public void unsetFederatedAssociationManager(FederatedAssociationManager federat ) protected void setClaimMetadataManagementService(ClaimMetadataManagementService claimMetadataManagementService) { - LOG.debug("Setting the ClaimMetadataManagementService in the Flow Engine component."); - FlowExecutionEngineDataHolder.getInstance() - .setClaimMetadataManagementService(claimMetadataManagementService); + LOG.debug("Setting the Claim Metadata Management Service in the Flow Engine component."); + FlowExecutionEngineDataHolder.getInstance().setClaimMetadataManagementService(claimMetadataManagementService); } - protected void unsetClaimMetadataManagementService( - ClaimMetadataManagementService claimMetadataManagementService) { + protected void unsetClaimMetadataManagementService(ClaimMetadataManagementService claimMetadataManagementService) { - LOG.debug("Unsetting the ClaimMetadataManagementService in the Flow Engine component."); + LOG.debug("Unsetting the Claim Metadata Management Service in the Flow Engine component."); FlowExecutionEngineDataHolder.getInstance().setClaimMetadataManagementService(null); } @@ -330,6 +328,25 @@ protected void unsetActionManagementService(ActionManagementService actionManage FlowExecutionEngineDataHolder.getInstance().setActionManagementService(null); } + @Reference( + name = "ActionExecutorService", + service = ActionExecutorService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetActionExecutorService" + ) + protected void setActionExecutorService(ActionExecutorService actionExecutorService) { + + LOG.debug("Setting the ActionExecutorService in the Flow Engine component."); + FlowExecutionEngineDataHolder.getInstance().setActionExecutorService(actionExecutorService); + } + + protected void unsetActionExecutorService(ActionExecutorService actionExecutorService) { + + LOG.debug("Unsetting the ActionExecutorService in the Flow Engine component."); + FlowExecutionEngineDataHolder.getInstance().setActionExecutorService(null); + } + @Reference( name = "CertificateManagementService", service = CertificateManagementService.class, From bc2a3cb20679a977585d151b4bcc16e2e686aa6d Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Mon, 30 Mar 2026 15:20:37 +0530 Subject: [PATCH 10/41] Fixed an issue in property and input encryption and modified unit tests. --- .../executor/HierarchicalPrefixMatcher.java | 65 +------------- .../executor/InFlowExtensionEvent.java | 19 ---- .../InFlowExtensionRequestBuilder.java | 47 +++++----- .../InFlowExtensionResponseProcessor.java | 19 +--- .../extension/executor/JWEEncryptionUtil.java | 6 -- .../executor/PathTypeAnnotationUtil.java | 1 - .../HierarchicalPrefixMatcherTest.java | 34 +------ .../executor/InFlowExtensionExecutorTest.java | 14 --- .../InFlowExtensionRequestBuilderTest.java | 88 ++++++++++++++++++- .../InFlowExtensionResponseProcessorTest.java | 14 --- .../executor/PathTypeAnnotationUtilTest.java | 16 ---- .../model/InFlowExtensionEventTest.java | 3 - .../model/OperationExecutionResultTest.java | 10 --- .../resources/identity.xml.j2 | 6 ++ ....identity.core.server.feature.default.json | 2 + 15 files changed, 125 insertions(+), 219 deletions(-) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java index 216349750199..18bd70d74582 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java @@ -20,9 +20,7 @@ import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; /** * Utility class for hierarchical prefix-based path matching and context area identification. @@ -35,7 +33,6 @@ * /user/ - User context * /user/claims/{claimURI} - User claims (keys are system-configured claim URIs) * /user/userId - User's unique identifier - * /user/username - Resolved username * /user/userStoreDomain - User store domain * /user/credentials/ - User credentials (system-configured types) * /user/federatedAssociations/ - Federated IDP associations @@ -49,11 +46,6 @@ * /flow/applicationId - Application ID * /flow/flowType - Flow type (REGISTRATION, etc.) * /flow/contextIdentifier - Flow context identifier - * - * /graph/ - Graph state (READ-ONLY) - * /graph/currentNode/ - Current node info - * /graph/currentNode/id - Current node ID - * /graph/currentNode/type - Current node type * */ public final class HierarchicalPrefixMatcher { @@ -64,7 +56,6 @@ public final class HierarchicalPrefixMatcher { public static final String USER_CREDENTIALS_PREFIX = "/user/credentials/"; public static final String USER_FEDERATED_PREFIX = "/user/federatedAssociations/"; public static final String USER_ID_PATH = "/user/userId"; - public static final String USER_NAME_PATH = "/user/username"; public static final String USER_STORE_DOMAIN_PATH = "/user/userStoreDomain"; public static final String PROPERTIES_PREFIX = "/properties/"; @@ -75,15 +66,12 @@ public final class HierarchicalPrefixMatcher { public static final String FLOW_APP_ID_PATH = "/flow/applicationId"; public static final String FLOW_TYPE_PATH = "/flow/flowType"; - public static final String GRAPH_PREFIX = "/graph/"; - public static final String GRAPH_CURRENT_NODE_PREFIX = "/graph/currentNode/"; - /** * Default expose configuration — all context areas are exposed. * Used when no explicit expose configuration is provided by the executor metadata. */ public static final List DEFAULT_EXPOSE = Collections.unmodifiableList( - Arrays.asList(USER_PREFIX, PROPERTIES_PREFIX, INPUT_PREFIX, FLOW_PREFIX, GRAPH_PREFIX)); + Arrays.asList(USER_PREFIX, PROPERTIES_PREFIX, INPUT_PREFIX, FLOW_PREFIX)); /** * Context area enum for categorization. @@ -95,8 +83,7 @@ public enum ContextArea { USER_SCALAR(USER_PREFIX, false), // Scalar user fields (userId, username, etc.) PROPERTIES(PROPERTIES_PREFIX, false), // Fully extensible INPUT(INPUT_PREFIX, false), // Runtime extensible - FLOW(FLOW_PREFIX, false), // Read-only scalar values - GRAPH(GRAPH_PREFIX, false); // Read-only graph state + FLOW(FLOW_PREFIX, false); // Read-only scalar values private final String prefix; private final boolean hasSystemConfiguredKeys; @@ -126,7 +113,6 @@ public boolean hasSystemConfiguredKeys() { private HierarchicalPrefixMatcher() { - // Utility class, no instantiation } /** @@ -163,9 +149,6 @@ public static ContextArea identifyContextArea(String path) { if (path.startsWith(FLOW_PREFIX)) { return ContextArea.FLOW; } - if (path.startsWith(GRAPH_PREFIX)) { - return ContextArea.GRAPH; - } return null; } @@ -255,7 +238,7 @@ public static boolean isReadOnly(String path) { if (path == null) { return false; } - return path.startsWith(FLOW_PREFIX) || path.startsWith(GRAPH_PREFIX); + return path.startsWith(FLOW_PREFIX); } /** @@ -298,46 +281,4 @@ public static boolean matchesAnyExpose(String path, List exposePrefixes) return false; } - /** - * Map legacy path prefixes to unified hierarchy prefixes. - * This provides backward compatibility with existing configurations. - * - * Legacy mapping: - * - /userInputs/ -> /input/ - * - /user/claims/ -> /user/claims/ (unchanged) - * - /properties/ -> /properties/ (unchanged) - */ - - // TODO: This is a simple mapping for demonstration. In a real implementation, - // this could be loaded from configuration OR use all context names unchanged. - private static final Map LEGACY_PREFIX_MAPPING = createLegacyMapping(); - - private static Map createLegacyMapping() { - - Map mapping = new HashMap<>(); - mapping.put("/userInputs/", INPUT_PREFIX); - // Add more legacy mappings as needed - return mapping; - } - - /** - * Normalize a path by converting legacy prefixes to unified prefixes. - * - * @param path The path to normalize - * @return The normalized path using unified prefixes - */ - public static String normalizePath(String path) { - - if (path == null) { - return null; - } - - for (Map.Entry entry : LEGACY_PREFIX_MAPPING.entrySet()) { - if (path.startsWith(entry.getKey())) { - return entry.getValue() + path.substring(entry.getKey().length()); - } - } - - return path; - } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionEvent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionEvent.java index 7671fa208974..c059e1fabdfd 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionEvent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionEvent.java @@ -37,7 +37,6 @@ public class InFlowExtensionEvent extends Event { private final String flowType; - private final String currentNodeId; private final Map userInputs; private final Map flowProperties; @@ -50,7 +49,6 @@ private InFlowExtensionEvent(Builder builder) { this.userStore = builder.userStore; this.application = builder.application; this.flowType = builder.flowType; - this.currentNodeId = builder.currentNodeId; this.userInputs = builder.userInputs != null ? Collections.unmodifiableMap(new HashMap<>(builder.userInputs)) : Collections.emptyMap(); this.flowProperties = builder.flowProperties != null ? @@ -67,16 +65,6 @@ public String getFlowType() { return flowType; } - /** - * Get the current node ID in the flow. - * - * @return The current node ID. - */ - public String getCurrentNodeId() { - - return currentNodeId; - } - /** * Get the user inputs collected during the flow. * @@ -109,7 +97,6 @@ public static class Builder { private UserStore userStore; private Application application; private String flowType; - private String currentNodeId; private Map userInputs; private Map flowProperties; @@ -155,12 +142,6 @@ public Builder flowType(String flowType) { return this; } - public Builder currentNodeId(String currentNodeId) { - - this.currentNodeId = currentNodeId; - return this; - } - public Builder userInputs(Map userInputs) { this.userInputs = userInputs != null ? new HashMap<>(userInputs) : null; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java index 918d3e633db8..a32858b02e41 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java @@ -39,7 +39,6 @@ import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; 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.mgt.model.NodeConfig; import java.util.ArrayList; import java.util.Collections; @@ -190,8 +189,6 @@ private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List userInputs = context.getUserInputData(); if (userInputs != null && !userInputs.isEmpty()) { - eventBuilder.userInputs(filterMap(userInputs, HierarchicalPrefixMatcher.INPUT_PREFIX, expose)); + eventBuilder.userInputs(filterMap(userInputs, HierarchicalPrefixMatcher.INPUT_PREFIX, + expose, accessConfig, certificatePEM)); } } @@ -250,7 +240,8 @@ private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List properties = context.getProperties(); if (properties != null && !properties.isEmpty()) { eventBuilder.flowProperties( - filterMap(properties, HierarchicalPrefixMatcher.PROPERTIES_PREFIX, expose)); + filterMap(properties, HierarchicalPrefixMatcher.PROPERTIES_PREFIX, + expose, accessConfig, certificatePEM)); } } @@ -324,29 +315,35 @@ private User buildUser(FlowUser flowUser, List expose, /** * Filter a map to only include entries whose paths are exposed. + * Values for expose paths marked as encrypted are JWE-encrypted. * - * @param map The source map. - * @param areaPrefix The area prefix (e.g. "/properties/"). - * @param expose The expose prefix list. - * @param The value type. - * @return A new map containing only exposed entries. + * @param map The source map. + * @param areaPrefix The area prefix (e.g. "/properties/"). + * @param expose The expose prefix list. + * @param accessConfig The access config with encryption flags (may be null). + * @param certificatePEM The certificate PEM for JWE encryption (may be null). + * @param The value type. + * @return A new map containing only exposed entries, with encrypted values where configured. */ - private Map filterMap(Map map, String areaPrefix, List expose) { + @SuppressWarnings("unchecked") + private Map filterMap(Map map, String areaPrefix, List expose, + AccessConfig accessConfig, String certificatePEM) { if (map == null) { return null; } boolean hasSpecificFilter = hasSpecificSubPathFilter(expose, areaPrefix); - if (!hasSpecificFilter) { - // The entire area is exposed — return a copy. - return new HashMap<>(map); - } Map filtered = new HashMap<>(); for (Map.Entry entry : map.entrySet()) { - if (isExposed(areaPrefix + entry.getKey(), expose)) { - filtered.put(entry.getKey(), entry.getValue()); + String fullPath = areaPrefix + entry.getKey(); + if (!hasSpecificFilter || isExposed(fullPath, expose)) { + T value = entry.getValue(); + if (shouldEncrypt(fullPath, accessConfig, certificatePEM)) { + value = (T) encryptValue(String.valueOf(value), certificatePEM); + } + filtered.put(entry.getKey(), value); } } return filtered; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java index 707334fb1c9b..102621474e04 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java @@ -79,12 +79,9 @@ public class InFlowExtensionResponseProcessor implements ActionExecutionResponse private static final Log LOG = LogFactory.getLog(InFlowExtensionResponseProcessor.class); - // Path prefixes for In-Flow Extension context (unified hierarchy) private static final String PROPERTIES_PATH_PREFIX = "/properties/"; private static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; private static final String USER_INPUTS_PATH_PREFIX = "/input/"; - // Legacy prefix for backward compatibility - private static final String LEGACY_USER_INPUTS_PATH_PREFIX = "/userInputs/"; // Cache for valid claim URIs (per tenant) private Map> validClaimUrisCache = new HashMap<>(); @@ -125,18 +122,6 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon if (operations != null && !operations.isEmpty()) { for (PerformableOperation operation : operations) { - // Normalize legacy paths. - // TODO: Remove this normalization logic in future once external services are updated - // to use unified paths. - String normalizedPath = HierarchicalPrefixMatcher.normalizePath(operation.getPath()); - if (!normalizedPath.equals(operation.getPath())) { - PerformableOperation normalizedOp = new PerformableOperation(); - normalizedOp.setOp(operation.getOp()); - normalizedOp.setPath(normalizedPath); - normalizedOp.setValue(operation.getValue()); - operation = normalizedOp; - } - // Decrypt inbound value if this operation path is marked as encrypted in AccessConfig. operation = decryptOperationValueIfNeeded(operation, accessConfig, tenantDomain); @@ -180,7 +165,7 @@ private OperationExecutionResult processOperation(PerformableOperation operation return handlePropertyOperation(operation, context, pathTypeAnnotations); } else if (path.startsWith(USER_CLAIMS_PATH_PREFIX)) { return handleUserClaimOperation(operation, context, tenantDomain); - } else if (path.startsWith(USER_INPUTS_PATH_PREFIX) || path.startsWith(LEGACY_USER_INPUTS_PATH_PREFIX)) { + } else if (path.startsWith(USER_INPUTS_PATH_PREFIX)) { return handleUserInputOperation(operation, context); } @@ -312,7 +297,7 @@ private OperationExecutionResult handleUserInputOperation(PerformableOperation o "User input replace applied."); } - //TODO: These validations can be removed once the attribute executor introduced. + //TODO: These validations can be removed once the attribute executor is introduced. /** * Validate if a claim URI exists in the local claim dialect. */ diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java index bfe9813199e5..26794f7f6a1e 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java @@ -60,12 +60,6 @@ public final class JWEEncryptionUtil { private static final Log LOG = LogFactory.getLog(JWEEncryptionUtil.class); - /** Number of dot-separated parts in a JWE compact serialization. */ - private static final int JWE_PART_COUNT = 5; - - /** Standard PEM line length per RFC 7468. */ - private static final int PEM_LINE_LENGTH = 64; - /** Cache of resolved private keys, keyed by tenant ID. */ private static final Map PRIVATE_KEYS = new ConcurrentHashMap<>(); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtil.java index 6e63a3108bee..3eb1d0cb49e3 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtil.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtil.java @@ -63,7 +63,6 @@ public final class PathTypeAnnotationUtil { private PathTypeAnnotationUtil() { - // Utility class, no instantiation. } /** diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java index 4b96b51b80f6..96ad757a465a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java @@ -60,8 +60,6 @@ public Object[][] contextAreaPaths() { { "/flow/tenantDomain", ContextArea.FLOW }, { "/flow/applicationId", ContextArea.FLOW }, { "/flow/", ContextArea.FLOW }, - { "/graph/currentNode/id", ContextArea.GRAPH }, - { "/graph/", ContextArea.GRAPH }, }; } @@ -186,8 +184,6 @@ public Object[][] readOnlyPaths() { { "/flow/tenantDomain", true }, { "/flow/applicationId", true }, { "/flow/", true }, - { "/graph/currentNode/id", true }, - { "/graph/", true }, { "/properties/riskScore", false }, { "/user/claims/email", false }, { "/input/field", false }, @@ -245,7 +241,7 @@ public void testMatchesAnyExposePrefixStartsWithPath() { @Test public void testMatchesAnyExposeNoMatch() { - List expose = Arrays.asList("/flow/", "/graph/"); + List expose = Arrays.asList("/flow/"); assertFalse(HierarchicalPrefixMatcher.matchesAnyExpose("/properties/score", expose)); assertFalse(HierarchicalPrefixMatcher.matchesAnyExpose("/user/claims/email", expose)); } @@ -271,31 +267,10 @@ public void testMatchesAnyExposeNullList() { @Test public void testMatchesAnyExposeMultiplePrefixesOneMatches() { - List expose = Arrays.asList("/flow/", "/graph/", "/properties/"); + List expose = Arrays.asList("/flow/", "/properties/"); assertTrue(HierarchicalPrefixMatcher.matchesAnyExpose("/properties/score", expose)); } - // ========================= normalizePath ========================= - - @Test - public void testNormalizePathLegacyUserInputs() { - - assertEquals(HierarchicalPrefixMatcher.normalizePath("/userInputs/field"), "/input/field"); - } - - @Test - public void testNormalizePathUnchanged() { - - assertEquals(HierarchicalPrefixMatcher.normalizePath("/properties/score"), "/properties/score"); - assertEquals(HierarchicalPrefixMatcher.normalizePath("/user/claims/email"), "/user/claims/email"); - } - - @Test - public void testNormalizePathNull() { - - assertNull(HierarchicalPrefixMatcher.normalizePath(null)); - } - // ========================= DEFAULT_EXPOSE ========================= @Test @@ -303,12 +278,11 @@ public void testDefaultExposeContainsAllAreas() { List defaultExpose = HierarchicalPrefixMatcher.DEFAULT_EXPOSE; assertNotNull(defaultExpose); - assertEquals(defaultExpose.size(), 5); + assertEquals(defaultExpose.size(), 4); assertTrue(defaultExpose.contains("/user/")); assertTrue(defaultExpose.contains("/properties/")); assertTrue(defaultExpose.contains("/input/")); assertTrue(defaultExpose.contains("/flow/")); - assertTrue(defaultExpose.contains("/graph/")); } @Test(expectedExceptions = UnsupportedOperationException.class) @@ -326,7 +300,6 @@ public void testContextAreaPrefix() { assertEquals(ContextArea.PROPERTIES.getPrefix(), "/properties/"); assertEquals(ContextArea.INPUT.getPrefix(), "/input/"); assertEquals(ContextArea.FLOW.getPrefix(), "/flow/"); - assertEquals(ContextArea.GRAPH.getPrefix(), "/graph/"); } @Test @@ -339,6 +312,5 @@ public void testContextAreaHasSystemConfiguredKeys() { assertFalse(ContextArea.PROPERTIES.hasSystemConfiguredKeys()); assertFalse(ContextArea.INPUT.hasSystemConfiguredKeys()); assertFalse(ContextArea.FLOW.hasSystemConfiguredKeys()); - assertFalse(ContextArea.GRAPH.hasSystemConfiguredKeys()); } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java index a66b1653369e..eb20a53dacdd 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java @@ -531,20 +531,6 @@ public void testExecuteNoExecutorDTO() throws Exception { assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); } - // ========================= ExecutorResult enum ========================= - - @Test - public void testExecutorResultValues() { - - assertEquals(ExecutorResult.values().length, 6); - assertNotNull(ExecutorResult.valueOf("COMPLETE")); - assertNotNull(ExecutorResult.valueOf("ERROR")); - assertNotNull(ExecutorResult.valueOf("USER_ERROR")); - assertNotNull(ExecutorResult.valueOf("USER_INPUT_REQUIRED")); - assertNotNull(ExecutorResult.valueOf("EXTERNAL_REDIRECTION")); - assertNotNull(ExecutorResult.valueOf("RETRY")); - } - // ========================= Helper methods ========================= private FlowExecutionContext createContextWithMetadata(Map metadata) { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java index e6fd5242239a..3bdd6d46530f 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java @@ -34,8 +34,11 @@ import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionEvent; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionRequestBuilder; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.JWEEncryptionUtil; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; +import org.wso2.carbon.identity.certificate.management.model.Certificate; 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.mgt.model.NodeConfig; @@ -141,7 +144,7 @@ public void testBuildRequestWithValidModifyPaths() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) .add(InFlowExtensionExecutor.EXPOSE_KEY, - Arrays.asList("/user/", "/properties/", "/input/", "/flow/", "/graph/")); + Arrays.asList("/user/", "/properties/", "/input/", "/flow/")); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -410,6 +413,89 @@ public void testExposeFilteringSpecificClaim() assertEquals(claims.size(), 1); } + // ========================= Outbound encryption of properties and inputs ========================= + + @Test + public void testPropertiesEncryptedWhenExposePathMarkedEncrypted() + throws ActionExecutionRequestBuilderException { + + try (MockedStatic jweUtilMock = mockStatic(JWEEncryptionUtil.class)) { + jweUtilMock.when(() -> JWEEncryptionUtil.encrypt(anyString(), anyString())) + .thenAnswer(inv -> "encrypted." + inv.getArgument(0) + ".jwe.part.four"); + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + execCtx.setProperty("riskScore", "85"); + + // Mark /properties/riskScore as expose-encrypted. + AccessConfig accessConfig = new AccessConfig( + Arrays.asList(new ContextPath("/properties/riskScore", true)), + null); + + Encryption encryption = new Encryption( + new Certificate.Builder().id("cert-1").name("test") + .certificateContent("test-cert-pem").build()); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE) + .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) + .add(InFlowExtensionExecutor.ENCRYPTION_KEY, encryption); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event.getFlowProperties()); + // riskScore should be encrypted. + Object riskScoreValue = event.getFlowProperties().get("riskScore"); + assertNotNull(riskScoreValue); + assertTrue(riskScoreValue.toString().startsWith("encrypted."), + "Property value should be encrypted when expose path is marked encrypted"); + // existingProp is NOT marked encrypted — should remain plaintext. + assertEquals(event.getFlowProperties().get("existingProp"), "existingValue"); + } + } + + @Test + public void testUserInputsEncryptedWhenExposePathMarkedEncrypted() + throws ActionExecutionRequestBuilderException { + + try (MockedStatic jweUtilMock = mockStatic(JWEEncryptionUtil.class)) { + jweUtilMock.when(() -> JWEEncryptionUtil.encrypt(anyString(), anyString())) + .thenAnswer(inv -> "encrypted." + inv.getArgument(0) + ".jwe.part.four"); + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + + // Mark /input/consent as expose-encrypted. + AccessConfig accessConfig = new AccessConfig( + Arrays.asList(new ContextPath("/input/consent", true)), + null); + + Encryption encryption = new Encryption( + new Certificate.Builder().id("cert-1").name("test") + .certificateContent("test-cert-pem").build()); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE) + .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) + .add(InFlowExtensionExecutor.ENCRYPTION_KEY, encryption); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event.getUserInputs()); + // consent should be encrypted. + String consentValue = event.getUserInputs().get("consent"); + assertNotNull(consentValue); + assertTrue(consentValue.startsWith("encrypted."), + "Input value should be encrypted when expose path is marked encrypted"); + // username input is NOT marked encrypted — should remain plaintext. + assertEquals(event.getUserInputs().get("username"), "testuser"); + } + } + // ========================= Helper methods ========================= private FlowExecutionContext createMinimalFlowExecutionContext() { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java index db83a4804e71..fee84576b494 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java @@ -530,20 +530,6 @@ public void testUnknownPathPrefix() throws ActionExecutionResponseProcessorExcep assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); } - // ========================= processSuccessResponse — Legacy path normalization ========================= - - @Test - public void testLegacyUserInputsPathNormalized() throws ActionExecutionResponseProcessorException { - - FlowExecutionContext execCtx = createFlowExecutionContext(); - - // Legacy /userInputs/ path should be normalized to /input/. - PerformableOperation op = createOperation(Operation.REPLACE, "/userInputs/legacyField", "value"); - executeSuccessResponse(execCtx, op, Collections.emptyMap()); - - assertEquals(execCtx.getUserInputData().get("legacyField"), "value"); - } - // ========================= processSuccessResponse — Multiple operations ========================= @Test diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/PathTypeAnnotationUtilTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/PathTypeAnnotationUtilTest.java index 8f176fe058a8..e2f7be53fbdf 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/PathTypeAnnotationUtilTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/PathTypeAnnotationUtilTest.java @@ -207,22 +207,6 @@ public void testCoerceValueStringValue() { assertEquals(result, "test"); } - // ========================= Constants ========================= - - @Test - public void testLocalClaimDialectPrefix() { - - // Verify the identity claim URI is a sub-prefix of the local claim dialect. - assertTrue("http://wso2.org/claims/identity/".startsWith("http://wso2.org/claims/")); - } - - @Test - public void testIdentityClaimUriPrefix() { - - // Verify identity claim prefix is distinct from the general local claim prefix. - assertTrue("http://wso2.org/claims/identity/".length() > "http://wso2.org/claims/".length()); - } - // ========================= validateAnnotationLimits ========================= @Test diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java index dab31deca53c..825e43dcb93c 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java @@ -45,13 +45,11 @@ public void testBuilderWithAllFields() { InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() .flowType("REGISTRATION") - .currentNodeId("node-1") .userInputs(userInputs) .flowProperties(flowProperties) .build(); assertEquals(event.getFlowType(), "REGISTRATION"); - assertEquals(event.getCurrentNodeId(), "node-1"); assertEquals(event.getUserInputs().get("email"), "test@example.com"); assertEquals(event.getFlowProperties().get("riskScore"), 85); } @@ -61,7 +59,6 @@ public void testBuilderWithNullMaps() { InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() .flowType("LOGIN") - .currentNodeId("node-2") .userInputs(null) .flowProperties(null) .build(); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/OperationExecutionResultTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/OperationExecutionResultTest.java index 60441c28b020..ea5d47c7eb52 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/OperationExecutionResultTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/OperationExecutionResultTest.java @@ -66,14 +66,4 @@ public void testFailureResult() { assertNotNull(result.getOperation()); } - @Test - public void testStatusEnumValues() { - - OperationExecutionResult.Status[] values = OperationExecutionResult.Status.values(); - assertEquals(values.length, 2); - assertEquals(OperationExecutionResult.Status.valueOf("SUCCESS"), - OperationExecutionResult.Status.SUCCESS); - assertEquals(OperationExecutionResult.Status.valueOf("FAILURE"), - OperationExecutionResult.Status.FAILURE); - } } diff --git a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 index 7b1f92f69011..4510052e7929 100644 --- a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 +++ b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 @@ -2518,6 +2518,12 @@ {{actions.types.authentication.default_userstore}} + + {{actions.types.in_flow_extension.enable}} + + {{actions.types.in_flow_extension.version.latest}} + + diff --git a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/org.wso2.carbon.identity.core.server.feature.default.json b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/org.wso2.carbon.identity.core.server.feature.default.json index 500d5473ab85..b38b44a5370d 100644 --- a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/org.wso2.carbon.identity.core.server.feature.default.json +++ b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/org.wso2.carbon.identity.core.server.feature.default.json @@ -2267,6 +2267,8 @@ "actions.types.pre_update_profile.enable": true, "actions.types.pre_update_profile.version.latest": "v1", "actions.types.pre_update_password.version.latest": "v2", + "actions.types.in_flow_extension.enable": true, + "actions.types.in_flow_extension.version.latest": "v1", "external_api_client.http_client.connection_timeout": "2000", "external_api_client.http_client.read_timeout": "3000", From 7ac1f50731dfdc931de92f5dff79df851348e08a Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Mon, 6 Apr 2026 19:17:08 +0530 Subject: [PATCH 11/41] Address code review comments for In-Flow Extension - Ensure JWE encryption/decryption failures throw ActionExecutionException instead of silently continuing with plaintext or raw values. - Handle credential conversion securely by zeroing out the char[] buffer after JWE string conversion. - Change validClaimUrisCache to a ConcurrentHashMap and add a TTL mechanism for local claim synchronization. - Fix bug where plain string items in the expose/modify list were incorrectly dropped in AccessConfigFlowUpdateInterceptor. - Add an instanceof String check before casting the operation value in InFlowExtensionResponseProcessor. - Implement eviction/TTL on PRIVATE_KEYS cache to support key rotations. - Implement X509 certificate validation to ensure expired or untrusted certificates are rejected. - Correct typo 'within an any flow' to 'within any flow' in Action.java. - Remove commented-out dependency block in pom.xml. --- .../action/management/api/model/Action.java | 2 +- .../api/service/ActionManagementService.java | 14 ++++ .../impl/ActionManagementServiceImpl.java | 11 +++ .../CacheBackedActionManagementService.java | 7 ++ .../management/model/ActionTypesTest.java | 2 +- .../pom.xml | 4 - .../InFlowExtensionRequestBuilder.java | 23 +++--- .../InFlowExtensionResponseProcessor.java | 58 ++++++++++++--- .../extension/executor/JWEEncryptionUtil.java | 52 +++++++++++-- .../AccessConfigFlowUpdateInterceptor.java | 5 +- .../InFlowExtensionActionConstants.java | 6 ++ .../InFlowExtensionActionConverter.java | 15 ++++ ...InFlowExtensionActionDTOModelResolver.java | 22 ++++++ .../model/InFlowExtensionAction.java | 27 +++++++ .../InFlowExtensionRequestBuilderTest.java | 33 +++++++++ .../InFlowExtensionResponseProcessorTest.java | 74 +++++++++++++++++++ 16 files changed, 318 insertions(+), 37 deletions(-) diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java index 4b238f69af1f..ed86789fd74b 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java @@ -80,7 +80,7 @@ public enum ActionTypes { "inFlowExtension", "IN_FLOW_EXTENSION", "In-Flow Extension", - "Configure an extension point within an any flow via a custom service.", + "Configure an extension point within any flow via a custom service.", Category.EXTENSION); private final String pathParam; diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java index cddc2d5548ed..de218c2902b4 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java @@ -140,4 +140,18 @@ Action updateActionEndpointAuthentication(String actionType, String actionId, Au * @throws ActionMgtException If an error occurs while checking name availability. */ boolean isActionNameAvailable(String actionType, String name, String tenantDomain) throws ActionMgtException; + + /** + * Check whether the given action name is available (unique) within the specified action type, + * excluding the action with the given ID (useful for update scenarios). + * + * @param actionType Action Type path parameter. + * @param name Action name to check. + * @param excludeActionId Action ID to exclude from the uniqueness check. + * @param tenantDomain Tenant domain. + * @return {@code true} if the name is available, {@code false} otherwise. + * @throws ActionMgtException If an error occurs while checking name availability. + */ + boolean isActionNameAvailable(String actionType, String name, String excludeActionId, String tenantDomain) + throws ActionMgtException; } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java index 974e18b02b45..084eb884b024 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java @@ -187,6 +187,17 @@ public boolean isActionNameAvailable(String actionType, String name, String tena .noneMatch(dto -> name.equalsIgnoreCase(dto.getName())); } + @Override + public boolean isActionNameAvailable(String actionType, String name, String excludeActionId, + String tenantDomain) throws ActionMgtException { + + String resolvedActionType = getActionTypeFromPath(actionType); + int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); + List actionDTOS = DAO_FACADE.getActionsByActionType(resolvedActionType, tenantId); + return actionDTOS.stream() + .noneMatch(dto -> name.equalsIgnoreCase(dto.getName()) && !dto.getId().equals(excludeActionId)); + } + private String resolveActionVersionAtUpdating(Action updatingAction, ActionDTO existingActionDTO) { String updatingActionVersion = updatingAction.getActionVersion(); diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java index 6ab187ce2c6c..ff3f399697d7 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java @@ -189,6 +189,13 @@ public boolean isActionNameAvailable(String actionType, String name, String tena return ACTION_MGT_SERVICE.isActionNameAvailable(actionType, name, tenantDomain); } + @Override + public boolean isActionNameAvailable(String actionType, String name, String excludeActionId, + String tenantDomain) throws ActionMgtException { + + return ACTION_MGT_SERVICE.isActionNameAvailable(actionType, name, excludeActionId, tenantDomain); + } + private void updateCache(Action action, ActionCacheEntry entry, ActionTypeCacheKey cacheKey, String tenantDomain) { if (LOG.isDebugEnabled()) { diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java index 1c17dc2d0478..342682d37456 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java @@ -55,7 +55,7 @@ public Object[][] actionTypesProvider() { Action.ActionTypes.Category.PRE_POST}, {Action.ActionTypes.IN_FLOW_EXTENSION, "inFlowExtension", "IN_FLOW_EXTENSION", "In-Flow Extension", - "Configure an extension point within an any flow via a custom service.", + "Configure an extension point within any flow via a custom service.", Action.ActionTypes.Category.EXTENSION} }; } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml index 952d0b0db8bd..50692dfe1ab3 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml @@ -52,10 +52,6 @@ org.ops4j.pax.logging pax-logging-api - - - - com.fasterxml.jackson.core jackson-databind diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java index a32858b02e41..86952e0f623a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java @@ -185,7 +185,8 @@ private List buildAllowedOperationsFromModify(AccessConfig acc * @return The InFlowExtensionEvent. */ private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List expose, - AccessConfig accessConfig, String certificatePEM) { + AccessConfig accessConfig, String certificatePEM) + throws ActionExecutionRequestBuilderException { InFlowExtensionEvent.Builder eventBuilder = new InFlowExtensionEvent.Builder(); @@ -259,7 +260,8 @@ private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List expose, - AccessConfig accessConfig, String certificatePEM) { + AccessConfig accessConfig, String certificatePEM) + throws ActionExecutionRequestBuilderException { String userId = isExposed(HierarchicalPrefixMatcher.USER_ID_PATH, expose) ? flowUser.getUserId() : null; @@ -300,6 +302,7 @@ private User buildUser(FlowUser flowUser, List expose, Map encryptedCredentials = new HashMap<>(); for (Map.Entry entry : credentials.entrySet()) { String plaintext = new String(entry.getValue()); + java.util.Arrays.fill(entry.getValue(), '\0'); String encrypted = encryptValue(plaintext, certificatePEM); encryptedCredentials.put(entry.getKey(), encrypted.toCharArray()); } @@ -327,7 +330,8 @@ private User buildUser(FlowUser flowUser, List expose, */ @SuppressWarnings("unchecked") private Map filterMap(Map map, String areaPrefix, List expose, - AccessConfig accessConfig, String certificatePEM) { + AccessConfig accessConfig, String certificatePEM) + throws ActionExecutionRequestBuilderException { if (map == null) { return null; @@ -371,8 +375,6 @@ private boolean hasSpecificSubPathFilter(List expose, String areaPrefix) return false; } - // ---- Encryption helpers ---- - /** * Determine if a value at the given path should be JWE-encrypted before sending to the * external service. Only expose paths with {@code encrypted: true} trigger outbound encryption. @@ -392,19 +394,20 @@ private boolean shouldEncrypt(String path, AccessConfig accessConfig, String cer /** * JWE-encrypt a plaintext value using the external service's certificate. - * If encryption fails, the original value is returned and a warning is logged. * * @param plaintext The value to encrypt. * @param certificatePEM The external service's certificate PEM. - * @return The JWE compact serialization string, or the original value on failure. + * @return The JWE compact serialization string. + * @throws ActionExecutionRequestBuilderException If encryption fails. */ - private String encryptValue(String plaintext, String certificatePEM) { + private String encryptValue(String plaintext, String certificatePEM) + throws ActionExecutionRequestBuilderException { try { return JWEEncryptionUtil.encrypt(plaintext, certificatePEM); } catch (Exception e) { - LOG.warn("Failed to JWE-encrypt outbound value. Sending plaintext.", e); - return plaintext; + throw new ActionExecutionRequestBuilderException( + "Failed to JWE-encrypt outbound value for In-Flow Extension action.", e); } } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java index 102621474e04..87c454aec8f7 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java @@ -57,6 +57,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; /** * This class is responsible for processing the response from In-Flow Extension actions. @@ -83,8 +85,9 @@ public class InFlowExtensionResponseProcessor implements ActionExecutionResponse private static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; private static final String USER_INPUTS_PATH_PREFIX = "/input/"; - // Cache for valid claim URIs (per tenant) - private Map> validClaimUrisCache = new HashMap<>(); + // Cache for valid claim URIs (per tenant) with TTL. + private static final long CLAIM_CACHE_TTL_MS = TimeUnit.MINUTES.toMillis(30); + private final Map validClaimUrisCache = new ConcurrentHashMap<>(); @Override public ActionType getSupportedActionType() { @@ -122,7 +125,6 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon if (operations != null && !operations.isEmpty()) { for (PerformableOperation operation : operations) { - // Decrypt inbound value if this operation path is marked as encrypted in AccessConfig. operation = decryptOperationValueIfNeeded(operation, accessConfig, tenantDomain); results.add(processOperation(operation, execCtx, tenantDomain, pathTypeAnnotations)); @@ -310,14 +312,15 @@ private boolean isValidClaimUri(String claimUri, String tenantDomain) { } /** - * Get valid claim URIs for a tenant, loading from ClaimMetadataManagementService if not cached. + * Get valid claim URIs for a tenant, loading from ClaimMetadataManagementService if not cached or expired. */ private Set getValidClaimUris(String tenantDomain) { String cacheKey = tenantDomain != null ? tenantDomain : "carbon.super"; - if (validClaimUrisCache.containsKey(cacheKey)) { - return validClaimUrisCache.get(cacheKey); + CachedClaimUris cached = validClaimUrisCache.get(cacheKey); + if (cached != null && !cached.isExpired()) { + return cached.getClaimUris(); } Set validUris = new HashSet<>(); @@ -335,7 +338,7 @@ private Set getValidClaimUris(String tenantDomain) { LOG.error("Failed to load claim URIs for tenant: " + tenantDomain, e); } - validClaimUrisCache.put(cacheKey, validUris); + validClaimUrisCache.put(cacheKey, new CachedClaimUris(validUris)); if (LOG.isDebugEnabled()) { LOG.debug("Loaded " + validUris.size() + " valid claim URIs for tenant: " + cacheKey); @@ -465,7 +468,8 @@ public static class InFlowExtensionSuccess implements Success { * @return The operation with decrypted value, or the original operation if no decryption needed. */ private PerformableOperation decryptOperationValueIfNeeded(PerformableOperation operation, - AccessConfig accessConfig, String tenantDomain) { + AccessConfig accessConfig, String tenantDomain) + throws ActionExecutionResponseProcessorException { if (accessConfig == null || operation.getValue() == null) { return operation; @@ -479,6 +483,14 @@ private PerformableOperation decryptOperationValueIfNeeded(PerformableOperation // Only decrypt string values that look like JWE compact serialization. Object value = operation.getValue(); + if (!(value instanceof String)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Value for encrypted path " + operation.getPath() + + " is not a String. Using as-is."); + } + return operation; + } + String stringValue = (String) value; if (!JWEEncryptionUtil.isJWEEncrypted(stringValue)) { if (LOG.isDebugEnabled()) { @@ -499,9 +511,33 @@ private PerformableOperation decryptOperationValueIfNeeded(PerformableOperation } return decryptedOp; } catch (Exception e) { - LOG.warn("Failed to decrypt inbound JWE value for path: " + operation.getPath() + - ". Using raw value.", e); - return operation; + throw new ActionExecutionResponseProcessorException( + "Failed to decrypt inbound JWE value for path: " + operation.getPath(), e); + } + } + + /** + * Cache wrapper for claim URIs with TTL support. + */ + private static class CachedClaimUris { + + private final Set claimUris; + private final long createdAt; + + CachedClaimUris(Set claimUris) { + + this.claimUris = claimUris; + this.createdAt = System.currentTimeMillis(); + } + + Set getClaimUris() { + + return claimUris; + } + + boolean isExpired() { + + return (System.currentTimeMillis() - createdAt) > CLAIM_CACHE_TTL_MS; } } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java index 26794f7f6a1e..d795fd7f94eb 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java @@ -37,13 +37,16 @@ import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.security.Key; +import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateFactory; +import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.text.ParseException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; /** * Utility class for JWE encryption and decryption operations used by In-Flow Extension actions. @@ -60,8 +63,9 @@ public final class JWEEncryptionUtil { private static final Log LOG = LogFactory.getLog(JWEEncryptionUtil.class); - /** Cache of resolved private keys, keyed by tenant ID. */ - private static final Map PRIVATE_KEYS = new ConcurrentHashMap<>(); + /** Cache of resolved private keys with TTL, keyed by tenant ID. */ + private static final Map PRIVATE_KEYS = new ConcurrentHashMap<>(); + private static final long PRIVATE_KEY_CACHE_TTL_MS = TimeUnit.MINUTES.toMillis(30); private JWEEncryptionUtil() { @@ -143,9 +147,9 @@ private static Key getPrivateKey(String tenantDomain) throws ActionExecutionExce int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); String cacheKey = String.valueOf(tenantId); - Key cachedKey = PRIVATE_KEYS.get(cacheKey); - if (cachedKey != null) { - return cachedKey; + CachedKey cached = PRIVATE_KEYS.get(cacheKey); + if (cached != null && !cached.isExpired()) { + return cached.getKey(); } try { @@ -160,7 +164,7 @@ private static Key getPrivateKey(String tenantDomain) throws ActionExecutionExce privateKey = keyStoreManager.getPrivateKey(tenantKeyStoreName, tenantDomain); } - PRIVATE_KEYS.put(cacheKey, privateKey); + PRIVATE_KEYS.put(cacheKey, new CachedKey(privateKey)); return privateKey; } catch (Exception e) { throw new ActionExecutionException( @@ -211,8 +215,17 @@ public static X509Certificate parsePEMCertificate(String base64EncodedPem) throw byte[] decodedPemBytes = java.util.Base64.getDecoder().decode(base64EncodedPem.trim()); CertificateFactory factory = CertificateFactory.getInstance("X.509"); - return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(decodedPemBytes)); + X509Certificate certificate = + (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(decodedPemBytes)); + certificate.checkValidity(); + return certificate; + } catch (CertificateExpiredException e) { + throw new ActionExecutionException( + "Certificate has expired for In-Flow Extension action.", e); + } catch (CertificateNotYetValidException e) { + throw new ActionExecutionException( + "Certificate is not yet valid for In-Flow Extension action.", e); } catch (IllegalArgumentException e) { throw new ActionExecutionException( "Failed to decode certificate: Input is not valid Base64.", e); @@ -221,4 +234,29 @@ public static X509Certificate parsePEMCertificate(String base64EncodedPem) throw "Failed to parse the decoded PEM certificate for In-Flow Extension action.", e); } } + + /** + * Cache wrapper for private keys with TTL support. + */ + private static class CachedKey { + + private final Key key; + private final long createdAt; + + CachedKey(Key key) { + + this.key = key; + this.createdAt = System.currentTimeMillis(); + } + + Key getKey() { + + return key; + } + + boolean isExpired() { + + return (System.currentTimeMillis() - createdAt) > PRIVATE_KEY_CACHE_TTL_MS; + } + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java index 0f49ba756b45..faabf6b2600b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java @@ -200,9 +200,8 @@ private List parseContextPaths(Object value) { if (path != null) { result.add(new ContextPath(path, encrypted)); } - } - else { - + } else if (item instanceof String) { + result.add(new ContextPath((String) item, false)); } } return result.isEmpty() ? null : result; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java index 15dfd2451913..3d5985de4005 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java @@ -73,6 +73,12 @@ public class InFlowExtensionActionConstants { */ public static final int MAX_EXPOSE_PATHS = 50; + /** + * Property key for the action's icon URL stored in IDN_ACTION_PROPERTIES. + * Stored as a PRIMITIVE string. + */ + public static final String ICON_URL = "ICON_URL"; + /** * Property key for the external service's certificate stored in IDN_ACTION_PROPERTIES. * During persistence the certificate object is replaced with its UUID (PRIMITIVE string); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java index 6cdc9204318a..fde4dba7df4c 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java @@ -35,6 +35,7 @@ import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY_PREFIX; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.CERTIFICATE; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ICON_URL; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE_PREFIX; @@ -92,6 +93,12 @@ public ActionDTO buildActionDTO(Action action) { new ActionProperty.BuilderForService(encryption.getCertificate()).build()); } + // Icon URL. + if (inFlowExtensionAction.getIconUrl() != null) { + properties.put(ICON_URL, + new ActionProperty.BuilderForService(inFlowExtensionAction.getIconUrl()).build()); + } + // Per-flow-type overrides using prefixed keys. Map overrides = inFlowExtensionAction.getFlowTypeOverrides(); if (overrides != null) { @@ -142,6 +149,13 @@ public Action buildAction(ActionDTO actionDTO) { encryption = new Encryption((Certificate) certValue); } + // Icon URL. + String iconUrl = null; + Object iconUrlValue = actionDTO.getPropertyValue(ICON_URL); + if (iconUrlValue instanceof String) { + iconUrl = (String) iconUrlValue; + } + // Reconstruct per-flow-type overrides from prefixed keys. Map flowTypeOverrides = new HashMap<>(); if (actionDTO.getProperties() != null) { @@ -176,6 +190,7 @@ public Action buildAction(ActionDTO actionDTO) { .endpoint(actionDTO.getEndpoint()) .accessConfig(accessConfig) .encryption(encryption) + .iconUrl(iconUrl) .flowTypeOverrides(flowTypeOverrides) .rule(actionDTO.getActionRule()) .build(); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java index c8063a7b5b4b..ef49bec42fc6 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java @@ -47,6 +47,7 @@ import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY_PREFIX; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.CERTIFICATE; +import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ICON_URL; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE_PREFIX; import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.CERTIFICATE_NAME_PREFIX; @@ -110,6 +111,12 @@ public ActionDTO resolveForAddOperation(ActionDTO actionDTO, String tenantDomain // Handle certificate: store via CertificateManagementService and replace with ID. handleCertificateAdd(actionDTO, properties, tenantDomain); + // Handle icon URL: pass through as a PRIMITIVE string. + Object iconUrlValue = actionDTO.getPropertyValue(ICON_URL); + if (iconUrlValue instanceof String && !((String) iconUrlValue).isEmpty()) { + properties.put(ICON_URL, new ActionProperty.BuilderForDAO((String) iconUrlValue).build()); + } + // Handle per-flow-type override properties (prefixed keys). if (actionDTO.getProperties() != null) { for (Map.Entry entry : actionDTO.getProperties().entrySet()) { @@ -155,6 +162,12 @@ public ActionDTO resolveForGetOperation(ActionDTO actionDTO, String tenantDomain // Retrieve certificate by stored ID. handleCertificateGet(actionDTO, properties, tenantDomain); + // Icon URL: pass through as-is (already a PRIMITIVE string). + Object iconUrlValue = actionDTO.getPropertyValue(ICON_URL); + if (iconUrlValue != null) { + properties.put(ICON_URL, new ActionProperty.BuilderForService(iconUrlValue.toString()).build()); + } + // Deserialize per-flow-type override properties (prefixed keys). if (actionDTO.getProperties() != null) { for (Map.Entry entry : actionDTO.getProperties().entrySet()) { @@ -221,6 +234,15 @@ public ActionDTO resolveForUpdateOperation(ActionDTO updatingActionDTO, ActionDT // Handle certificate update. handleCertificateUpdate(updatingActionDTO, existingActionDTO, properties, tenantDomain); + // Handle icon URL update (PUT semantics: carry forward existing if not provided). + Object updatingIconUrl = updatingActionDTO.getPropertyValue(ICON_URL); + if (updatingIconUrl instanceof String && !((String) updatingIconUrl).isEmpty()) { + properties.put(ICON_URL, new ActionProperty.BuilderForDAO((String) updatingIconUrl).build()); + } else if (existingActionDTO.getPropertyValue(ICON_URL) != null) { + properties.put(ICON_URL, new ActionProperty.BuilderForDAO( + existingActionDTO.getPropertyValue(ICON_URL).toString()).build()); + } + // Handle per-flow-type override properties. Since DAO treats update as PUT (full replace), // we must carry forward all existing overrides that are not explicitly being updated. // First, carry forward all existing per-flow-type overrides. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java index c4539d783235..7cb125c34201 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java @@ -38,6 +38,7 @@ public class InFlowExtensionAction extends Action { private final AccessConfig accessConfig; private final Encryption encryption; private final Map flowTypeOverrides; + private final String iconUrl; public InFlowExtensionAction(ResponseBuilder responseBuilder) { @@ -47,6 +48,7 @@ public InFlowExtensionAction(ResponseBuilder responseBuilder) { this.flowTypeOverrides = responseBuilder.flowTypeOverrides != null ? Collections.unmodifiableMap(new HashMap<>(responseBuilder.flowTypeOverrides)) : Collections.emptyMap(); + this.iconUrl = responseBuilder.iconUrl; } public InFlowExtensionAction(RequestBuilder requestBuilder) { @@ -57,6 +59,7 @@ public InFlowExtensionAction(RequestBuilder requestBuilder) { this.flowTypeOverrides = requestBuilder.flowTypeOverrides != null ? Collections.unmodifiableMap(new HashMap<>(requestBuilder.flowTypeOverrides)) : Collections.emptyMap(); + this.iconUrl = requestBuilder.iconUrl; } /** @@ -79,6 +82,16 @@ public Encryption getEncryption() { return encryption; } + /** + * Returns the icon URL for this In-Flow Extension action. + * + * @return The icon URL, or {@code null} if not configured. + */ + public String getIconUrl() { + + return iconUrl; + } + /** * Returns the per-flow-type access config overrides. * Keys are flow type strings (e.g., "REGISTRATION", "LOGIN"). @@ -114,6 +127,7 @@ public static class ResponseBuilder extends ActionResponseBuilder { private AccessConfig accessConfig; private Encryption encryption; private Map flowTypeOverrides; + private String iconUrl; @Override public ResponseBuilder id(String id) { @@ -203,6 +217,12 @@ public ResponseBuilder flowTypeOverrides(Map flowTypeOverr return this; } + public ResponseBuilder iconUrl(String iconUrl) { + + this.iconUrl = iconUrl; + return this; + } + @Override public InFlowExtensionAction build() { @@ -219,6 +239,7 @@ public static class RequestBuilder extends ActionRequestBuilder { private AccessConfig accessConfig; private Encryption encryption; private Map flowTypeOverrides; + private String iconUrl; public RequestBuilder(Action action) { @@ -268,6 +289,12 @@ public RequestBuilder flowTypeOverrides(Map flowTypeOverri return this; } + public RequestBuilder iconUrl(String iconUrl) { + + this.iconUrl = iconUrl; + return this; + } + @Override public InFlowExtensionAction build() { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java index 3bdd6d46530f..c6e4c70394ee 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java @@ -55,6 +55,7 @@ import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; /** * Unit tests for {@link InFlowExtensionRequestBuilder}. @@ -496,6 +497,38 @@ public void testUserInputsEncryptedWhenExposePathMarkedEncrypted() } } + // ========================= Outbound encryption failure ========================= + + @Test(expectedExceptions = ActionExecutionRequestBuilderException.class) + public void testEncryptionFailureThrowsException() + throws ActionExecutionRequestBuilderException { + + try (MockedStatic jweUtilMock = mockStatic(JWEEncryptionUtil.class)) { + jweUtilMock.when(() -> JWEEncryptionUtil.encrypt(anyString(), anyString())) + .thenThrow(new RuntimeException("Encryption failed")); + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + execCtx.setProperty("riskScore", "85"); + + AccessConfig accessConfig = new AccessConfig( + Arrays.asList(new ContextPath("/properties/riskScore", true)), + null); + + Encryption encryption = new Encryption( + new Certificate.Builder().id("cert-1").name("test") + .certificateContent("test-cert-pem").build()); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) + .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE) + .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) + .add(InFlowExtensionExecutor.ENCRYPTION_KEY, encryption); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + } + } + // ========================= Helper methods ========================= private FlowExecutionContext createMinimalFlowExecutionContext() { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java index fee84576b494..b5be1bea6229 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java @@ -38,6 +38,9 @@ import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionResponseProcessor; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.JWEEncryptionUtil; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; @@ -668,4 +671,75 @@ private ActionExecutionStatus executeSuccessResponse( return responseProcessor.processSuccessResponse(flowContext, responseContext); } + + @SuppressWarnings("unchecked") + private ActionExecutionStatus executeSuccessResponseWithAccessConfig( + FlowExecutionContext execCtx, List operations, + Map pathTypeAnnotations, AccessConfig accessConfig) + throws ActionExecutionResponseProcessorException { + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + if (pathTypeAnnotations != null && !pathTypeAnnotations.isEmpty()) { + flowContext.add(InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); + } + if (accessConfig != null) { + flowContext.add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig); + } + + ActionInvocationSuccessResponse successResponse = mock(ActionInvocationSuccessResponse.class); + when(successResponse.getOperations()).thenReturn(operations); + + ActionExecutionResponseContext responseContext = + mock(ActionExecutionResponseContext.class); + when(responseContext.getActionInvocationResponse()).thenReturn(successResponse); + + return responseProcessor.processSuccessResponse(flowContext, responseContext); + } + + // ========================= Inbound decryption failure ========================= + + @Test(expectedExceptions = ActionExecutionResponseProcessorException.class) + public void testDecryptionFailureThrowsException() + throws ActionExecutionResponseProcessorException { + + try (MockedStatic jweUtilMock = mockStatic(JWEEncryptionUtil.class)) { + jweUtilMock.when(() -> JWEEncryptionUtil.isJWEEncrypted(anyString())).thenReturn(true); + jweUtilMock.when(() -> JWEEncryptionUtil.decrypt(anyString(), anyString())) + .thenThrow(new RuntimeException("Decryption failed")); + + FlowExecutionContext execCtx = createFlowExecutionContext(); + // Create AccessConfig with an encrypted modify path. + AccessConfig accessConfig = new AccessConfig(null, + Arrays.asList(new ContextPath("/properties/secret", true))); + + // Value looks like JWE (5 dot-separated parts). + PerformableOperation op = createOperation( + Operation.REPLACE, "/properties/secret", "a.b.c.d.e"); + + executeSuccessResponseWithAccessConfig(execCtx, Collections.singletonList(op), + Collections.emptyMap(), accessConfig); + } + } + + @Test + public void testNonStringValueForEncryptedPathHandledGracefully() + throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + // Create AccessConfig with an encrypted modify path. + AccessConfig accessConfig = new AccessConfig(null, + Arrays.asList(new ContextPath("/properties/data", true))); + + // Non-string value (Integer) for an encrypted path — should pass through without decryption. + PerformableOperation op = createOperation(Operation.REPLACE, "/properties/data", 42); + + ActionExecutionStatus status = executeSuccessResponseWithAccessConfig( + execCtx, Collections.singletonList(op), Collections.emptyMap(), accessConfig); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + // Value should be coerced to String (default behavior for properties). + assertEquals(execCtx.getProperties().get("data"), "42"); + } } From 14c6366676be3bb30f2999da66ba7a8941712f1e Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Fri, 17 Apr 2026 15:04:12 +0530 Subject: [PATCH 12/41] Removed flow update interceptor - direct action api call for overrides Removed default fallback for access configuration in prefix matcher - configured to expose nothing and modify nothing as fallback. Added package import slf4j to flow execution engine - handles correlation id missing issue in flow execution context. --- .../pom.xml | 1 + .../executor/HierarchicalPrefixMatcher.java | 31 +-- .../executor/InFlowExtensionExecutor.java | 7 +- .../InFlowExtensionRequestBuilder.java | 24 +- .../InFlowExtensionResponseProcessor.java | 1 - .../AccessConfigFlowUpdateInterceptor.java | 227 ------------------ .../InFlowExtensionActionConstants.java | 14 -- .../InFlowExtensionActionConverter.java | 4 + ...InFlowExtensionActionDTOModelResolver.java | 16 +- .../inflow/extension/model/AccessConfig.java | 12 +- .../FlowExecutionEngineServiceComponent.java | 5 - .../HierarchicalPrefixMatcherTest.java | 20 -- .../executor/InFlowExtensionExecutorTest.java | 4 +- .../InFlowExtensionRequestBuilderTest.java | 32 +-- .../engine/model/AccessConfigTest.java | 6 +- .../identity/flow/mgt/FlowMgtService.java | 8 - .../flow/mgt/FlowUpdateInterceptor.java | 52 ---- .../mgt/internal/FlowMgtServiceComponent.java | 17 -- .../internal/FlowMgtServiceDataHolder.java | 20 -- 19 files changed, 66 insertions(+), 435 deletions(-) delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowUpdateInterceptor.java diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml index 50692dfe1ab3..04588fca4ac4 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml @@ -196,6 +196,7 @@ 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.wso2.carbon.identity.flow.execution.internal, diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java index 18bd70d74582..a34d77fa1691 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java @@ -18,8 +18,6 @@ package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; -import java.util.Arrays; -import java.util.Collections; import java.util.List; /** @@ -66,13 +64,6 @@ public final class HierarchicalPrefixMatcher { public static final String FLOW_APP_ID_PATH = "/flow/applicationId"; public static final String FLOW_TYPE_PATH = "/flow/flowType"; - /** - * Default expose configuration — all context areas are exposed. - * Used when no explicit expose configuration is provided by the executor metadata. - */ - public static final List DEFAULT_EXPOSE = Collections.unmodifiableList( - Arrays.asList(USER_PREFIX, PROPERTIES_PREFIX, INPUT_PREFIX, FLOW_PREFIX)); - /** * Context area enum for categorization. */ @@ -254,19 +245,23 @@ public static boolean requiresSystemKeyValidation(String path) { } /** - * Check if a path matches any of the given expose prefixes using bidirectional prefix matching. - * A match occurs if the path starts with a prefix (prefix covers path) OR the prefix starts - * with the path (path is a parent of prefix). + * Check if a path matches any of the given expose paths using bidirectional prefix matching. * - *

      Examples:

      + *

      Expose paths are always leaf-level (e.g. {@code /user/claims/http://wso2.org/claims/email}). + * Bidirectional matching is required because this method serves two distinct purposes:

      *
        - *
      • {@code /user/claims/email} matches prefix {@code /user/} → prefix covers path
      • - *
      • {@code /user/} matches prefix {@code /user/claims/email} → path is parent of prefix
      • + *
      • Area-level gate checks: {@code path} is an area prefix (e.g. {@code /user/}). + * Direction 2 ({@code prefix.startsWith(path)}) detects that a leaf expose path + * falls under this area — e.g. {@code /user/claims/email} starts with {@code /user/}.
      • + *
      • Leaf-level filtering: {@code path} is a specific runtime path + * (e.g. {@code /user/claims/http://wso2.org/claims/email}). + * Direction 1 ({@code path.startsWith(prefix)}) performs exact match against + * leaf expose paths.
      • *
      * - * @param path The path to check. - * @param exposePrefixes The list of expose prefixes. - * @return {@code true} if the path matches any expose prefix. + * @param path The path to check (can be an area prefix or a specific leaf path). + * @param exposePrefixes The list of leaf-level expose paths. + * @return {@code true} if the path matches any expose path. */ public static boolean matchesAnyExpose(String path, List exposePrefixes) { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java index 5f4fd60307bd..d6a58c46ebf9 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java @@ -41,7 +41,6 @@ import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; import org.wso2.carbon.identity.flow.mgt.model.NodeConfig; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -69,7 +68,7 @@ public class InFlowExtensionExecutor implements Executor { private static final Log LOG = LogFactory.getLog(InFlowExtensionExecutor.class); - private static final String EXECUTOR_NAME = "ExtensionExecutor"; + private static final String EXECUTOR_NAME = "InFlowExtensionExecutor"; public static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; public static final String EXPOSE_KEY = "expose"; @@ -109,8 +108,8 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE if (resolvedConfig != null && resolvedConfig.getExpose() != null) { expose = resolvedConfig.getExposePaths(); } else { - // No access config on action — use system defaults. - expose = new ArrayList<>(HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + // No access config on action — expose nothing by default. + expose = Collections.emptyList(); } FlowContext flowContext = FlowContext.create() diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java index 86952e0f623a..ab458a2553e1 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java @@ -81,7 +81,7 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex List expose = flowContext.getValue(InFlowExtensionExecutor.EXPOSE_KEY, List.class); if (expose == null) { - expose = HierarchicalPrefixMatcher.DEFAULT_EXPOSE; + expose = Collections.emptyList(); } // Read access config for encryption metadata. @@ -271,11 +271,9 @@ private User buildUser(FlowUser flowUser, List expose, Map claims = flowUser.getClaims(); if (claims != null && !claims.isEmpty()) { List userClaims = new ArrayList<>(); - boolean hasSpecificFilter = hasSpecificSubPathFilter( - expose, HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX); for (Map.Entry claim : claims.entrySet()) { - if (!hasSpecificFilter || isExposed( + if (isExposed( HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX + claim.getKey(), expose)) { String claimValue = claim.getValue(); // Encrypt claim value if the expose path is marked as encrypted. @@ -337,12 +335,10 @@ private Map filterMap(Map map, String areaPrefix, List return null; } - boolean hasSpecificFilter = hasSpecificSubPathFilter(expose, areaPrefix); - Map filtered = new HashMap<>(); for (Map.Entry entry : map.entrySet()) { String fullPath = areaPrefix + entry.getKey(); - if (!hasSpecificFilter || isExposed(fullPath, expose)) { + if (isExposed(fullPath, expose)) { T value = entry.getValue(); if (shouldEncrypt(fullPath, accessConfig, certificatePEM)) { value = (T) encryptValue(String.valueOf(value), certificatePEM); @@ -361,20 +357,6 @@ private boolean isExposed(String path, List expose) { return HierarchicalPrefixMatcher.matchesAnyExpose(path, expose); } - /** - * Check whether the expose list contains specific sub-paths under a given area prefix, - * indicating that per-key filtering is required rather than exposing the entire area. - */ - private boolean hasSpecificSubPathFilter(List expose, String areaPrefix) { - - for (String prefix : expose) { - if (prefix.startsWith(areaPrefix) && !prefix.equals(areaPrefix)) { - return true; - } - } - return false; - } - /** * Determine if a value at the given path should be JWE-encrypted before sending to the * external service. Only expose paths with {@code encrypted: true} trigger outbound encryption. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java index 87c454aec8f7..2e166b6e1d8a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java @@ -299,7 +299,6 @@ private OperationExecutionResult handleUserInputOperation(PerformableOperation o "User input replace applied."); } - //TODO: These validations can be removed once the attribute executor is introduced. /** * Validate if a claim URI exists in the local claim dialect. */ diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java deleted file mode 100644 index faabf6b2600b..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/AccessConfigFlowUpdateInterceptor.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (c) 2025, 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.inflow.extension.management; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.wso2.carbon.identity.action.management.api.exception.ActionMgtException; -import org.wso2.carbon.identity.action.management.api.model.Action; -import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; -import org.wso2.carbon.identity.core.util.IdentityTenantUtil; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; -import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; -import org.wso2.carbon.identity.flow.mgt.FlowUpdateInterceptor; -import org.wso2.carbon.identity.flow.mgt.exception.FlowMgtFrameworkException; -import org.wso2.carbon.identity.flow.mgt.exception.FlowMgtServerException; -import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; -import org.wso2.carbon.identity.flow.mgt.model.GraphConfig; -import org.wso2.carbon.identity.flow.mgt.model.NodeConfig; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_METADATA_KEY; - -/** - * Flow update interceptor that extracts access config overrides from executor metadata - * and stores them as per-flow-type action properties. - *

      - * This interceptor is invoked before the flow graph is persisted. It: - *

        - *
      1. Iterates graph nodes looking for In-Flow Extension executors with an - * {@code accessConfig} metadata entry.
      2. - *
      3. Parses the unified {@code accessConfig} JSON object - * ({@code {"expose": [...], "modify": [...]}}).
      4. - *
      5. Saves the parsed data as per-flow-type override properties on the corresponding action - * via {@code ActionManagementService.updateAction()}.
      6. - *
      7. Strips the {@code accessConfig} key from executor metadata so it is NOT persisted - * to the flow executor metadata table (avoiding VARCHAR(255) column size limitations).
      8. - *
      - *

      - */ -public class AccessConfigFlowUpdateInterceptor implements FlowUpdateInterceptor { - - private static final Log LOG = LogFactory.getLog(AccessConfigFlowUpdateInterceptor.class); - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final String IN_FLOW_EXTENSION_EXECUTOR_NAME = "ExtensionExecutor"; - private static final TypeReference> MAP_TYPE_REF = - new TypeReference>() { }; - - private static final String ACTION_ID_METADATA_KEY = "actionId"; - private static final String EXPOSE_FIELD = "expose"; - private static final String MODIFY_FIELD = "modify"; - - @Override - public void onFlowUpdate(String flowType, GraphConfig graphConfig, int tenantId) - throws FlowMgtFrameworkException { - - if (graphConfig == null || graphConfig.getNodeConfigs() == null) { - return; - } - - ActionManagementService actionMgtService = - FlowExecutionEngineDataHolder.getInstance().getActionManagementService(); - if (actionMgtService == null) { - LOG.warn("ActionManagementService is not available. Skipping access config override processing."); - return; - } - - String tenantDomain = IdentityTenantUtil.getTenantDomain(tenantId); - - for (NodeConfig node : graphConfig.getNodeConfigs().values()) { - processNode(node, flowType, tenantDomain, actionMgtService); - } - } - - /** - * Process a single node: extract unified accessConfig from executor metadata, save as action - * properties, and strip the accessConfig key from metadata before DAO persistence. - */ - @SuppressWarnings("unchecked") - private void processNode(NodeConfig node, String flowType, String tenantDomain, - ActionManagementService actionMgtService) throws FlowMgtFrameworkException { - - ExecutorDTO executorConfig = node.getExecutorConfig(); - if (executorConfig == null || !executorConfig.getName().equals(IN_FLOW_EXTENSION_EXECUTOR_NAME) - ||executorConfig.getMetadata() == null) { - return; - } - - Map metadata = executorConfig.getMetadata(); - String actionId = metadata.get(ACTION_ID_METADATA_KEY); - if (actionId == null || actionId.isEmpty()) { - return; - } - - String accessConfigJson = metadata.get(ACCESS_CONFIG_METADATA_KEY); - if (accessConfigJson == null) { - return; - } - - // Always strip the accessConfig key from metadata — it must NOT be persisted - // to executor metadata (VARCHAR(255) column size limitation). - metadata.remove(ACCESS_CONFIG_METADATA_KEY); - - try { - // Parse the unified accessConfig JSON: {"expose": [...], "modify": [...]} - Map accessConfigMap = OBJECT_MAPPER.readValue(accessConfigJson, MAP_TYPE_REF); - - List expose = parseContextPaths(accessConfigMap.get(EXPOSE_FIELD)); - List modify = parseContextPaths(accessConfigMap.get(MODIFY_FIELD)); - // Certificate is action-level only — not overridable per flow type. - AccessConfig overrideConfig = new AccessConfig(expose, modify); - - // Retrieve the existing action to merge overrides (preserving other flow types). - Action currentAction = actionMgtService.getActionByActionId( - Action.ActionTypes.IN_FLOW_EXTENSION.getPathParam(), actionId, tenantDomain); - if (!(currentAction instanceof InFlowExtensionAction)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Action " + actionId + " is not an InFlowExtensionAction. Skipping override."); - } - return; - } - - InFlowExtensionAction existingAction = (InFlowExtensionAction) currentAction; - - // Merge: preserve other flow types, add/replace this one. - Map overrides = new HashMap<>(existingAction.getFlowTypeOverrides()); - overrides.put(flowType, overrideConfig); - - InFlowExtensionAction updatedAction = new InFlowExtensionAction.RequestBuilder(existingAction) - .accessConfig(existingAction.getAccessConfig()) - .flowTypeOverrides(overrides) - .build(); - - actionMgtService.updateAction( - Action.ActionTypes.IN_FLOW_EXTENSION.getPathParam(), - actionId, updatedAction, tenantDomain); - - if (LOG.isDebugEnabled()) { - LOG.debug("Updated access config override for action " + actionId - + " in flow type " + flowType + " and stripped from executor metadata."); - } - - } catch (ActionMgtException e) { - throw new FlowMgtServerException("FLOW-60010", - "Error updating access config override.", - "Error updating access config override for action: " + actionId, e); - } catch (JsonProcessingException e) { - throw new FlowMgtServerException("FLOW-60011", - "Error parsing access config metadata.", - "Error parsing access config metadata for action: " + actionId, e); - } - } - - /** - * Parse expose entries from the raw JSON value. - * Accepts both simple strings (no encryption) and objects ({path, encrypted}). - */ - @SuppressWarnings("unchecked") - private List parseContextPaths(Object value) { - - if (value == null) { - return null; - } - if (!(value instanceof List)) { - LOG.warn("Invalid expose value in flow override accessConfig. Expected list."); - return null; - } - - List rawList = (List) value; - List result = new ArrayList<>(); - for (Object item : rawList) { - if (item instanceof Map) { - Map map = (Map) item; - String path = (String) map.get("path"); - boolean encrypted = toBooleanSafe(map.get("encrypted")); - if (path != null) { - result.add(new ContextPath(path, encrypted)); - } - } else if (item instanceof String) { - result.add(new ContextPath((String) item, false)); - } - } - return result.isEmpty() ? null : result; - } - - /** - * Safely converts a value to boolean, handling both {@link Boolean} and {@link String} types. - * Jackson deserializes JSON {@code true} as {@link Boolean} but JSON {@code "true"} as {@link String}. - * - * @param value The value to convert. - * @return {@code true} if the value is Boolean TRUE or the string "true" (case-insensitive). - */ - private static boolean toBooleanSafe(Object value) { - - if (value instanceof Boolean) { - return (Boolean) value; - } - if (value instanceof String) { - return Boolean.parseBoolean((String) value); - } - return false; - } -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java index 3d5985de4005..009fa1053c1b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java @@ -54,20 +54,6 @@ public class InFlowExtensionActionConstants { public static final String ACCESS_CONFIG_MODIFY_PREFIX = ACCESS_CONFIG_MODIFY + OVERRIDE_KEY_SEPARATOR; - /** - * Metadata key used in executor metadata (flow configuration) to pass a unified access config - * object containing both {@code expose} and {@code allowedOperations}. - *

      - * The value is a JSON object: {@code {"expose": [...], "allowedOperations": [...]}} - *

      - *

      - * This key is extracted by the {@code AccessConfigFlowUpdateInterceptor} during flow updates, - * stored as per-flow-type action properties, and stripped from executor metadata before - * the flow graph is persisted (to avoid VARCHAR(255) column size limitations). - *

      - */ - public static final String ACCESS_CONFIG_METADATA_KEY = "accessConfig"; - /** * Maximum number of expose path prefixes allowed per action. */ diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java index fde4dba7df4c..fe1f62d1f74a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java @@ -91,6 +91,10 @@ public ActionDTO buildActionDTO(Action action) { if (encryption != null && encryption.getCertificate() != null) { properties.put(CERTIFICATE, new ActionProperty.BuilderForService(encryption.getCertificate()).build()); + } else if (encryption != null) { + // Encryption object present but no certificate — signals explicit removal. + properties.put(CERTIFICATE, + new ActionProperty.BuilderForService("").build()); } // Icon URL. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java index ef49bec42fc6..670fcc550004 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java @@ -451,7 +451,19 @@ private void handleCertificateUpdate(ActionDTO updatingActionDTO, ActionDTO exis Object newCertValue = updatingActionDTO.getPropertyValue(CERTIFICATE); Object existingCertValue = existingActionDTO.getPropertyValue(CERTIFICATE); - if (newCertValue != null && existingCertValue != null) { + // Empty string signals explicit certificate removal. + boolean isExplicitRemoval = newCertValue instanceof String && ((String) newCertValue).isEmpty(); + + if (isExplicitRemoval && existingCertValue != null) { + // Explicitly clearing the certificate — delete the existing one. + try { + String existingCertId = extractCertificateId(existingCertValue); + certificateManagementService.deleteCertificate(existingCertId, tenantDomain); + } catch (CertificateMgtException e) { + throw new ActionDTOModelResolverException("Error deleting certificate for action: " + + updatingActionDTO.getId(), e); + } + } else if (newCertValue != null && !isExplicitRemoval && existingCertValue != null) { // Update existing certificate. String certificatePEM = extractCertificatePEM(newCertValue); try { @@ -465,7 +477,7 @@ private void handleCertificateUpdate(ActionDTO updatingActionDTO, ActionDTO exis throw new ActionDTOModelResolverException("Error updating certificate for action: " + updatingActionDTO.getId(), e); } - } else if (newCertValue != null) { + } else if (newCertValue != null && !isExplicitRemoval) { // Add new certificate (previously had none). handleCertificateAdd(updatingActionDTO, properties, tenantDomain); } else if (existingCertValue != null) { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java index 2fcf3165a3e4..887d0d379075 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java @@ -113,20 +113,20 @@ public List getModifyPaths() { } /** - * Check if a given path prefix has outbound encryption enabled. - * Matches the most specific (longest) expose path that is a prefix of the given path. + * Check if a given expose path has outbound encryption enabled. + * With leaf-only expose paths, this performs an exact match lookup. * - * @param pathPrefix The path to check. + * @param path The expose path to check. * @return {@code true} if the matching expose entry has {@code encrypted = true}. */ - public boolean isExposePathEncrypted(String pathPrefix) { + public boolean isExposePathEncrypted(String path) { if (expose == null) { return false; } return expose.stream() - .filter(ep -> pathPrefix.startsWith(ep.getPath())) - .reduce((a, b) -> a.getPath().length() >= b.getPath().length() ? a : b) + .filter(ep -> ep.getPath().equals(path)) + .findFirst() .map(ContextPath::isEncrypted) .orElse(false); } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java index e32c2c758ca2..315519e53fc2 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java @@ -43,13 +43,11 @@ import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionRequestBuilder; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionResponseProcessor; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.AccessConfigFlowUpdateInterceptor; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConverter; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionDTOModelResolver; import org.wso2.carbon.identity.flow.execution.engine.listener.FlowExecutionListener; import org.wso2.carbon.identity.flow.execution.engine.validation.InputProcessingListener; import org.wso2.carbon.identity.flow.mgt.FlowMgtService; -import org.wso2.carbon.identity.flow.mgt.FlowUpdateInterceptor; import org.wso2.carbon.identity.input.validation.mgt.services.InputValidationManagementService; import org.wso2.carbon.identity.user.profile.mgt.association.federation.FederatedAssociationManager; import org.wso2.carbon.user.core.service.RealmService; @@ -107,9 +105,6 @@ protected void activate(ComponentContext context) { FlowExecutionEngineDataHolder.getInstance().getCertificateManagementService()), null); - bundleContext.registerService(FlowUpdateInterceptor.class.getName(), - new AccessConfigFlowUpdateInterceptor(), null); - LOG.debug("Flow Engine service successfully activated."); } catch (Throwable e) { LOG.error("Error while initiating Flow Engine service", e); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java index 96ad757a465a..a5e1d26cb0e7 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java @@ -271,26 +271,6 @@ public void testMatchesAnyExposeMultiplePrefixesOneMatches() { assertTrue(HierarchicalPrefixMatcher.matchesAnyExpose("/properties/score", expose)); } - // ========================= DEFAULT_EXPOSE ========================= - - @Test - public void testDefaultExposeContainsAllAreas() { - - List defaultExpose = HierarchicalPrefixMatcher.DEFAULT_EXPOSE; - assertNotNull(defaultExpose); - assertEquals(defaultExpose.size(), 4); - assertTrue(defaultExpose.contains("/user/")); - assertTrue(defaultExpose.contains("/properties/")); - assertTrue(defaultExpose.contains("/input/")); - assertTrue(defaultExpose.contains("/flow/")); - } - - @Test(expectedExceptions = UnsupportedOperationException.class) - public void testDefaultExposeIsUnmodifiable() { - - HierarchicalPrefixMatcher.DEFAULT_EXPOSE.add("/new/"); - } - // ========================= ContextArea enum ========================= @Test diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java index eb20a53dacdd..decb9be348b6 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java @@ -95,7 +95,7 @@ public void tearDown() throws Exception { @Test public void testGetName() { - assertEquals(executor.getName(), "ExtensionExecutor"); + assertEquals(executor.getName(), "InFlowExtensionExecutor"); } // ========================= getInitiationData ========================= @@ -538,7 +538,7 @@ private FlowExecutionContext createContextWithMetadata(Map metad FlowExecutionContext context = new FlowExecutionContext(); context.setTenantDomain("carbon.super"); - ExecutorDTO executorDTO = new ExecutorDTO("extensionExecutor", metadata); + ExecutorDTO executorDTO = new ExecutorDTO("InFlowExtensionExecutor", metadata); NodeConfig nodeConfig = new NodeConfig.Builder() .executorConfig(executorDTO) .build(); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java index c6e4c70394ee..8fc4ebd61ca3 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java @@ -30,7 +30,6 @@ import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.HierarchicalPrefixMatcher; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionEvent; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionRequestBuilder; @@ -62,6 +61,8 @@ */ public class InFlowExtensionRequestBuilderTest { + private static final List ALL_EXPOSE = Arrays.asList("/user/", "/properties/", "/input/", "/flow/"); + private InFlowExtensionRequestBuilder requestBuilder; private MockedStatic identityTenantUtilMock; @@ -114,7 +115,7 @@ public void testBuildRequestWithMinimalContext() throws ActionExecutionRequestBu } @Test - public void testBuildRequestUsesDefaultExposeWhenExposeIsNull() + public void testBuildRequestUsesEmptyExposeWhenExposeIsNull() throws ActionExecutionRequestBuilderException { FlowExecutionContext execCtx = createFullFlowExecutionContext(); @@ -124,11 +125,12 @@ public void testBuildRequestUsesDefaultExposeWhenExposeIsNull() ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); - // With DEFAULT_EXPOSE, all areas are exposed — event should have user, properties, inputs, etc. + // With empty expose, no context areas should be included in the event. InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); assertNotNull(event); - assertNotNull(event.getFlowType()); - assertNotNull(event.getUserInputs()); + assertNull(event.getFlowType()); + // userInputs defaults to emptyMap() in the builder — verify it is empty. + assertTrue(event.getUserInputs().isEmpty()); } // ========================= buildAllowedOperations (from modify) ========================= @@ -164,7 +166,7 @@ public void testBuildRequestWithNoAccessConfig() FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -184,7 +186,7 @@ public void testBuildRequestWithEmptyModifyPaths() FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -208,7 +210,7 @@ public void testPathAnnotationStrippingSimpleArray() FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -237,7 +239,7 @@ public void testPathAnnotationStrippingSchemaAnnotation() FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -263,7 +265,7 @@ public void testPathWithoutAnnotationNotStoredInAnnotationsMap() FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -288,7 +290,7 @@ public void testMultipleAnnotatedPaths() FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -352,7 +354,7 @@ public void testMultipleModifyPathsProduceSingleReplaceOperation() FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE); + .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -438,7 +440,7 @@ public void testPropertiesEncryptedWhenExposePathMarkedEncrypted() FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE) + .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE) .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) .add(InFlowExtensionExecutor.ENCRYPTION_KEY, encryption); @@ -478,7 +480,7 @@ public void testUserInputsEncryptedWhenExposePathMarkedEncrypted() FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE) + .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE) .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) .add(InFlowExtensionExecutor.ENCRYPTION_KEY, encryption); @@ -520,7 +522,7 @@ public void testEncryptionFailureThrowsException() FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.EXPOSE_KEY, HierarchicalPrefixMatcher.DEFAULT_EXPOSE) + .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE) .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) .add(InFlowExtensionExecutor.ENCRYPTION_KEY, encryption); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/AccessConfigTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/AccessConfigTest.java index 92dc6bc0651a..8d752e6d4c15 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/AccessConfigTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/AccessConfigTest.java @@ -95,8 +95,8 @@ public void testGetModifyPaths() { public void testIsExposePathEncryptedMatchesLongestPrefix() { List exposePaths = Arrays.asList( - new ContextPath("/user/", false), - new ContextPath("/user/claims/", true) + new ContextPath("/user/claims/email", true), + new ContextPath("/user/username", false) ); AccessConfig config = new AccessConfig(exposePaths, null); @@ -108,7 +108,7 @@ public void testIsExposePathEncryptedMatchesLongestPrefix() { public void testIsExposePathEncryptedNoMatch() { List exposePaths = Collections.singletonList( - new ContextPath("/user/claims/", true) + new ContextPath("/user/claims/email", true) ); AccessConfig config = new AccessConfig(exposePaths, null); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowMgtService.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowMgtService.java index b1c2ea36c13b..11208f233c06 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowMgtService.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowMgtService.java @@ -92,14 +92,6 @@ public void updateFlow(FlowDTO flowDTO, int tenantID) clearFlowResolveCache(flowDTO.getFlowType(), tenantID); GraphConfig flowConfig = new GraphBuilder().withSteps(flowDTO.getSteps()).build(); - // Notify interceptors BEFORE persistence. Interceptors may extract and strip - // metadata from the graph (e.g., access config overrides) so that the stripped - // data is NOT persisted to executor metadata but is stored elsewhere. - for (FlowUpdateInterceptor interceptor : - FlowMgtServiceDataHolder.getInstance().getFlowUpdateInterceptors()) { - interceptor.onFlowUpdate(flowDTO.getFlowType(), flowConfig, tenantID); - } - FLOW_DAO.updateFlow(flowDTO.getFlowType(), flowConfig, tenantID, DEFAULT_FLOW_NAME); AuditLog.AuditLogBuilder auditLogBuilder = diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowUpdateInterceptor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowUpdateInterceptor.java deleted file mode 100644 index a418cdf2bbc4..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowUpdateInterceptor.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2025, 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.mgt; - -import org.wso2.carbon.identity.flow.mgt.exception.FlowMgtFrameworkException; -import org.wso2.carbon.identity.flow.mgt.model.GraphConfig; - -/** - * Interceptor interface for flow update operations. - *

      - * Implementations can hook into the flow update lifecycle to perform additional - * processing when a flow is updated. Interceptors are invoked before the - * graph is persisted to the database, allowing them to: - *

        - *
      • Extract data from executor metadata for external storage (e.g., access config overrides - * stored as action properties instead of executor metadata).
      • - *
      • Strip extracted keys from executor metadata so they are not persisted to the - * flow executor metadata table (e.g., to avoid column size limitations).
      • - *
      - *

      - */ -public interface FlowUpdateInterceptor { - - /** - * Called after the flow graph has been built but before it is persisted to the database. - * Implementations may modify the {@code graphConfig} (e.g., stripping metadata keys) and the - * modifications will be reflected in the persisted graph. - * - * @param flowType The flow type (e.g., "REGISTRATION", "LOGIN"). - * @param graphConfig The built graph configuration containing node and executor details. - * Mutable — interceptors may modify executor metadata. - * @param tenantId The tenant ID. - * @throws FlowMgtFrameworkException If an error occurs during interception. - */ - void onFlowUpdate(String flowType, GraphConfig graphConfig, int tenantId) throws FlowMgtFrameworkException; -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceComponent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceComponent.java index d1cfbfc71df0..d10eaa7b501d 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceComponent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceComponent.java @@ -32,7 +32,6 @@ import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager; import org.wso2.carbon.identity.flow.mgt.FlowAIService; import org.wso2.carbon.identity.flow.mgt.FlowMgtService; -import org.wso2.carbon.identity.flow.mgt.FlowUpdateInterceptor; import org.wso2.carbon.identity.organization.management.service.OrganizationManager; import org.wso2.carbon.identity.organization.resource.hierarchy.traverse.service.OrgResourceResolverService; @@ -131,20 +130,4 @@ protected void unsetCompatibilitySettingsManager(CompatibilitySettingsManager co FlowMgtServiceDataHolder.getInstance().setCompatibilitySettingsManager(null); } - - @Reference( - name = "flow.update.interceptor", - service = FlowUpdateInterceptor.class, - cardinality = ReferenceCardinality.MULTIPLE, - policy = ReferencePolicy.DYNAMIC, - unbind = "unsetFlowUpdateInterceptor") - protected void setFlowUpdateInterceptor(FlowUpdateInterceptor interceptor) { - - FlowMgtServiceDataHolder.getInstance().addFlowUpdateInterceptor(interceptor); - } - - protected void unsetFlowUpdateInterceptor(FlowUpdateInterceptor interceptor) { - - FlowMgtServiceDataHolder.getInstance().removeFlowUpdateInterceptor(interceptor); - } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceDataHolder.java index 7e21dde44e80..371cf58366b2 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceDataHolder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceDataHolder.java @@ -20,13 +20,9 @@ import org.wso2.carbon.identity.compatibility.settings.core.CompatibilitySettingsManager; import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager; -import org.wso2.carbon.identity.flow.mgt.FlowUpdateInterceptor; import org.wso2.carbon.identity.organization.management.service.OrganizationManager; import org.wso2.carbon.identity.organization.resource.hierarchy.traverse.service.OrgResourceResolverService; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; /** * A singleton class to hold the data of the flow management service. @@ -37,7 +33,6 @@ public class FlowMgtServiceDataHolder { private OrgResourceResolverService orgResourceResolverService; private ConfigurationManager configurationManager; private CompatibilitySettingsManager compatibilitySettingsManager; - private final List flowUpdateInterceptors = new ArrayList<>(); private static final FlowMgtServiceDataHolder INSTANCE = new FlowMgtServiceDataHolder(); @@ -89,19 +84,4 @@ public void setCompatibilitySettingsManager(CompatibilitySettingsManager compati this.compatibilitySettingsManager = compatibilitySettingsManager; } - - public List getFlowUpdateInterceptors() { - - return Collections.unmodifiableList(flowUpdateInterceptors); - } - - public void addFlowUpdateInterceptor(FlowUpdateInterceptor interceptor) { - - flowUpdateInterceptors.add(interceptor); - } - - public void removeFlowUpdateInterceptor(FlowUpdateInterceptor interceptor) { - - flowUpdateInterceptors.remove(interceptor); - } } From f1efaacc3cd416f0dc77e6001b20cb75d3d47686 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Thu, 23 Apr 2026 14:49:48 +0530 Subject: [PATCH 13/41] Moved action property handling to request builder and response processor. --- .../impl/ActionManagementServiceImpl.java | 7 +- .../engine/graph/TaskExecutionNode.java | 1 + .../executor/InFlowExtensionExecutor.java | 176 ++++-------------- .../InFlowExtensionRequestBuilder.java | 40 +++- .../InFlowExtensionResponseProcessor.java | 65 ++++++- .../executor/InFlowExtensionExecutorTest.java | 144 ++++++-------- .../InFlowExtensionRequestBuilderTest.java | 156 ++++++++-------- .../InFlowExtensionResponseProcessorTest.java | 125 +++++++++++-- .../InputValidationServiceTest.java | 6 +- 9 files changed, 380 insertions(+), 340 deletions(-) diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java index 084eb884b024..711366bd2969 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java @@ -26,6 +26,7 @@ import org.wso2.carbon.identity.action.management.api.exception.ActionMgtException; import org.wso2.carbon.identity.action.management.api.exception.ActionMgtServerException; import org.wso2.carbon.identity.action.management.api.model.Action; +import org.wso2.carbon.identity.action.management.api.model.Action.ActionTypes; import org.wso2.carbon.identity.action.management.api.model.ActionDTO; import org.wso2.carbon.identity.action.management.api.model.Authentication; import org.wso2.carbon.identity.action.management.api.model.EndpointConfig; @@ -78,8 +79,10 @@ public Action addAction(String actionType, Action action, String tenantDomain) t castedActionType, ActionManagementConfig.getInstance().getLatestVersion(castedActionType), action); // Check whether the maximum allowed actions per type is reached. validateMaxActionsPerType(resolvedActionType, tenantDomain); - // Check whether the action name is unique within the action type. - validateActionNameUniqueness(resolvedActionType, action.getName(), null, tenantId); + // Check whether the action name is unique within the action type for in-flow extensions. + if (resolvedActionType.equals(ActionTypes.IN_FLOW_EXTENSION.getActionType())) { + validateActionNameUniqueness(resolvedActionType, action.getName(), null, tenantId); + } String generatedActionId = UUID.randomUUID().toString(); ActionDTO creatingActionDTO = buildActionDTOForCreation(resolvedActionType, generatedActionId, action); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/graph/TaskExecutionNode.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/graph/TaskExecutionNode.java index 88f1a0edd6f1..c57ff63bd380 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/graph/TaskExecutionNode.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/graph/TaskExecutionNode.java @@ -142,6 +142,7 @@ private NodeResponse handleIncompleteStatus(FlowExecutionContext context, Execut .type(VIEW) .requiredData(response.getRequiredData()) .optionalData(response.getOptionalData()) + .additionalInfo(response.getAdditionalInfo()) .error(response.getErrorMessage()) .build(); case STATUS_USER_INPUT_REQUIRED: diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java index d6a58c46ebf9..e2caa0dc45fb 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java @@ -27,21 +27,17 @@ import org.wso2.carbon.identity.action.execution.api.model.Failure; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; -import org.wso2.carbon.identity.action.management.api.exception.ActionMgtException; -import org.wso2.carbon.identity.action.management.api.model.Action; -import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; +import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; 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.graph.Executor; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; import org.wso2.carbon.identity.flow.mgt.model.NodeConfig; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -50,15 +46,12 @@ * It integrates with the {@link ActionExecutorService} to call external services and process * their responses. * - * *

      Execution lifecycle:

      *
        *
      1. Extract executor metadata: {@code actionId}.
      2. - *
      3. Resolve access config from the action (with per-flow-type override support via - * {@link ActionManagementService}). Falls back to system defaults if unavailable.
      4. - *
      5. Build a minimal {@link FlowContext} containing the full {@link FlowExecutionContext}, - * the expose list, the access config, and the encryption config. - * The request builder will use these to construct the filtered request.
      6. + *
      7. Build a minimal {@link FlowContext} containing only the {@link FlowExecutionContext}. + * The request builder resolves access config / encryption from the action and populates + * additional FlowContext keys for the response processor.
      8. *
      9. Invoke the external service via {@link ActionExecutorService}.
      10. *
      11. Map the {@link ActionExecutionStatus} to an {@link ExecutorResponse}. * Context updates are already applied directly to the {@link FlowExecutionContext} @@ -71,11 +64,10 @@ public class InFlowExtensionExecutor implements Executor { private static final String EXECUTOR_NAME = "InFlowExtensionExecutor"; public static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; - public static final String EXPOSE_KEY = "expose"; public static final String PATH_TYPE_ANNOTATIONS_KEY = "pathTypeAnnotations"; - public static final String ACCESS_CONFIG_KEY = "accessConfig"; - public static final String ENCRYPTION_KEY = "encryption"; private static final String ACTION_ID_METADATA_KEY = "actionId"; + private static final String ERROR_TYPE_KEY = "errorType"; + private static final String EXTENSION_ERROR_TYPE = "EXTENSION_ERROR"; @Override public String getName() { @@ -89,10 +81,12 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE ExecutorResponse response = new ExecutorResponse(); try { + + // TODO: STATUS_ERROR should be returned, should be a diagnostic log String actionId = getMetadataValue(context, ACTION_ID_METADATA_KEY); if (actionId == null || actionId.isEmpty()) { LOG.warn("No action ID configured for In-Flow Extension executor. Skipping execution."); - response.setResult(ExecutorResult.COMPLETE.name()); + response.setResult(ExecutorStatus.STATUS_COMPLETE); return response; } @@ -100,51 +94,43 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE LOG.debug("Executing In-Flow Extension action with ID: " + actionId); } - AccessConfig resolvedConfig = resolveAccessConfigFromAction(actionId, context); - Encryption encryption = resolveEncryptionFromAction(actionId, context); - - List expose; - - if (resolvedConfig != null && resolvedConfig.getExpose() != null) { - expose = resolvedConfig.getExposePaths(); - } else { - // No access config on action — expose nothing by default. - expose = Collections.emptyList(); - } - - FlowContext flowContext = FlowContext.create() - .add(FLOW_EXECUTION_CONTEXT_KEY, context) - .add(EXPOSE_KEY, expose); - - // Pass the full AccessConfig so request builder can derive allowed operations - // from modify paths, and response processor can check encryption flags. - if (resolvedConfig != null) { - flowContext.add(ACCESS_CONFIG_KEY, resolvedConfig); - } - - // Pass the Encryption config (certificate) separately. - if (encryption != null) { - flowContext.add(ENCRYPTION_KEY, encryption); - } - ActionExecutorService actionExecutorService = getActionExecutorService(); if (actionExecutorService == null) { throw new FlowEngineException("ActionExecutorService is not available."); } + if (!actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)) { - LOG.debug("In-Flow Extension action execution is disabled. Skipping."); - response.setResult(ExecutorResult.COMPLETE.name()); + LOG.debug("In-Flow Extension action execution is disabled."); + response.setResult(ExecutorStatus.STATUS_ERROR); return response; } + FlowContext flowContext = FlowContext.create() + .add(FLOW_EXECUTION_CONTEXT_KEY, context); + + // TODO: Have and switch-case ActionExecutionStatus executionStatus = actionExecutorService.execute( ActionType.IN_FLOW_EXTENSION, actionId, flowContext, context.getTenantDomain()); - return mapExecutionStatus(executionStatus); + // TODO: Handle the redirection as well, currently we are treating it as an error since the flow execution engine doesn't support it yet + + ExecutorResponse executionResponse = mapExecutionStatus(executionStatus); - } catch (ActionExecutionException e) { + // Tag RETRY responses with errorType so the frontend can identify extension errors. + if (ExecutorStatus.STATUS_RETRY.equals(executionResponse.getResult())) { + Map additionalInfo = executionResponse.getAdditionalInfo(); + if (additionalInfo == null) { + additionalInfo = new HashMap<>(); + } + additionalInfo.put(ERROR_TYPE_KEY, EXTENSION_ERROR_TYPE); + executionResponse.setAdditionalInfo(additionalInfo); + } + + return executionResponse; + + } catch (ActionExecutionException e) { // TODO: replace with a action execution server exception LOG.error("Error executing In-Flow Extension action.", e); - response.setResult(ExecutorResult.RETRY.name()); + response.setResult(ExecutorStatus.STATUS_ERROR); response.setErrorMessage("An error occurred while processing the extension. Please try again."); return response; } @@ -174,17 +160,17 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt ExecutorResponse response = new ExecutorResponse(); if (executionStatus == null) { - response.setResult(ExecutorResult.USER_INPUT_REQUIRED.name()); + response.setResult(ExecutorStatus.STATUS_ERROR); return response; } switch (executionStatus.getStatus()) { case SUCCESS: - response.setResult(ExecutorResult.COMPLETE.name()); + response.setResult(ExecutorStatus.STATUS_COMPLETE); break; case FAILED: - response.setResult(ExecutorResult.RETRY.name()); + response.setResult(ExecutorStatus.STATUS_RETRY); Failure failure = (Failure) executionStatus.getResponse(); if (failure != null) { response.setErrorMessage(buildUserFacingErrorMessage(failure)); @@ -192,7 +178,7 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt break; case ERROR: - response.setResult(ExecutorResult.RETRY.name()); + response.setResult(ExecutorStatus.STATUS_ERROR); Error error = (Error) executionStatus.getResponse(); if (error != null) { response.setErrorMessage(buildUserFacingErrorMessage(error)); @@ -200,12 +186,12 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt break; case INCOMPLETE: - response.setResult(ExecutorResult.USER_INPUT_REQUIRED.name()); + response.setResult(ExecutorStatus.STATUS_ERROR); break; default: LOG.warn("Unknown execution status: " + executionStatus.getStatus()); - response.setResult(ExecutorResult.COMPLETE.name()); + response.setResult(ExecutorStatus.STATUS_ERROR); } return response; @@ -258,73 +244,6 @@ private ActionExecutorService getActionExecutorService() { return FlowExecutionEngineDataHolder.getInstance().getActionExecutorService(); } - private ActionManagementService getActionManagementService() { - - return FlowExecutionEngineDataHolder.getInstance().getActionManagementService(); - } - - /** - * Resolve the effective access config for the given action ID and flow context. - * Uses the action's flow-type-specific overrides if available, otherwise falls back to - * the action's default access config. - * - * @param actionId The action ID. - * @param context The flow execution context (contains flow type and tenant info). - * @return The resolved AccessConfig, or {@code null} if the action cannot be resolved. - */ - private AccessConfig resolveAccessConfigFromAction(String actionId, FlowExecutionContext context) { - - ActionManagementService actionMgtService = getActionManagementService(); - if (actionMgtService == null) { - LOG.warn("ActionManagementService is not available. Using system defaults for access config."); - return null; - } - - try { - Action action = actionMgtService.getActionByActionId( - Action.ActionTypes.IN_FLOW_EXTENSION.getPathParam(), - actionId, context.getTenantDomain()); - - if (action instanceof InFlowExtensionAction) { - InFlowExtensionAction extensionAction = (InFlowExtensionAction) action; - String flowType = context.getFlowType(); - return extensionAction.resolveAccessConfig(flowType); - } - } catch (ActionMgtException e) { - LOG.error("Error retrieving action " + actionId + " for access config resolution. " - + "Using system defaults.", e); - } - return null; - } - - /** - * Resolve the encryption configuration (certificate) from the action. - * - * @param actionId The action ID. - * @param context The flow execution context. - * @return The Encryption config, or {@code null} if none configured. - */ - private Encryption resolveEncryptionFromAction(String actionId, FlowExecutionContext context) { - - ActionManagementService actionMgtService = getActionManagementService(); - if (actionMgtService == null) { - return null; - } - - try { - Action action = actionMgtService.getActionByActionId( - Action.ActionTypes.IN_FLOW_EXTENSION.getPathParam(), - actionId, context.getTenantDomain()); - - if (action instanceof InFlowExtensionAction) { - return ((InFlowExtensionAction) action).getEncryption(); - } - } catch (ActionMgtException e) { - LOG.error("Error retrieving encryption config for action " + actionId, e); - } - return null; - } - /** * Read a single metadata value from the current node's executor configuration. * @@ -348,19 +267,4 @@ private String getMetadataValue(FlowExecutionContext context, String key) { } return metadata.get(key); } - - /** - * Enum representing the possible results of executor execution. - * These values must match the expected status values in the flow execution engine. - * - * @see org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus - */ - public enum ExecutorResult { - COMPLETE, - ERROR, - USER_ERROR, - USER_INPUT_REQUIRED, - EXTERNAL_REDIRECTION, - RETRY - } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java index ab458a2553e1..20e7ca1ba82c 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java @@ -33,10 +33,12 @@ import org.wso2.carbon.identity.action.execution.api.model.UserClaim; import org.wso2.carbon.identity.action.execution.api.model.UserStore; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionRequestBuilder; +import org.wso2.carbon.identity.action.management.api.model.Action; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; @@ -60,6 +62,9 @@ public class InFlowExtensionRequestBuilder implements ActionExecutionRequestBuil private static final Log LOG = LogFactory.getLog(InFlowExtensionRequestBuilder.class); + public static final String MODIFY_PATHS_KEY = "modifyPaths"; + public static final String ACTION_NAME_KEY = "actionName"; + @Override public ActionType getSupportedActionType() { @@ -67,7 +72,6 @@ public ActionType getSupportedActionType() { } @Override - @SuppressWarnings("unchecked") public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContext, ActionExecutionRequestContext actionExecutionContext) throws ActionExecutionRequestBuilderException { @@ -79,18 +83,34 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex "FlowExecutionContext not found in FlowContext."); } - List expose = flowContext.getValue(InFlowExtensionExecutor.EXPOSE_KEY, List.class); - if (expose == null) { + // Resolve action-specific config. The action is already resolved by ActionExecutorServiceImpl. + Action rawAction = actionExecutionContext.getAction(); + AccessConfig accessConfig = null; + Encryption encryption = null; + String actionName = null; + if (rawAction instanceof InFlowExtensionAction) { + InFlowExtensionAction ext = (InFlowExtensionAction) rawAction; + accessConfig = ext.resolveAccessConfig(execCtx.getFlowType()); + encryption = ext.getEncryption(); + actionName = ext.getName(); + } + + List expose; + if (accessConfig != null && accessConfig.getExpose() != null) { + expose = accessConfig.getExposePaths(); + } else { expose = Collections.emptyList(); } - // Read access config for encryption metadata. - AccessConfig accessConfig = flowContext.getValue( - InFlowExtensionExecutor.ACCESS_CONFIG_KEY, AccessConfig.class); + // Store resolved modify paths (with encryption flags) for the response processor. + List modifyPaths = (accessConfig != null && accessConfig.getModify() != null) + ? accessConfig.getModify() : Collections.emptyList(); + flowContext.add(MODIFY_PATHS_KEY, modifyPaths); - // Read encryption configuration (certificate) separately. - Encryption encryption = flowContext.getValue( - InFlowExtensionExecutor.ENCRYPTION_KEY, Encryption.class); + // Store action name for i18n error key prefixing in the response processor. + if (actionName != null) { + flowContext.add(ACTION_NAME_KEY, actionName); + } // Build allowed operations from modify paths. List allowedOperations = buildAllowedOperationsFromModify( diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java index 2e166b6e1d8a..a5dec5cd37f2 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java @@ -45,6 +45,7 @@ import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; 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; @@ -59,6 +60,8 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * This class is responsible for processing the response from In-Flow Extension actions. @@ -84,6 +87,8 @@ public class InFlowExtensionResponseProcessor implements ActionExecutionResponse private static final String PROPERTIES_PATH_PREFIX = "/properties/"; private static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; private static final String USER_INPUTS_PATH_PREFIX = "/input/"; + private static final String I18N_KEY_PREFIX = "inflow.extension."; + private static final Pattern I18N_KEY_PATTERN = Pattern.compile("^\\{\\{(.+)\\}\\}$"); // Cache for valid claim URIs (per tenant) with TTL. private static final long CLAIM_CACHE_TTL_MS = TimeUnit.MINUTES.toMillis(30); @@ -113,10 +118,12 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon pathTypeAnnotations = Collections.emptyMap(); } - // Read access config for encryption metadata. - // Uses AccessConfig.isModifyPathEncrypted() for canonical encryption checking. - AccessConfig accessConfig = flowContext.getValue( - InFlowExtensionExecutor.ACCESS_CONFIG_KEY, AccessConfig.class); + // Reconstruct AccessConfig from the resolved modify paths stored by the request builder. + // This reuses AccessConfig.isModifyPathEncrypted() for canonical prefix-based encryption checking. + @SuppressWarnings("unchecked") + List modifyPaths = flowContext.getValue( + InFlowExtensionRequestBuilder.MODIFY_PATHS_KEY, List.class); + AccessConfig accessConfig = modifyPaths != null ? new AccessConfig(null, modifyPaths) : null; List results = new ArrayList<>(); @@ -395,9 +402,59 @@ public ActionExecutionStatus processFailureResponse(FlowContext flowCon LOG.debug("Processing failure response from In-Flow Extension. Reason: " + failureReason + ", Description: " + failureDescription); } + + // Apply i18n key prefixing so the frontend can locate extension-specific error messages. + String actionName = flowContext.getValue(InFlowExtensionRequestBuilder.ACTION_NAME_KEY, String.class); + failureDescription = applyI18nKeyPrefix(failureDescription, actionName); + failureReason = applyI18nKeyPrefix(failureReason, actionName); + return new FailedStatus(new Failure(failureReason, failureDescription)); } + /** + * Transform a short i18n key (e.g., {@code {{denied}}}) into a fully qualified key + * (e.g., {@code {{inflow.extension.risk.assessment.extension.denied}}}) using the + * sanitized action name as the prefix. Already-qualified keys are left unchanged. + * + * @param message The error message or reason string. May be null. + * @param actionName The action display name. May be null. + * @return The transformed message, or the original if no transformation applies. + */ + private String applyI18nKeyPrefix(String message, String actionName) { + + if (message == null || actionName == null) { + return message; + } + Matcher matcher = I18N_KEY_PATTERN.matcher(message); + if (matcher.matches()) { + String shortKey = matcher.group(1); + if (!shortKey.startsWith(I18N_KEY_PREFIX)) { + String sanitizedName = sanitizeConnectionName(actionName); + return "{{" + I18N_KEY_PREFIX + sanitizedName + "." + shortKey + "}}"; + } + } + return message; + } + + /** + * Sanitize a connection name into a dot-separated lowercase key segment. + * Non-alphanumeric characters are replaced with dots, consecutive dots are collapsed, + * and leading/trailing dots are trimmed. + * + * @param name The connection display name. + * @return The sanitized key segment (e.g., "Risk Assessment Extension" → "risk.assessment.extension"). + */ + public static String sanitizeConnectionName(String name) { + + if (name == null || name.isEmpty()) { + return ""; + } + String sanitized = name.toLowerCase().replaceAll("[^a-z0-9]", "."); + sanitized = sanitized.replaceAll("\\.{2,}", "."); + sanitized = sanitized.replaceAll("^\\.+|\\.+$", ""); + return sanitized; + } + /** * Log operation execution results for diagnostics and debugging. */ diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java index decb9be348b6..e31c6e8d8010 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java @@ -32,9 +32,10 @@ import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; +import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor.ExecutorResult; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionResponseProcessor; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; @@ -124,7 +125,7 @@ public void testExecuteNoActionId() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_COMPLETE); verify(actionExecutorService, never()) .execute(any(ActionType.class), anyString(), any(FlowContext.class), anyString()); } @@ -138,7 +139,7 @@ public void testExecuteEmptyActionId() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_COMPLETE); } // ========================= execute — execution disabled ========================= @@ -154,7 +155,7 @@ public void testExecuteDisabledExecution() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); verify(actionExecutorService, never()) .execute(any(ActionType.class), anyString(), any(FlowContext.class), anyString()); } @@ -180,7 +181,7 @@ public void testExecuteSuccess() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_COMPLETE); } // ========================= execute — FAILED ========================= @@ -206,8 +207,11 @@ public void testExecuteFailed() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorResult.RETRY.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_RETRY); assertEquals(response.getErrorMessage(), "Risk score exceeds threshold"); + // Verify errorType metadata is set for RETRY. + assertNotNull(response.getAdditionalInfo()); + assertEquals(response.getAdditionalInfo().get("errorType"), "EXTENSION_ERROR"); } @Test @@ -231,7 +235,7 @@ public void testExecuteFailedNoDescription() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorResult.RETRY.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_RETRY); // Falls back to reason when description is null. assertEquals(response.getErrorMessage(), "risk_detected"); } @@ -257,7 +261,7 @@ public void testExecuteFailedBothNull() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorResult.RETRY.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_RETRY); assertEquals(response.getErrorMessage(), "The operation could not be completed due to an external service failure."); } @@ -285,7 +289,7 @@ public void testExecuteError() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorResult.RETRY.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); assertEquals(response.getErrorMessage(), "DB connection failed"); } @@ -310,7 +314,7 @@ public void testExecuteErrorNoDescription() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorResult.RETRY.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); assertEquals(response.getErrorMessage(), "internal_error"); } @@ -335,7 +339,7 @@ public void testExecuteErrorBothNull() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorResult.RETRY.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); assertEquals(response.getErrorMessage(), "An unexpected error occurred in the external service."); } @@ -361,7 +365,7 @@ public void testExecuteIncomplete() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorResult.USER_INPUT_REQUIRED.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); } // ========================= execute — null status ========================= @@ -381,7 +385,7 @@ public void testExecuteNullStatus() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorResult.USER_INPUT_REQUIRED.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); } // ========================= execute — exception ========================= @@ -401,7 +405,7 @@ public void testExecuteActionException() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorResult.RETRY.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); assertEquals(response.getErrorMessage(), "An error occurred while processing the extension. Please try again."); } @@ -429,79 +433,6 @@ public void testExecuteServiceUnavailable() throws Exception { executor.execute(context); } - // ========================= execute — expose JSON parsing ========================= - - @Test - @SuppressWarnings("unchecked") - public void testExecuteWithValidExposeJson() throws Exception { - - Map metadata = new HashMap<>(); - metadata.put("actionId", "test-action-001"); - metadata.put("expose", "[\"/properties\",\"/user/claims\"]"); - FlowExecutionContext context = createContextWithMetadata(metadata); - - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); - - ActionExecutionStatus successStatus = mock(ActionExecutionStatus.class); - when(successStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.SUCCESS); - when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), - any(FlowContext.class), eq("carbon.super"))) - .thenReturn(successStatus); - - ExecutorResponse response = executor.execute(context); - - assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); - } - - @Test - @SuppressWarnings("unchecked") - public void testExecuteWithMalformedExposeJson() throws Exception { - - Map metadata = new HashMap<>(); - metadata.put("actionId", "test-action-001"); - metadata.put("expose", "not_valid_json"); - FlowExecutionContext context = createContextWithMetadata(metadata); - - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); - - ActionExecutionStatus successStatus = mock(ActionExecutionStatus.class); - when(successStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.SUCCESS); - when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), - any(FlowContext.class), eq("carbon.super"))) - .thenReturn(successStatus); - - // Should fall back to default expose but not crash. - ExecutorResponse response = executor.execute(context); - - assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); - } - - @Test - @SuppressWarnings("unchecked") - public void testExecuteWithAllowedOperationsJson() throws Exception { - - Map metadata = new HashMap<>(); - metadata.put("actionId", "test-action-001"); - metadata.put("allowedOperations", - "[{\"op\":\"add\",\"paths\":[\"/properties/riskScore\"]}]"); - FlowExecutionContext context = createContextWithMetadata(metadata); - - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); - - ActionExecutionStatus successStatus = mock(ActionExecutionStatus.class); - when(successStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.SUCCESS); - when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), - any(FlowContext.class), eq("carbon.super"))) - .thenReturn(successStatus); - - ExecutorResponse response = executor.execute(context); - - assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); - } - // ========================= execute — no node config ========================= @Test @@ -514,7 +445,7 @@ public void testExecuteNoNodeConfig() throws Exception { ExecutorResponse response = executor.execute(context); // actionId is null → COMPLETE. - assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_COMPLETE); } @Test @@ -528,7 +459,7 @@ public void testExecuteNoExecutorDTO() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorResult.COMPLETE.name()); + assertEquals(response.getResult(), ExecutorStatus.STATUS_COMPLETE); } // ========================= Helper methods ========================= @@ -546,4 +477,39 @@ private FlowExecutionContext createContextWithMetadata(Map metad return context; } + + // ========================= sanitizeConnectionName ========================= + + @Test + public void testSanitizeConnectionNameBasic() { + + assertEquals(InFlowExtensionResponseProcessor.sanitizeConnectionName("Risk Assessment Extension"), + "risk.assessment.extension"); + } + + @Test + public void testSanitizeConnectionNameSpecialChars() { + + assertEquals(InFlowExtensionResponseProcessor.sanitizeConnectionName("My-Extension_v2 (test)"), + "my.extension.v2.test"); + } + + @Test + public void testSanitizeConnectionNameAlreadyClean() { + + assertEquals(InFlowExtensionResponseProcessor.sanitizeConnectionName("simpleext"), + "simpleext"); + } + + @Test + public void testSanitizeConnectionNameEmpty() { + + assertEquals(InFlowExtensionResponseProcessor.sanitizeConnectionName(""), ""); + } + + @Test + public void testSanitizeConnectionNameNull() { + + assertEquals(InFlowExtensionResponseProcessor.sanitizeConnectionName(null), ""); + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java index 8fc4ebd61ca3..672f159e79de 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java @@ -37,6 +37,7 @@ import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; import org.wso2.carbon.identity.certificate.management.model.Certificate; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; @@ -46,9 +47,11 @@ import java.util.List; import java.util.Map; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; @@ -61,8 +64,6 @@ */ public class InFlowExtensionRequestBuilderTest { - private static final List ALL_EXPOSE = Arrays.asList("/user/", "/properties/", "/input/", "/flow/"); - private InFlowExtensionRequestBuilder requestBuilder; private MockedStatic identityTenantUtilMock; @@ -144,13 +145,10 @@ public void testBuildRequestWithValidModifyPaths() Arrays.asList(new ContextPath("/properties/riskScore", false))); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.EXPOSE_KEY, - Arrays.asList("/user/", "/properties/", "/input/", "/flow/")); + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); List ops = request.getAllowedOperations(); assertNotNull(ops); @@ -165,9 +163,9 @@ public void testBuildRequestWithNoAccessConfig() FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE); + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + // No action → no access config → no modify paths → empty allowed ops. ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -184,12 +182,10 @@ public void testBuildRequestWithEmptyModifyPaths() AccessConfig accessConfig = new AccessConfig(null, Arrays.asList()); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE); + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); List ops = request.getAllowedOperations(); assertNotNull(ops); @@ -208,12 +204,10 @@ public void testPathAnnotationStrippingSimpleArray() Arrays.asList(new ContextPath("/properties/riskFactors{[String]}", false))); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE); + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); // AllowedOperation should have clean path (without {[String]}). AllowedOperation op = request.getAllowedOperations().get(0); @@ -237,12 +231,10 @@ public void testPathAnnotationStrippingSchemaAnnotation() Arrays.asList(new ContextPath("/properties/items{name: String, count: Integer}", false))); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE); + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); AllowedOperation op = request.getAllowedOperations().get(0); assertTrue(op.getPaths().contains("/properties/items")); @@ -263,12 +255,9 @@ public void testPathWithoutAnnotationNotStoredInAnnotationsMap() Arrays.asList(new ContextPath("/properties/riskScore", false))); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE); + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + requestBuilder.buildActionExecutionRequest(flowContext, mockReqCtx(accessConfig, null)); // No annotations should be stored when paths have no annotations. Map annotations = flowContext.getValue( @@ -288,12 +277,10 @@ public void testMultipleAnnotatedPaths() new ContextPath("/properties/items{name: String, count: Integer}", false))); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE); + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); AllowedOperation op = request.getAllowedOperations().get(0); assertEquals(op.getPaths().size(), 3); @@ -316,20 +303,20 @@ public void testModifyPathsDoNotAffectExpose() FlowExecutionContext execCtx = createFullFlowExecutionContext(); // Expose only /input/ and /flow/ — /properties/ is NOT exposed. - List expose = Arrays.asList("/input/", "/flow/"); // Modify path targets /properties/riskScore — but this should NOT auto-expose it. - AccessConfig accessConfig = new AccessConfig(null, + AccessConfig accessConfig = new AccessConfig( + Arrays.asList( + new ContextPath("/input/", false), + new ContextPath("/flow/", false)), Arrays.asList(new ContextPath("/properties/riskScore", false))); execCtx.setProperty("riskScore", "50"); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.EXPOSE_KEY, expose); + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); // Modify paths should produce REPLACE allowed operation. List ops = request.getAllowedOperations(); @@ -352,12 +339,10 @@ public void testMultipleModifyPathsProduceSingleReplaceOperation() new ContextPath("/user/claims/http://wso2.org/claims/email", false))); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE); + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); // All modify paths should be grouped into a single REPLACE operation. List ops = request.getAllowedOperations(); @@ -374,14 +359,15 @@ public void testExposeFilteringOnlyExposedAreaIncluded() FlowExecutionContext execCtx = createFullFlowExecutionContext(); // Only expose /flow/ — no user, no properties, no input. - List expose = Arrays.asList("/flow/tenantDomain", "/flow/flowType"); + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/flow/tenantDomain", false), + new ContextPath("/flow/flowType", false)), null); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.EXPOSE_KEY, expose); + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); // User should NOT be in the event since /user/ is not exposed. @@ -398,16 +384,15 @@ public void testExposeFilteringSpecificClaim() FlowExecutionContext execCtx = createFullFlowExecutionContext(); // Only expose a specific claim. - List expose = Arrays.asList( - "/user/claims/http://wso2.org/claims/email", - "/user/userId"); + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/user/claims/http://wso2.org/claims/email", false), + new ContextPath("/user/userId", false)), null); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.EXPOSE_KEY, expose); + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); assertNotNull(event.getUser()); @@ -429,9 +414,11 @@ public void testPropertiesEncryptedWhenExposePathMarkedEncrypted() FlowExecutionContext execCtx = createFullFlowExecutionContext(); execCtx.setProperty("riskScore", "85"); - // Mark /properties/riskScore as expose-encrypted. + // riskScore is expose-encrypted; existingProp is exposed plaintext. AccessConfig accessConfig = new AccessConfig( - Arrays.asList(new ContextPath("/properties/riskScore", true)), + Arrays.asList( + new ContextPath("/properties/riskScore", true), + new ContextPath("/properties/existingProp", false)), null); Encryption encryption = new Encryption( @@ -439,13 +426,10 @@ public void testPropertiesEncryptedWhenExposePathMarkedEncrypted() .certificateContent("test-cert-pem").build()); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE) - .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.ENCRYPTION_KEY, encryption); + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, encryption)); InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); assertNotNull(event.getFlowProperties()); @@ -469,9 +453,11 @@ public void testUserInputsEncryptedWhenExposePathMarkedEncrypted() FlowExecutionContext execCtx = createFullFlowExecutionContext(); - // Mark /input/consent as expose-encrypted. + // consent is expose-encrypted; username is exposed plaintext. AccessConfig accessConfig = new AccessConfig( - Arrays.asList(new ContextPath("/input/consent", true)), + Arrays.asList( + new ContextPath("/input/consent", true), + new ContextPath("/input/username", false)), null); Encryption encryption = new Encryption( @@ -479,13 +465,10 @@ public void testUserInputsEncryptedWhenExposePathMarkedEncrypted() .certificateContent("test-cert-pem").build()); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE) - .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.ENCRYPTION_KEY, encryption); + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, encryption)); InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); assertNotNull(event.getUserInputs()); @@ -521,18 +504,29 @@ public void testEncryptionFailureThrowsException() .certificateContent("test-cert-pem").build()); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx) - .add(InFlowExtensionExecutor.EXPOSE_KEY, ALL_EXPOSE) - .add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig) - .add(InFlowExtensionExecutor.ENCRYPTION_KEY, encryption); + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); - ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); - requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + requestBuilder.buildActionExecutionRequest(flowContext, mockReqCtx(accessConfig, encryption)); } } // ========================= Helper methods ========================= + /** + * Create a mock ActionExecutionRequestContext whose getAction() returns an InFlowExtensionAction + * configured with the given access config and encryption. + */ + private ActionExecutionRequestContext mockReqCtx(AccessConfig accessConfig, Encryption encryption) { + + InFlowExtensionAction action = mock(InFlowExtensionAction.class); + when(action.resolveAccessConfig(any())).thenReturn(accessConfig); + when(action.getEncryption()).thenReturn(encryption); + when(action.getName()).thenReturn("Test Action"); + ActionExecutionRequestContext ctx = mock(ActionExecutionRequestContext.class); + when(ctx.getAction()).thenReturn(action); + return ctx; + } + private FlowExecutionContext createMinimalFlowExecutionContext() { FlowExecutionContext context = new FlowExecutionContext(); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java index b5be1bea6229..2238f48f4d30 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java @@ -37,9 +37,9 @@ import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionRequestBuilder; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionResponseProcessor; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.JWEEncryptionUtil; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; @@ -589,6 +589,105 @@ public void testProcessFailureResponse() throws ActionExecutionResponseProcessor assertEquals(status.getResponse().getFailureDescription(), "Risk score exceeds threshold"); } + // ========================= processFailureResponse — i18n key prefixing ========================= + + @Test + public void testProcessFailureResponseWithI18nKey() + throws ActionExecutionResponseProcessorException { + + ActionInvocationFailureResponse failureResponse = mock(ActionInvocationFailureResponse.class); + when(failureResponse.getFailureReason()).thenReturn("high_risk"); + when(failureResponse.getFailureDescription()).thenReturn("{{denied}}"); + + @SuppressWarnings("unchecked") + ActionExecutionResponseContext responseContext = + mock(ActionExecutionResponseContext.class); + when(responseContext.getActionInvocationResponse()).thenReturn(failureResponse); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionRequestBuilder.ACTION_NAME_KEY, "Risk Assessment Extension"); + + ActionExecutionStatus status = responseProcessor.processFailureResponse( + flowContext, responseContext); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.FAILED); + assertEquals(status.getResponse().getFailureDescription(), + "{{inflow.extension.risk.assessment.extension.denied}}"); + // Plain reason string — not an i18n key, passes through unchanged. + assertEquals(status.getResponse().getFailureReason(), "high_risk"); + } + + @Test + public void testProcessFailureResponseWithPlainText() + throws ActionExecutionResponseProcessorException { + + ActionInvocationFailureResponse failureResponse = mock(ActionInvocationFailureResponse.class); + when(failureResponse.getFailureReason()).thenReturn("high_risk_detected"); + when(failureResponse.getFailureDescription()).thenReturn("Risk score exceeds threshold"); + + @SuppressWarnings("unchecked") + ActionExecutionResponseContext responseContext = + mock(ActionExecutionResponseContext.class); + when(responseContext.getActionInvocationResponse()).thenReturn(failureResponse); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionRequestBuilder.ACTION_NAME_KEY, "Risk Assessment Extension"); + + ActionExecutionStatus status = responseProcessor.processFailureResponse( + flowContext, responseContext); + + // Plain text passes through unchanged — no {{}} wrapper present. + assertEquals(status.getResponse().getFailureDescription(), "Risk score exceeds threshold"); + assertEquals(status.getResponse().getFailureReason(), "high_risk_detected"); + } + + @Test + public void testProcessFailureResponseWithFullyQualifiedKey() + throws ActionExecutionResponseProcessorException { + + ActionInvocationFailureResponse failureResponse = mock(ActionInvocationFailureResponse.class); + when(failureResponse.getFailureReason()).thenReturn("high_risk"); + when(failureResponse.getFailureDescription()) + .thenReturn("{{inflow.extension.risk.assessment.extension.denied}}"); + + @SuppressWarnings("unchecked") + ActionExecutionResponseContext responseContext = + mock(ActionExecutionResponseContext.class); + when(responseContext.getActionInvocationResponse()).thenReturn(failureResponse); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionRequestBuilder.ACTION_NAME_KEY, "Risk Assessment Extension"); + + ActionExecutionStatus status = responseProcessor.processFailureResponse( + flowContext, responseContext); + + // Already-qualified key must not be double-prefixed. + assertEquals(status.getResponse().getFailureDescription(), + "{{inflow.extension.risk.assessment.extension.denied}}"); + } + + @Test + public void testProcessFailureResponseWithoutActionName() + throws ActionExecutionResponseProcessorException { + + ActionInvocationFailureResponse failureResponse = mock(ActionInvocationFailureResponse.class); + when(failureResponse.getFailureReason()).thenReturn("high_risk"); + when(failureResponse.getFailureDescription()).thenReturn("{{denied}}"); + + @SuppressWarnings("unchecked") + ActionExecutionResponseContext responseContext = + mock(ActionExecutionResponseContext.class); + when(responseContext.getActionInvocationResponse()).thenReturn(failureResponse); + + // No ACTION_NAME_KEY in FlowContext — no prefix should be applied. + FlowContext flowContext = FlowContext.create(); + + ActionExecutionStatus status = responseProcessor.processFailureResponse( + flowContext, responseContext); + + assertEquals(status.getResponse().getFailureDescription(), "{{denied}}"); + } + // ========================= processErrorResponse ========================= @Test @@ -673,9 +772,9 @@ private ActionExecutionStatus executeSuccessResponse( } @SuppressWarnings("unchecked") - private ActionExecutionStatus executeSuccessResponseWithAccessConfig( + private ActionExecutionStatus executeSuccessResponseWithModifyPaths( FlowExecutionContext execCtx, List operations, - Map pathTypeAnnotations, AccessConfig accessConfig) + Map pathTypeAnnotations, List modifyPaths) throws ActionExecutionResponseProcessorException { FlowContext flowContext = FlowContext.create() @@ -684,8 +783,8 @@ private ActionExecutionStatus executeSuccessResponseWithAccessConfig( if (pathTypeAnnotations != null && !pathTypeAnnotations.isEmpty()) { flowContext.add(InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); } - if (accessConfig != null) { - flowContext.add(InFlowExtensionExecutor.ACCESS_CONFIG_KEY, accessConfig); + if (modifyPaths != null) { + flowContext.add(InFlowExtensionRequestBuilder.MODIFY_PATHS_KEY, modifyPaths); } ActionInvocationSuccessResponse successResponse = mock(ActionInvocationSuccessResponse.class); @@ -710,16 +809,14 @@ public void testDecryptionFailureThrowsException() .thenThrow(new RuntimeException("Decryption failed")); FlowExecutionContext execCtx = createFlowExecutionContext(); - // Create AccessConfig with an encrypted modify path. - AccessConfig accessConfig = new AccessConfig(null, - Arrays.asList(new ContextPath("/properties/secret", true))); + List modifyPaths = Arrays.asList(new ContextPath("/properties/secret", true)); // Value looks like JWE (5 dot-separated parts). PerformableOperation op = createOperation( Operation.REPLACE, "/properties/secret", "a.b.c.d.e"); - executeSuccessResponseWithAccessConfig(execCtx, Collections.singletonList(op), - Collections.emptyMap(), accessConfig); + executeSuccessResponseWithModifyPaths(execCtx, Collections.singletonList(op), + Collections.emptyMap(), modifyPaths); } } @@ -728,15 +825,13 @@ public void testNonStringValueForEncryptedPathHandledGracefully() throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = createFlowExecutionContext(); - // Create AccessConfig with an encrypted modify path. - AccessConfig accessConfig = new AccessConfig(null, - Arrays.asList(new ContextPath("/properties/data", true))); + List modifyPaths = Arrays.asList(new ContextPath("/properties/data", true)); // Non-string value (Integer) for an encrypted path — should pass through without decryption. PerformableOperation op = createOperation(Operation.REPLACE, "/properties/data", 42); - ActionExecutionStatus status = executeSuccessResponseWithAccessConfig( - execCtx, Collections.singletonList(op), Collections.emptyMap(), accessConfig); + ActionExecutionStatus status = executeSuccessResponseWithModifyPaths( + execCtx, Collections.singletonList(op), Collections.emptyMap(), modifyPaths); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); // Value should be coerced to String (default behavior for properties). diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/validation/InputValidationServiceTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/validation/InputValidationServiceTest.java index 998114557833..65420b2e5d81 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/validation/InputValidationServiceTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/validation/InputValidationServiceTest.java @@ -786,7 +786,7 @@ public void testValidateUserInputsWithDuplicateClaimValue() ClaimConstants.ClaimUniquenessScope.ACROSS_USERSTORES)).thenReturn(true); claimValidationUtilMock.when(() -> ClaimValidationUtil.isClaimDuplicated(anyString(), anyString())) .thenReturn(true); - // Duplicate claim validation failure should result in RETRY status. + // Duplicate claim validation failure should result in STATUS_RETRY status. ExecutorResponse response = inputValidationService.resolveInputValidationResponse(FlowExecutionContext); Assert.assertEquals(response.getResult(), STATUS_RETRY); } @@ -837,7 +837,7 @@ public void testValidateUserInputsWithServerError() when(mockClaimService.getLocalClaim(anyString(), anyString())) .thenThrow(new ClaimMetadataException("Test server error")); - // Server errors during input validation result in RETRY status from resolveInputValidationResponse. + // Server errors during input validation result in STATUS_RETRY status from resolveInputValidationResponse. ExecutorResponse response = inputValidationService.resolveInputValidationResponse(FlowExecutionContext); Assert.assertEquals(response.getResult(), STATUS_RETRY); } @@ -1425,7 +1425,7 @@ public void testValidatePasswordFormatServerErrorThrowsServerException() when(mockInputValidationService.getInputValidationConfiguration(anyString())) .thenThrow(new InputValidationMgtException("65000", "Server error", "Server error")); - // Server errors during password validation result in RETRY status from resolveInputValidationResponse. + // Server errors during password validation result in STATUS_RETRY status from resolveInputValidationResponse. ExecutorResponse response = inputValidationService.resolveInputValidationResponse(FlowExecutionContext); Assert.assertEquals(response.getResult(), STATUS_RETRY); Assert.assertNotNull(response.getErrorCode()); From 1f9e78a68af2edb154978d2b9441c648007f7762 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Fri, 24 Apr 2026 14:53:36 +0530 Subject: [PATCH 14/41] Removed the context modification logic from response processor. to be handled by the task execution node --- .../util/ActionExecutionDiagnosticLogger.java | 13 + .../engine/core/FlowExecutionEngine.java | 33 +-- .../executor/HierarchicalPrefixMatcher.java | 247 ++++------------- .../executor/InFlowExtensionEvent.java | 57 +++- .../executor/InFlowExtensionExecutor.java | 98 +++++-- .../InFlowExtensionRequestBuilder.java | 102 ++++--- .../InFlowExtensionResponseProcessor.java | 221 +++++---------- .../HierarchicalPrefixMatcherTest.java | 258 +++++------------- .../executor/InFlowExtensionExecutorTest.java | 16 +- .../InFlowExtensionRequestBuilderTest.java | 87 +++++- .../InFlowExtensionResponseProcessorTest.java | 82 ++++-- .../model/InFlowExtensionEventTest.java | 50 ++-- 12 files changed, 564 insertions(+), 700 deletions(-) diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutionDiagnosticLogger.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutionDiagnosticLogger.java index 4e8e142609f3..5ba214da6887 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutionDiagnosticLogger.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutionDiagnosticLogger.java @@ -327,6 +327,19 @@ private DiagnosticLog.DiagnosticLogBuilder initializeDiagnosticLogBuilder(String return diagLogBuilder; } + public void logActionExecutionError(Action action, String errorMessage) { + + if (!LoggerUtils.isDiagnosticLogsEnabled()) { + return; + } + + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = initializeDiagnosticLogBuilder( + ActionExecutionLogConstants.ActionIDs.EXECUTE_ACTION, + "Failed to execute " + action.getType().getDisplayName() + " action. " + errorMessage, + DiagnosticLog.ResultStatus.FAILED); + triggerLogEvent(addActionConfigParams(diagnosticLogBuilder, action)); + } + private void triggerLogEvent(DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder) { LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/core/FlowExecutionEngine.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/core/FlowExecutionEngine.java index 93fe2befeca8..9b36cd0b707b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/core/FlowExecutionEngine.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/core/FlowExecutionEngine.java @@ -236,30 +236,18 @@ private FlowExecutionStep resolveStepForPrompt(GraphConfig graph, NodeConfig cur StepDTO stepDTO = graph.getNodePageMappings().get(currentNode.getId()); - // If the current node has no page mapping (e.g., a background EXECUTION node), - // fall back to the previous node's page mapping so the previous form re-renders - // with any error message displayed inline. - if (stepDTO == null && currentNode.getPreviousNodeId() != null) { - stepDTO = graph.getNodePageMappings().get(currentNode.getPreviousNodeId()); - if (LOG.isDebugEnabled()) { - LOG.debug("Node " + currentNode.getId() + " has no page mapping. " - + "Falling back to previous node: " + currentNode.getPreviousNodeId()); - } - } - - DataDTO dataDTO = stepDTO != null ? stepDTO.getData() : null; + DataDTO.Builder dataDTOBuilder = new DataDTO.Builder() + .requiredParams(nodeResponse.getRequiredData()) + .optionalParams(nodeResponse.getOptionalData()) + .additionalData(nodeResponse.getAdditionalInfo()); - DataDTO finalDataDTO = null; - if (dataDTO != null) { - finalDataDTO = new DataDTO.Builder() - .components(dataDTO.getComponents()) - .requiredParams(nodeResponse.getRequiredData()) - .optionalParams(nodeResponse.getOptionalData()) - .additionalData(nodeResponse.getAdditionalInfo()) - .build(); - handleError(finalDataDTO, nodeResponse); + if (stepDTO != null && stepDTO.getData() != null) { + dataDTOBuilder.components(stepDTO.getData().getComponents()); } + DataDTO finalDataDTO = dataDTOBuilder.build(); + handleError(finalDataDTO, nodeResponse); + // When the END node is reached, mark the flow status as COMPLETE, set the step type to REDIRECTION, // and assign the redirect URL. Note: all END nodes are expected to be of type PROMPT_ONLY. if (END_NODE_ID.equals(currentNode.getId())) { @@ -268,9 +256,6 @@ private FlowExecutionStep resolveStepForPrompt(GraphConfig graph, NodeConfig cur "end node. Changing the flow status to COMPLETE, step type to REDIRECTION and setting " + "the redirect URL."); } - if (finalDataDTO == null ) { - finalDataDTO = new DataDTO(); - } finalDataDTO.setRedirectURL(FlowExecutionEngineUtils.resolveCompletionRedirectionUrl(context)); return new FlowExecutionStep.Builder() .flowId(context.getContextIdentifier()) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java index a34d77fa1691..5496312061eb 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java @@ -21,29 +21,33 @@ import java.util.List; /** - * Utility class for hierarchical prefix-based path matching and context area identification. - * - * This class provides utilities for working with the unified prefix hierarchy used in - * In-Flow Extensions for context access control. - * - * Prefix Hierarchy Structure: + * Utility class for hierarchical prefix-based path matching for In-Flow Extension access control. + * + *

        Expose and modify path lists always contain exact leaf paths (no trailing {@code /}). + * Two distinct matching operations are needed, served by two explicit methods:

        + *
          + *
        • {@link #anyExposedUnder(String, List)} — area-gate check: is any leaf path + * in the list under a given area prefix (e.g. {@code /user/claims/})?
        • + *
        • {@link #isExposedPath(String, List)} — exact check: is a specific leaf path + * (e.g. {@code /user/claims/http://wso2.org/claims/email}) present in the list?
        • + *
        + * + *

        Prefix hierarchy:

        *
          * /user/                           - User context
        - *   /user/claims/{claimURI}        - User claims (keys are system-configured claim URIs)
        + *   /user/claims/{claimURI}        - User claims
          *   /user/userId                   - User's unique identifier
          *   /user/userStoreDomain          - User store domain
        - *   /user/credentials/             - User credentials (system-configured types)
        - *   /user/federatedAssociations/   - Federated IDP associations
        - * 
        + *   /user/credentials/{key}        - User credentials (no key validation required)
        + *
          * /properties/{key}                - Flow properties (fully extensible)
        - * 
        - * /input/{key}                     - User input data (runtime extensible)
        - * 
        + *
          * /flow/                           - Flow metadata (READ-ONLY)
          *   /flow/tenantDomain             - Tenant domain
          *   /flow/applicationId            - Application ID
          *   /flow/flowType                 - Flow type (REGISTRATION, etc.)
        - *   /flow/contextIdentifier        - Flow context identifier
        + *   /flow/callbackUrl              - Callback URL (expose-only)
        + *   /flow/portalUrl                - Portal URL (expose-only)
          * 
        */ public final class HierarchicalPrefixMatcher { @@ -52,174 +56,24 @@ public final class HierarchicalPrefixMatcher { public static final String USER_PREFIX = "/user/"; public static final String USER_CLAIMS_PREFIX = "/user/claims/"; public static final String USER_CREDENTIALS_PREFIX = "/user/credentials/"; - public static final String USER_FEDERATED_PREFIX = "/user/federatedAssociations/"; public static final String USER_ID_PATH = "/user/userId"; public static final String USER_STORE_DOMAIN_PATH = "/user/userStoreDomain"; public static final String PROPERTIES_PREFIX = "/properties/"; - public static final String INPUT_PREFIX = "/input/"; public static final String FLOW_PREFIX = "/flow/"; public static final String FLOW_TENANT_PATH = "/flow/tenantDomain"; public static final String FLOW_APP_ID_PATH = "/flow/applicationId"; public static final String FLOW_TYPE_PATH = "/flow/flowType"; - - /** - * Context area enum for categorization. - */ - public enum ContextArea { - USER_CLAIMS(USER_CLAIMS_PREFIX, true), // System-configured keys (claim URIs) - USER_CREDENTIALS(USER_CREDENTIALS_PREFIX, true), // System-configured keys - USER_FEDERATED(USER_FEDERATED_PREFIX, true), // System-configured keys (IDP names) - USER_SCALAR(USER_PREFIX, false), // Scalar user fields (userId, username, etc.) - PROPERTIES(PROPERTIES_PREFIX, false), // Fully extensible - INPUT(INPUT_PREFIX, false), // Runtime extensible - FLOW(FLOW_PREFIX, false); // Read-only scalar values - - private final String prefix; - private final boolean hasSystemConfiguredKeys; - - ContextArea(String prefix, boolean hasSystemConfiguredKeys) { - - this.prefix = prefix; - this.hasSystemConfiguredKeys = hasSystemConfiguredKeys; - } - - public String getPrefix() { - - return prefix; - } - - /** - * Whether this context area has system-configured keys that must be validated. - * - USER_CLAIMS: Keys are claim URIs configured in claim dialect - * - USER_CREDENTIALS: Keys are credential types (e.g., "password") - * - USER_FEDERATED: Keys are IDP names - */ - public boolean hasSystemConfiguredKeys() { - - return hasSystemConfiguredKeys; - } - } + public static final String FLOW_CALLBACK_URL_PATH = "/flow/callbackUrl"; + public static final String FLOW_PORTAL_URL_PATH = "/flow/portalUrl"; private HierarchicalPrefixMatcher() { } /** - * Identify the context area for a given path. - * - * @param path The full path (e.g., "/user/claims/http://wso2.org/claims/email") - * @return The ContextArea or null if path doesn't match any known area - */ - public static ContextArea identifyContextArea(String path) { - - if (path == null || path.isEmpty()) { - return null; - } - - // Check specific prefixes first (more specific before less specific) - if (path.startsWith(USER_CLAIMS_PREFIX)) { - return ContextArea.USER_CLAIMS; - } - if (path.startsWith(USER_CREDENTIALS_PREFIX)) { - return ContextArea.USER_CREDENTIALS; - } - if (path.startsWith(USER_FEDERATED_PREFIX)) { - return ContextArea.USER_FEDERATED; - } - if (path.startsWith(USER_PREFIX)) { - return ContextArea.USER_SCALAR; - } - if (path.startsWith(PROPERTIES_PREFIX)) { - return ContextArea.PROPERTIES; - } - if (path.startsWith(INPUT_PREFIX)) { - return ContextArea.INPUT; - } - if (path.startsWith(FLOW_PREFIX)) { - return ContextArea.FLOW; - } - - return null; - } - - /** - * Extract the key from a path within a context area. - * For map-based areas (claims, properties, input), this extracts the map key. - * - * @param path The full path - * @param prefix The prefix to remove - * @return The extracted key or null if invalid - */ - public static String extractKey(String path, String prefix) { - - if (path == null || prefix == null || !path.startsWith(prefix)) { - return null; - } - - String key = path.substring(prefix.length()); - - // Handle trailing slash - if (key.endsWith("/")) { - key = key.substring(0, key.length() - 1); - } - - // Handle nested paths (e.g., /properties/nested/field -> return "nested") - int slashIndex = key.indexOf('/'); - if (slashIndex > 0) { - // For simple use, return only the first level key - // The full remaining path is available via getSubPath() - key = key.substring(0, slashIndex); - } - - return key.isEmpty() ? null : key; - } - - /** - * Extract the full remaining path after the prefix. - * Unlike extractKey, this returns the complete sub-path including nested paths. - * - * @param path The full path - * @param prefix The prefix to remove - * @return The full remaining path or null if invalid - */ - public static String getSubPath(String path, String prefix) { - - if (path == null || prefix == null || !path.startsWith(prefix)) { - return null; - } - - String subPath = path.substring(prefix.length()); - - // Handle trailing slash - if (subPath.endsWith("/")) { - subPath = subPath.substring(0, subPath.length() - 1); - } - - return subPath.isEmpty() ? null : subPath; - } - - /** - * Build a full path from a prefix and key. - * - * @param prefix The context area prefix - * @param key The key within that area - * @return The full path - */ - public static String buildPath(String prefix, String key) { - - if (prefix == null || key == null) { - return null; - } - - // Ensure prefix ends with / - String normalizedPrefix = prefix.endsWith("/") ? prefix : prefix + "/"; - return normalizedPrefix + key; - } - - /** - * Check if a path is read-only (in /flow/ or /graph/ areas). + * Check if a path is read-only (in /flow/ area). * * @param path The path to check * @return true if the path is in a read-only area @@ -233,47 +87,48 @@ public static boolean isReadOnly(String path) { } /** - * Check if a path requires system-configured key validation. + * Check if any leaf path in the list falls under the given area prefix. * - * @param path The path to check - * @return true if the path's keys must be validated against system configuration + *

        Used as an area-gate check before iterating over a data block — e.g., to decide + * whether to include any claims, credentials, or properties in the outgoing request. + * The {@code areaPrefix} always ends with {@code /} (e.g. {@code /user/claims/}). + * The {@code leafPaths} list contains only exact leaf paths with no trailing {@code /}.

        + * + * @param areaPrefix The area prefix to check (must end with {@code /}). + * @param leafPaths The list of exposed leaf paths. + * @return {@code true} if at least one leaf path starts with the area prefix. */ - public static boolean requiresSystemKeyValidation(String path) { + public static boolean anyExposedUnder(String areaPrefix, List leafPaths) { - ContextArea area = identifyContextArea(path); - return area != null && area.hasSystemConfiguredKeys(); + if (areaPrefix == null || leafPaths == null || leafPaths.isEmpty()) { + return false; + } + for (String path : leafPaths) { + if (path != null && path.startsWith(areaPrefix)) { + return true; + } + } + return false; } /** - * Check if a path matches any of the given expose paths using bidirectional prefix matching. + * Check if an exact leaf path is present in the expose list. * - *

        Expose paths are always leaf-level (e.g. {@code /user/claims/http://wso2.org/claims/email}). - * Bidirectional matching is required because this method serves two distinct purposes:

        - *
          - *
        • Area-level gate checks: {@code path} is an area prefix (e.g. {@code /user/}). - * Direction 2 ({@code prefix.startsWith(path)}) detects that a leaf expose path - * falls under this area — e.g. {@code /user/claims/email} starts with {@code /user/}.
        • - *
        • Leaf-level filtering: {@code path} is a specific runtime path - * (e.g. {@code /user/claims/http://wso2.org/claims/email}). - * Direction 1 ({@code path.startsWith(prefix)}) performs exact match against - * leaf expose paths.
        • - *
        + *

        Used for leaf-level filtering — e.g., to decide whether a specific claim URI, + * credential key, or scalar field should be included in the outgoing request. + * The {@code leafPath} has no trailing {@code /}. + * The {@code leafPaths} list contains only exact leaf paths.

        * - * @param path The path to check (can be an area prefix or a specific leaf path). - * @param exposePrefixes The list of leaf-level expose paths. - * @return {@code true} if the path matches any expose path. + * @param leafPath The exact path to look up. + * @param leafPaths The list of exposed leaf paths. + * @return {@code true} if the path is present in the list. */ - public static boolean matchesAnyExpose(String path, List exposePrefixes) { + public static boolean isExposedPath(String leafPath, List leafPaths) { - if (path == null || exposePrefixes == null || exposePrefixes.isEmpty()) { + if (leafPath == null || leafPaths == null || leafPaths.isEmpty()) { return false; } - for (String prefix : exposePrefixes) { - if (path.startsWith(prefix) || prefix.startsWith(path)) { - return true; - } - } - return false; + return leafPaths.contains(leafPath); } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionEvent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionEvent.java index c059e1fabdfd..bdce2942e214 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionEvent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionEvent.java @@ -37,7 +37,9 @@ public class InFlowExtensionEvent extends Event { private final String flowType; - private final Map userInputs; + private final String flowId; + private final String callbackUrl; + private final String portalUrl; private final Map flowProperties; private InFlowExtensionEvent(Builder builder) { @@ -49,8 +51,9 @@ private InFlowExtensionEvent(Builder builder) { this.userStore = builder.userStore; this.application = builder.application; this.flowType = builder.flowType; - this.userInputs = builder.userInputs != null ? - Collections.unmodifiableMap(new HashMap<>(builder.userInputs)) : Collections.emptyMap(); + this.flowId = builder.flowId; + this.callbackUrl = builder.callbackUrl; + this.portalUrl = builder.portalUrl; this.flowProperties = builder.flowProperties != null ? Collections.unmodifiableMap(new HashMap<>(builder.flowProperties)) : Collections.emptyMap(); } @@ -66,13 +69,33 @@ public String getFlowType() { } /** - * Get the user inputs collected during the flow. + * Get the flow identifier (context identifier of the executing flow). * - * @return Unmodifiable map of user inputs. + * @return The flow identifier. */ - public Map getUserInputs() { + public String getFlowId() { - return userInputs; + return flowId; + } + + /** + * Get the callback URL for the flow, if exposed. + * + * @return The callback URL, or null if not exposed. + */ + public String getCallbackUrl() { + + return callbackUrl; + } + + /** + * Get the portal URL for the flow, if exposed. + * + * @return The portal URL, or null if not exposed. + */ + public String getPortalUrl() { + + return portalUrl; } /** @@ -97,7 +120,9 @@ public static class Builder { private UserStore userStore; private Application application; private String flowType; - private Map userInputs; + private String flowId; + private String callbackUrl; + private String portalUrl; private Map flowProperties; public Builder request(Request request) { @@ -142,9 +167,21 @@ public Builder flowType(String flowType) { return this; } - public Builder userInputs(Map userInputs) { + public Builder flowId(String flowId) { + + this.flowId = flowId; + return this; + } + + public Builder callbackUrl(String callbackUrl) { + + this.callbackUrl = callbackUrl; + return this; + } + + public Builder portalUrl(String portalUrl) { - this.userInputs = userInputs != null ? new HashMap<>(userInputs) : null; + this.portalUrl = portalUrl; return this; } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java index e2caa0dc45fb..e7c726f1f2d4 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java @@ -20,6 +20,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.action.execution.api.constant.ActionExecutionLogConstants; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionException; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionStatus; import org.wso2.carbon.identity.action.execution.api.model.ActionType; @@ -27,7 +28,9 @@ import org.wso2.carbon.identity.action.execution.api.model.Failure; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; +import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; +import org.wso2.carbon.utils.DiagnosticLog; 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.graph.Executor; @@ -54,8 +57,10 @@ * additional FlowContext keys for the response processor.
      12. *
      13. Invoke the external service via {@link ActionExecutorService}.
      14. *
      15. Map the {@link ActionExecutionStatus} to an {@link ExecutorResponse}. - * Context updates are already applied directly to the {@link FlowExecutionContext} - * by the response processor.
      16. + * On {@code SUCCESS}, pending context updates collected by the response processor + * are extracted from the {@link FlowContext} and forwarded to + * {@code TaskExecutionNode} via {@link ExecutorResponse} fields + * ({@code updatedUserClaims}, {@code userCredentials}, {@code contextProperties}). *
      */ public class InFlowExtensionExecutor implements Executor { @@ -65,6 +70,9 @@ public class InFlowExtensionExecutor implements Executor { public static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; public static final String PATH_TYPE_ANNOTATIONS_KEY = "pathTypeAnnotations"; + public static final String PENDING_CLAIMS_KEY = "pendingClaims"; + public static final String PENDING_CREDENTIALS_KEY = "pendingCredentials"; + public static final String PENDING_PROPERTIES_KEY = "pendingProperties"; private static final String ACTION_ID_METADATA_KEY = "actionId"; private static final String ERROR_TYPE_KEY = "errorType"; private static final String EXTENSION_ERROR_TYPE = "EXTENSION_ERROR"; @@ -76,35 +84,53 @@ public String getName() { } @Override + @SuppressWarnings("unchecked") public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineException { ExecutorResponse response = new ExecutorResponse(); - try { - - // TODO: STATUS_ERROR should be returned, should be a diagnostic log - String actionId = getMetadataValue(context, ACTION_ID_METADATA_KEY); - if (actionId == null || actionId.isEmpty()) { - LOG.warn("No action ID configured for In-Flow Extension executor. Skipping execution."); - response.setResult(ExecutorStatus.STATUS_COMPLETE); - return response; + String actionId = getMetadataValue(context, ACTION_ID_METADATA_KEY); + if (actionId == null || actionId.isEmpty()) { + LOG.warn("No action ID configured for In-Flow Extension executor. Cannot execute."); + if (LoggerUtils.isDiagnosticLogsEnabled()) { + LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( + ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, + ActionExecutionLogConstants.ActionIDs.EXECUTE_ACTION) + .resultMessage("In-Flow Extension action execution failed: action ID is not configured.") + .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED)); } + response.setResult(ExecutorStatus.STATUS_ERROR); + return response; + } - if (LOG.isDebugEnabled()) { - LOG.debug("Executing In-Flow Extension action with ID: " + actionId); - } + if (LOG.isDebugEnabled()) { + LOG.debug("Executing In-Flow Extension action with ID: " + actionId); + } - ActionExecutorService actionExecutorService = getActionExecutorService(); - if (actionExecutorService == null) { - throw new FlowEngineException("ActionExecutorService is not available."); - } + ActionExecutorService actionExecutorService = getActionExecutorService(); + if (actionExecutorService == null) { + throw new FlowEngineException("ActionExecutorService is not available."); + } - if (!actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)) { - LOG.debug("In-Flow Extension action execution is disabled."); - response.setResult(ExecutorStatus.STATUS_ERROR); - return response; + if (!actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)) { + LOG.debug("In-Flow Extension action execution is disabled."); + if (LoggerUtils.isDiagnosticLogsEnabled()) { + LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( + ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, + ActionExecutionLogConstants.ActionIDs.EXECUTE_ACTION) + .resultMessage("In-Flow Extension action execution failed: action type is disabled.") + .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .configParam("actionId", actionId) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED)); } + response.setResult(ExecutorStatus.STATUS_ERROR); + return response; + } + try { FlowContext flowContext = FlowContext.create() .add(FLOW_EXECUTION_CONTEXT_KEY, context); @@ -116,6 +142,26 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE ExecutorResponse executionResponse = mapExecutionStatus(executionStatus); + // On success, extract pending context updates collected by the response processor + // and forward them to TaskExecutionNode via ExecutorResponse fields. + if (ExecutorStatus.STATUS_COMPLETE.equals(executionResponse.getResult())) { + Map pendingClaims = + (Map) flowContext.getValue(PENDING_CLAIMS_KEY, Map.class); + if (pendingClaims != null && !pendingClaims.isEmpty()) { + executionResponse.setUpdatedUserClaims(pendingClaims); + } + Map pendingCredentials = + (Map) flowContext.getValue(PENDING_CREDENTIALS_KEY, Map.class); + if (pendingCredentials != null && !pendingCredentials.isEmpty()) { + executionResponse.setUserCredentials(pendingCredentials); + } + Map pendingProperties = + (Map) flowContext.getValue(PENDING_PROPERTIES_KEY, Map.class); + if (pendingProperties != null && !pendingProperties.isEmpty()) { + executionResponse.setContextProperty(pendingProperties); + } + } + // Tag RETRY responses with errorType so the frontend can identify extension errors. if (ExecutorStatus.STATUS_RETRY.equals(executionResponse.getResult())) { Map additionalInfo = executionResponse.getAdditionalInfo(); @@ -130,6 +176,16 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE } catch (ActionExecutionException e) { // TODO: replace with a action execution server exception LOG.error("Error executing In-Flow Extension action.", e); + if (LoggerUtils.isDiagnosticLogsEnabled()) { + LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( + ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, + ActionExecutionLogConstants.ActionIDs.EXECUTE_ACTION) + .resultMessage("In-Flow Extension action execution failed: " + e.getMessage()) + .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .configParam("actionId", actionId) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED)); + } response.setResult(ExecutorStatus.STATUS_ERROR); response.setErrorMessage("An error occurred while processing the extension. Please try again."); return response; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java index 20e7ca1ba82c..185959fa5f76 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java @@ -20,7 +20,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.action.execution.api.constant.ActionExecutionLogConstants; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionRequestBuilderException; +import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; +import org.wso2.carbon.utils.DiagnosticLog; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequest; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequestContext; import org.wso2.carbon.identity.action.execution.api.model.ActionType; @@ -93,6 +96,10 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex accessConfig = ext.resolveAccessConfig(execCtx.getFlowType()); encryption = ext.getEncryption(); actionName = ext.getName(); + } else { + if (LOG.isDebugEnabled()) { + LOG.debug("No InFlowExtensionAction resolved. Proceeding with empty access configuration."); + } } List expose; @@ -107,6 +114,20 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex ? accessConfig.getModify() : Collections.emptyList(); flowContext.add(MODIFY_PATHS_KEY, modifyPaths); + if (LoggerUtils.isDiagnosticLogsEnabled()) { + LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( + ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, + ActionExecutionLogConstants.ActionIDs.PROCESS_ACTION_REQUEST) + .resultMessage("Building request for In-Flow Extension action.") + .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .configParam("flowType", execCtx.getFlowType()) + .configParam("exposePaths", expose.size()) + .configParam("modifyPaths", modifyPaths.size()) + .configParam("outboundEncryption", encryption != null) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.SUCCESS)); + } + // Store action name for i18n error key prefixing in the response processor. if (actionName != null) { flowContext.add(ACTION_NAME_KEY, actionName); @@ -126,7 +147,7 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex return new ActionExecutionRequest.Builder() .actionType(ActionType.IN_FLOW_EXTENSION) - .flowId(execCtx.getCorrelationId()) + .flowId(execCtx.getContextIdentifier()) .event(event) .allowedOperations(allowedOperations) .build(); @@ -211,7 +232,7 @@ private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List userInputs = context.getUserInputData(); - if (userInputs != null && !userInputs.isEmpty()) { - eventBuilder.userInputs(filterMap(userInputs, HierarchicalPrefixMatcher.INPUT_PREFIX, - expose, accessConfig, certificatePEM)); + // Flow ID — always set from context identifier. + eventBuilder.flowId(context.getContextIdentifier()); + + // Callback URL + if (isLeafExposed(HierarchicalPrefixMatcher.FLOW_CALLBACK_URL_PATH, expose)) { + String callbackUrl = context.getCallbackUrl(); + if (callbackUrl != null) { + eventBuilder.callbackUrl(callbackUrl); } } // Flow properties - if (isExposed(HierarchicalPrefixMatcher.PROPERTIES_PREFIX, expose)) { + if (isAreaExposed(HierarchicalPrefixMatcher.PROPERTIES_PREFIX, expose)) { Map properties = context.getProperties(); if (properties != null && !properties.isEmpty()) { eventBuilder.flowProperties( @@ -283,21 +306,20 @@ private User buildUser(FlowUser flowUser, List expose, AccessConfig accessConfig, String certificatePEM) throws ActionExecutionRequestBuilderException { - String userId = isExposed(HierarchicalPrefixMatcher.USER_ID_PATH, expose) + String userId = isLeafExposed(HierarchicalPrefixMatcher.USER_ID_PATH, expose) ? flowUser.getUserId() : null; User.Builder userBuilder = new User.Builder(userId); - if (isExposed(HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX, expose)) { + if (isAreaExposed(HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX, expose)) { Map claims = flowUser.getClaims(); if (claims != null && !claims.isEmpty()) { List userClaims = new ArrayList<>(); for (Map.Entry claim : claims.entrySet()) { - if (isExposed( - HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX + claim.getKey(), expose)) { + String claimPath = HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX + claim.getKey(); + if (isLeafExposed(claimPath, expose)) { String claimValue = claim.getValue(); // Encrypt claim value if the expose path is marked as encrypted. - String claimPath = HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX + claim.getKey(); if (shouldEncrypt(claimPath, accessConfig, certificatePEM)) { claimValue = encryptValue(claimValue, certificatePEM); } @@ -310,23 +332,25 @@ private User buildUser(FlowUser flowUser, List expose, } } - if (isExposed(HierarchicalPrefixMatcher.USER_CREDENTIALS_PREFIX, expose)) { + if (isAreaExposed(HierarchicalPrefixMatcher.USER_CREDENTIALS_PREFIX, expose)) { Map credentials = flowUser.getUserCredentials(); if (credentials != null && !credentials.isEmpty()) { - // Check if credentials expose path is encrypted. - if (shouldEncrypt(HierarchicalPrefixMatcher.USER_CREDENTIALS_PREFIX, accessConfig, - certificatePEM)) { - // Encrypt each credential value and store as char[] of the JWE compact string. - Map encryptedCredentials = new HashMap<>(); - for (Map.Entry entry : credentials.entrySet()) { + Map filteredCredentials = new HashMap<>(); + for (Map.Entry entry : credentials.entrySet()) { + String credPath = HierarchicalPrefixMatcher.USER_CREDENTIALS_PREFIX + entry.getKey(); + if (isLeafExposed(credPath, expose)) { String plaintext = new String(entry.getValue()); java.util.Arrays.fill(entry.getValue(), '\0'); - String encrypted = encryptValue(plaintext, certificatePEM); - encryptedCredentials.put(entry.getKey(), encrypted.toCharArray()); + if (shouldEncrypt(credPath, accessConfig, certificatePEM)) { + filteredCredentials.put(entry.getKey(), + encryptValue(plaintext, certificatePEM).toCharArray()); + } else { + filteredCredentials.put(entry.getKey(), plaintext.toCharArray()); + } } - userBuilder.userCredentials(encryptedCredentials); - } else { - userBuilder.userCredentials(new HashMap<>(credentials)); + } + if (!filteredCredentials.isEmpty()) { + userBuilder.userCredentials(filteredCredentials); } } } @@ -358,7 +382,7 @@ private Map filterMap(Map map, String areaPrefix, List Map filtered = new HashMap<>(); for (Map.Entry entry : map.entrySet()) { String fullPath = areaPrefix + entry.getKey(); - if (isExposed(fullPath, expose)) { + if (isLeafExposed(fullPath, expose)) { T value = entry.getValue(); if (shouldEncrypt(fullPath, accessConfig, certificatePEM)) { value = (T) encryptValue(String.valueOf(value), certificatePEM); @@ -370,11 +394,21 @@ private Map filterMap(Map map, String areaPrefix, List } /** - * Check if a path is exposed using bidirectional prefix matching. + * Check if any exposed leaf path falls under the given area prefix. + * Used as a gate before iterating a data block (claims, credentials, properties). + */ + private boolean isAreaExposed(String areaPrefix, List expose) { + + return HierarchicalPrefixMatcher.anyExposedUnder(areaPrefix, expose); + } + + /** + * Check if a specific leaf path is present in the expose list. + * Used for exact leaf-level inclusion decisions. */ - private boolean isExposed(String path, List expose) { + private boolean isLeafExposed(String leafPath, List expose) { - return HierarchicalPrefixMatcher.matchesAnyExpose(path, expose); + return HierarchicalPrefixMatcher.isExposedPath(leafPath, expose); } /** diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java index a5dec5cd37f2..96a462f50ae9 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java @@ -41,43 +41,35 @@ import org.wso2.carbon.identity.action.execution.api.model.SuccessStatus; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionResponseProcessor; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; -import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; -import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; -import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; 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.utils.DiagnosticLog; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * This class is responsible for processing the response from In-Flow Extension actions. * - *

      Responsibility: operation processing and applying context updates directly to - * the {@link FlowExecutionContext}. It processes REPLACE operations on flow - * properties, user claims, and user inputs.

      + *

      Responsibility: operation processing and collecting context updates into pending maps + * that are stored in {@link FlowContext} for the executor to forward to {@code TaskExecutionNode} + * via {@link org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse} fields. + * It processes {@code REPLACE} operations on flow properties, user claims, and user credentials.

      * *

      Only {@code REPLACE} operations are supported. The {@code allowedOperations} list * (derived from modify paths and sent to the external service in the request, enforced * upstream by {@code ActionExecutorServiceImpl}) is the sole mechanism for gating which * operations are permitted. This processor performs additional validations:

      *
        - *
      • Read-only areas: No modifications allowed to {@code /flow/} or {@code /graph/}.
      • - *
      • Claim URI validation: For {@code /user/claims/} operations, validates that - * the claim URI exists in the local claim dialect and is not an identity claim.
      • + *
      • Read-only areas: No modifications allowed to {@code /flow/} paths.
      • *
      */ public class InFlowExtensionResponseProcessor implements ActionExecutionResponseProcessor { @@ -86,14 +78,10 @@ public class InFlowExtensionResponseProcessor implements ActionExecutionResponse private static final String PROPERTIES_PATH_PREFIX = "/properties/"; private static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; - private static final String USER_INPUTS_PATH_PREFIX = "/input/"; + private static final String USER_CREDENTIALS_PATH_PREFIX = "/user/credentials/"; private static final String I18N_KEY_PREFIX = "inflow.extension."; private static final Pattern I18N_KEY_PATTERN = Pattern.compile("^\\{\\{(.+)\\}\\}$"); - // Cache for valid claim URIs (per tenant) with TTL. - private static final long CLAIM_CACHE_TTL_MS = TimeUnit.MINUTES.toMillis(30); - private final Map validClaimUrisCache = new ConcurrentHashMap<>(); - @Override public ActionType getSupportedActionType() { @@ -111,7 +99,7 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon String tenantDomain = execCtx != null ? execCtx.getTenantDomain() : null; // Read path type annotations set by the request builder. - // Maps clean paths to annotation content + // Maps clean paths to annotation content. Map pathTypeAnnotations = flowContext.getValue( InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); if (pathTypeAnnotations == null) { @@ -120,11 +108,15 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon // Reconstruct AccessConfig from the resolved modify paths stored by the request builder. // This reuses AccessConfig.isModifyPathEncrypted() for canonical prefix-based encryption checking. - @SuppressWarnings("unchecked") List modifyPaths = flowContext.getValue( InFlowExtensionRequestBuilder.MODIFY_PATHS_KEY, List.class); AccessConfig accessConfig = modifyPaths != null ? new AccessConfig(null, modifyPaths) : null; + // Accumulate pending updates — applied by TaskExecutionNode via ExecutorResponse fields. + Map pendingClaims = new HashMap<>(); + Map pendingCredentials = new HashMap<>(); + Map pendingProperties = new HashMap<>(); + List results = new ArrayList<>(); List operations = @@ -133,11 +125,22 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon if (operations != null && !operations.isEmpty()) { for (PerformableOperation operation : operations) { operation = decryptOperationValueIfNeeded(operation, accessConfig, tenantDomain); - - results.add(processOperation(operation, execCtx, tenantDomain, pathTypeAnnotations)); + results.add(processOperation( + operation, pathTypeAnnotations, pendingClaims, pendingCredentials, pendingProperties)); } } + // Store non-empty pending maps in FlowContext for the executor to forward to TaskExecutionNode. + if (!pendingClaims.isEmpty()) { + flowContext.add(InFlowExtensionExecutor.PENDING_CLAIMS_KEY, pendingClaims); + } + if (!pendingCredentials.isEmpty()) { + flowContext.add(InFlowExtensionExecutor.PENDING_CREDENTIALS_KEY, pendingCredentials); + } + if (!pendingProperties.isEmpty()) { + flowContext.add(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, pendingProperties); + } + logOperationExecutionResults(results); return new SuccessStatus.Builder() @@ -148,18 +151,22 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon /** - * Process a single operation by validating and applying it directly to the - * {@link FlowExecutionContext}. + * Process a single operation by validating and collecting it into the appropriate pending map. + * Updates are not applied directly — they are stored in the pending maps and forwarded to + * {@code TaskExecutionNode} via {@link org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse}. * * @param operation The operation to process. - * @param context The FlowExecutionContext to apply updates to. - * @param tenantDomain The tenant domain for claim validation. * @param pathTypeAnnotations Map of clean paths to their type annotations from allowed operations. + * @param pendingClaims Accumulator map for user claim updates. + * @param pendingCredentials Accumulator map for user credential updates. + * @param pendingProperties Accumulator map for flow property updates. * @return The result of the operation execution. */ private OperationExecutionResult processOperation(PerformableOperation operation, - FlowExecutionContext context, String tenantDomain, - Map pathTypeAnnotations) { + Map pathTypeAnnotations, + Map pendingClaims, + Map pendingCredentials, + Map pendingProperties) { String path = operation.getPath(); @@ -171,32 +178,31 @@ private OperationExecutionResult processOperation(PerformableOperation operation // Route to appropriate handler based on path prefix. if (path.startsWith(PROPERTIES_PATH_PREFIX)) { - return handlePropertyOperation(operation, context, pathTypeAnnotations); + return handlePropertyOperation(operation, pathTypeAnnotations, pendingProperties); } else if (path.startsWith(USER_CLAIMS_PATH_PREFIX)) { - return handleUserClaimOperation(operation, context, tenantDomain); - } else if (path.startsWith(USER_INPUTS_PATH_PREFIX)) { - return handleUserInputOperation(operation, context); + return handleUserClaimOperation(operation, pendingClaims); + } else if (path.startsWith(USER_CREDENTIALS_PATH_PREFIX)) { + return handleUserCredentialOperation(operation, pendingCredentials); } return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Unknown path prefix. Supported: " + PROPERTIES_PATH_PREFIX + - ", " + USER_CLAIMS_PATH_PREFIX + ", " + USER_INPUTS_PATH_PREFIX); + ", " + USER_CLAIMS_PATH_PREFIX + ", " + USER_CREDENTIALS_PATH_PREFIX); } /** - * Handle operation on flow properties — apply directly to {@link FlowExecutionContext}. + * Handle operation on flow properties — collect into pending properties map. * *

      Only flat property paths are supported (e.g., {@code /properties/riskScore}). - * The property is created if it does not already exist. Value coercion is applied - * based on path type annotations from the request builder via + * Value coercion is applied based on path type annotations from the request builder via * {@link PathTypeAnnotationUtil#coerceValue}.

      * * @param operation The performable operation. - * @param context The FlowExecutionContext. * @param pathTypeAnnotations Path type annotations map from request builder. + * @param pendingProperties Accumulator map for property updates. */ private OperationExecutionResult handlePropertyOperation(PerformableOperation operation, - FlowExecutionContext context, Map pathTypeAnnotations) { + Map pathTypeAnnotations, Map pendingProperties) { String propertyName = extractNameFromPath(operation.getPath(), PROPERTIES_PATH_PREFIX); @@ -228,24 +234,23 @@ private OperationExecutionResult handlePropertyOperation(PerformableOperation op "Array value exceeds maximum item limit for path: " + operation.getPath()); } - context.setProperty(propertyName, coercedValue); + pendingProperties.put(propertyName, coercedValue); return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, "Property replace applied."); } /** - * Handle REPLACE operation on user claims — validate and apply directly to {@link FlowUser}. + * Handle REPLACE operation on user claims — collect into pending claims map. + * The value is always stringified via {@code String.valueOf()}. + * Claim URI validation is intentionally omitted — validation is the responsibility + * of flow validators, not the response processor. * - *

      Validates that the claim URI:

      - *
        - *
      • Exists in the local claim dialect (via {@link ClaimMetadataManagementService}).
      • - *
      • Is not an identity claim ({@code http://wso2.org/claims/identity/}).
      • - *
      - *

      The value is always stringified via {@code String.valueOf()}.

      + * @param operation The performable operation. + * @param pendingClaims Accumulator map for user claim updates. */ private OperationExecutionResult handleUserClaimOperation(PerformableOperation operation, - FlowExecutionContext context, String tenantDomain) { + Map pendingClaims) { String claimUri = extractNameFromPath(operation.getPath(), USER_CLAIMS_PATH_PREFIX); @@ -254,46 +259,33 @@ private OperationExecutionResult handleUserClaimOperation(PerformableOperation o "Invalid claim path. Claim URI is required."); } - // Reject identity claims — these are system-managed and not user-modifiable. - if (claimUri.startsWith(PathTypeAnnotationUtil.IDENTITY_CLAIM_URI_PREFIX)) { - return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Identity claims cannot be modified via extensions: " + claimUri); - } - - // Validate claim exists in local claim dialect. - if (!isValidClaimUri(claimUri, tenantDomain)) { - return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Invalid claim URI. Claim must be configured in the local claim dialect: " + claimUri); - } - - FlowUser user = context.getFlowUser(); - if (user == null) { - return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "No FlowUser in FlowExecutionContext. Cannot apply user claim operation."); - } - if (operation.getValue() == null) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Value is required for REPLACE operation."); } - user.addClaim(claimUri, String.valueOf(operation.getValue())); + pendingClaims.put(claimUri, String.valueOf(operation.getValue())); return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, "User claim replace applied."); } /** - * Handle REPLACE operation on user inputs — apply directly to {@link FlowExecutionContext}. - * The value is always stringified via {@code String.valueOf()}. + * Handle REPLACE operation on user credentials — collect into pending credentials map. + * No key validation is applied; any credential key is accepted. The value is converted + * to {@code char[]} immediately to avoid holding the secret as a plain {@code String} + * any longer than necessary. + * + * @param operation The performable operation. + * @param pendingCredentials Accumulator map for user credential updates. */ - private OperationExecutionResult handleUserInputOperation(PerformableOperation operation, - FlowExecutionContext context) { + private OperationExecutionResult handleUserCredentialOperation(PerformableOperation operation, + Map pendingCredentials) { - String inputName = extractNameFromPath(operation.getPath(), USER_INPUTS_PATH_PREFIX); + String credentialKey = extractNameFromPath(operation.getPath(), USER_CREDENTIALS_PATH_PREFIX); - if (inputName == null || inputName.isEmpty()) { + if (credentialKey == null || credentialKey.isEmpty()) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Invalid user input path. Input name is required."); + "Invalid credential path. Credential key is required."); } if (operation.getValue() == null) { @@ -301,60 +293,9 @@ private OperationExecutionResult handleUserInputOperation(PerformableOperation o "Value is required for REPLACE operation."); } - context.addUserInputData(inputName, String.valueOf(operation.getValue())); + pendingCredentials.put(credentialKey, String.valueOf(operation.getValue()).toCharArray()); return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, - "User input replace applied."); - } - - /** - * Validate if a claim URI exists in the local claim dialect. - */ - private boolean isValidClaimUri(String claimUri, String tenantDomain) { - - if (claimUri == null || claimUri.isEmpty()) { - return false; - } - return getValidClaimUris(tenantDomain).contains(claimUri); - } - - /** - * Get valid claim URIs for a tenant, loading from ClaimMetadataManagementService if not cached or expired. - */ - private Set getValidClaimUris(String tenantDomain) { - - String cacheKey = tenantDomain != null ? tenantDomain : "carbon.super"; - - CachedClaimUris cached = validClaimUrisCache.get(cacheKey); - if (cached != null && !cached.isExpired()) { - return cached.getClaimUris(); - } - - Set validUris = new HashSet<>(); - try { - ClaimMetadataManagementService claimService = getClaimMetadataManagementService(); - if (claimService != null) { - List localClaims = claimService.getLocalClaims(tenantDomain); - if (localClaims != null) { - for (LocalClaim claim : localClaims) { - validUris.add(claim.getClaimURI()); - } - } - } - } catch (ClaimMetadataException e) { - LOG.error("Failed to load claim URIs for tenant: " + tenantDomain, e); - } - - validClaimUrisCache.put(cacheKey, new CachedClaimUris(validUris)); - - if (LOG.isDebugEnabled()) { - LOG.debug("Loaded " + validUris.size() + " valid claim URIs for tenant: " + cacheKey); - } - return validUris; - } - - private ClaimMetadataManagementService getClaimMetadataManagementService() { - - return FlowExecutionEngineDataHolder.getInstance().getClaimMetadataManagementService(); + "User credential replace applied."); } /** @@ -449,7 +390,7 @@ public static String sanitizeConnectionName(String name) { if (name == null || name.isEmpty()) { return ""; } - String sanitized = name.toLowerCase().replaceAll("[^a-z0-9]", "."); + String sanitized = name.toLowerCase(Locale.ENGLISH).replaceAll("[^a-z0-9]", "."); sanitized = sanitized.replaceAll("\\.{2,}", "."); sanitized = sanitized.replaceAll("^\\.+|\\.+$", ""); return sanitized; @@ -572,28 +513,4 @@ private PerformableOperation decryptOperationValueIfNeeded(PerformableOperation } } - /** - * Cache wrapper for claim URIs with TTL support. - */ - private static class CachedClaimUris { - - private final Set claimUris; - private final long createdAt; - - CachedClaimUris(Set claimUris) { - - this.claimUris = claimUris; - this.createdAt = System.currentTimeMillis(); - } - - Set getClaimUris() { - - return claimUris; - } - - boolean isExpired() { - - return (System.currentTimeMillis() - createdAt) > CLAIM_CACHE_TTL_MS; - } - } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java index a5e1d26cb0e7..1d40efa00f4d 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java @@ -21,7 +21,6 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.HierarchicalPrefixMatcher; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.HierarchicalPrefixMatcher.ContextArea; import java.util.Arrays; import java.util.Collections; @@ -29,8 +28,6 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; /** @@ -38,143 +35,6 @@ */ public class HierarchicalPrefixMatcherTest { - // ========================= identifyContextArea ========================= - - @DataProvider(name = "contextAreaPaths") - public Object[][] contextAreaPaths() { - - return new Object[][] { - { "/user/claims/http://wso2.org/claims/email", ContextArea.USER_CLAIMS }, - { "/user/claims/", ContextArea.USER_CLAIMS }, - { "/user/credentials/password", ContextArea.USER_CREDENTIALS }, - { "/user/credentials/", ContextArea.USER_CREDENTIALS }, - { "/user/federatedAssociations/google", ContextArea.USER_FEDERATED }, - { "/user/federatedAssociations/", ContextArea.USER_FEDERATED }, - { "/user/userId", ContextArea.USER_SCALAR }, - { "/user/username", ContextArea.USER_SCALAR }, - { "/user/userStoreDomain", ContextArea.USER_SCALAR }, - { "/properties/riskScore", ContextArea.PROPERTIES }, - { "/properties/", ContextArea.PROPERTIES }, - { "/input/someField", ContextArea.INPUT }, - { "/input/", ContextArea.INPUT }, - { "/flow/tenantDomain", ContextArea.FLOW }, - { "/flow/applicationId", ContextArea.FLOW }, - { "/flow/", ContextArea.FLOW }, - }; - } - - @Test(dataProvider = "contextAreaPaths") - public void testIdentifyContextArea(String path, ContextArea expected) { - - assertEquals(HierarchicalPrefixMatcher.identifyContextArea(path), expected); - } - - @DataProvider(name = "unknownPaths") - public Object[][] unknownPaths() { - - return new Object[][] { - { null }, - { "" }, - { "/unknown/path" }, - { "noSlash" }, - { "/other/" }, - }; - } - - @Test(dataProvider = "unknownPaths") - public void testIdentifyContextAreaReturnsNullForUnknownPaths(String path) { - - assertNull(HierarchicalPrefixMatcher.identifyContextArea(path)); - } - - // ========================= extractKey ========================= - - @DataProvider(name = "extractKeyData") - public Object[][] extractKeyData() { - - return new Object[][] { - { "/properties/riskScore", "/properties/", "riskScore" }, - { "/properties/nested/field", "/properties/", "nested" }, - { "/user/claims/http://wso2.org/claims/email", "/user/claims/", "http:" }, - { "/input/fieldName", "/input/", "fieldName" }, - }; - } - - @Test(dataProvider = "extractKeyData") - public void testExtractKey(String path, String prefix, String expected) { - - assertEquals(HierarchicalPrefixMatcher.extractKey(path, prefix), expected); - } - - @DataProvider(name = "extractKeyNullCases") - public Object[][] extractKeyNullCases() { - - return new Object[][] { - { null, "/properties/" }, - { "/properties/", "/properties/" }, - { "/user/claims/email", "/properties/" }, - }; - } - - @Test(dataProvider = "extractKeyNullCases") - public void testExtractKeyReturnsNullForInvalidInput(String path, String prefix) { - - assertNull(HierarchicalPrefixMatcher.extractKey(path, prefix)); - } - - // ========================= getSubPath ========================= - - @DataProvider(name = "subPathData") - public Object[][] subPathData() { - - return new Object[][] { - { "/properties/nested/field", "/properties/", "nested/field" }, - { "/properties/riskScore", "/properties/", "riskScore" }, - { "/user/claims/http://wso2.org/claims/email", "/user/claims/", "http://wso2.org/claims/email" }, - }; - } - - @Test(dataProvider = "subPathData") - public void testGetSubPath(String path, String prefix, String expected) { - - assertEquals(HierarchicalPrefixMatcher.getSubPath(path, prefix), expected); - } - - @Test - public void testGetSubPathReturnsNullForNullPath() { - - assertNull(HierarchicalPrefixMatcher.getSubPath(null, "/properties/")); - } - - @Test - public void testGetSubPathReturnsNullForMismatchedPrefix() { - - assertNull(HierarchicalPrefixMatcher.getSubPath("/user/claims/x", "/properties/")); - } - - // ========================= buildPath ========================= - - @Test - public void testBuildPath() { - - assertEquals(HierarchicalPrefixMatcher.buildPath("/properties/", "riskScore"), - "/properties/riskScore"); - } - - @Test - public void testBuildPathWithTrailingSlash() { - - assertEquals(HierarchicalPrefixMatcher.buildPath("/user/claims/", "http://wso2.org/claims/email"), - "/user/claims/http://wso2.org/claims/email"); - } - - @Test - public void testBuildPathReturnsNullForNullInputs() { - - assertNull(HierarchicalPrefixMatcher.buildPath(null, "key")); - assertNull(HierarchicalPrefixMatcher.buildPath("/properties/", null)); - } - // ========================= isReadOnly ========================= @DataProvider(name = "readOnlyPaths") @@ -186,7 +46,6 @@ public Object[][] readOnlyPaths() { { "/flow/", true }, { "/properties/riskScore", false }, { "/user/claims/email", false }, - { "/input/field", false }, { null, false }, }; } @@ -197,100 +56,111 @@ public void testIsReadOnly(String path, boolean expected) { assertEquals(HierarchicalPrefixMatcher.isReadOnly(path), expected); } - // ========================= requiresSystemKeyValidation ========================= + // ========================= anyExposedUnder ========================= - @DataProvider(name = "systemKeyPaths") - public Object[][] systemKeyPaths() { + @Test + public void testAnyExposedUnderMatchesLeafUnderPrefix() { - return new Object[][] { - { "/user/claims/http://wso2.org/claims/email", true }, - { "/user/credentials/password", true }, - { "/user/federatedAssociations/google", true }, - { "/properties/score", false }, - { "/input/field", false }, - { "/flow/tenantDomain", false }, - { "/user/userId", false }, - }; + List leafPaths = Arrays.asList( + "/user/claims/http://wso2.org/claims/email", + "/properties/riskScore"); + assertTrue(HierarchicalPrefixMatcher.anyExposedUnder("/user/claims/", leafPaths)); + assertTrue(HierarchicalPrefixMatcher.anyExposedUnder("/properties/", leafPaths)); } - @Test(dataProvider = "systemKeyPaths") - public void testRequiresSystemKeyValidation(String path, boolean expected) { + @Test + public void testAnyExposedUnderNoMatch() { - assertEquals(HierarchicalPrefixMatcher.requiresSystemKeyValidation(path), expected); + List leafPaths = Arrays.asList("/flow/tenantDomain", "/flow/applicationId"); + assertFalse(HierarchicalPrefixMatcher.anyExposedUnder("/user/claims/", leafPaths)); + assertFalse(HierarchicalPrefixMatcher.anyExposedUnder("/properties/", leafPaths)); } - // ========================= matchesAnyExpose ========================= + @Test + public void testAnyExposedUnderNullPrefix() { + + assertFalse(HierarchicalPrefixMatcher.anyExposedUnder(null, + Arrays.asList("/user/claims/email"))); + } @Test - public void testMatchesAnyExposePathStartsWithPrefix() { + public void testAnyExposedUnderNullList() { - List expose = Arrays.asList("/user/", "/properties/"); - assertTrue(HierarchicalPrefixMatcher.matchesAnyExpose("/user/claims/email", expose)); - assertTrue(HierarchicalPrefixMatcher.matchesAnyExpose("/properties/riskScore", expose)); + assertFalse(HierarchicalPrefixMatcher.anyExposedUnder("/user/claims/", null)); } @Test - public void testMatchesAnyExposePrefixStartsWithPath() { + public void testAnyExposedUnderEmptyList() { - // Bidirectional: path is parent of a prefix in the list. - List expose = Arrays.asList("/user/claims/http://wso2.org/claims/email"); - assertTrue(HierarchicalPrefixMatcher.matchesAnyExpose("/user/", expose)); - assertTrue(HierarchicalPrefixMatcher.matchesAnyExpose("/user/claims/", expose)); + assertFalse(HierarchicalPrefixMatcher.anyExposedUnder("/user/claims/", + Collections.emptyList())); } @Test - public void testMatchesAnyExposeNoMatch() { + public void testAnyExposedUnderDoesNotMatchShortPath() { - List expose = Arrays.asList("/flow/"); - assertFalse(HierarchicalPrefixMatcher.matchesAnyExpose("/properties/score", expose)); - assertFalse(HierarchicalPrefixMatcher.matchesAnyExpose("/user/claims/email", expose)); + // A leaf path of "/user/userId" should NOT be matched by area prefix "/user/claims/" + List leafPaths = Collections.singletonList("/user/userId"); + assertFalse(HierarchicalPrefixMatcher.anyExposedUnder("/user/claims/", leafPaths)); } @Test - public void testMatchesAnyExposeNullPath() { + public void testAnyExposedUnderMultipleLeafsOneMatches() { - assertFalse(HierarchicalPrefixMatcher.matchesAnyExpose(null, Arrays.asList("/user/"))); + List leafPaths = Arrays.asList( + "/flow/tenantDomain", + "/user/credentials/password"); + assertTrue(HierarchicalPrefixMatcher.anyExposedUnder("/user/credentials/", leafPaths)); } + // ========================= isExposedPath ========================= + @Test - public void testMatchesAnyExposeEmptyList() { + public void testIsExposedPathExactMatch() { - assertFalse(HierarchicalPrefixMatcher.matchesAnyExpose("/user/claims/email", Collections.emptyList())); + List leafPaths = Arrays.asList( + "/user/claims/http://wso2.org/claims/email", + "/flow/tenantDomain", + "/user/userId"); + assertTrue(HierarchicalPrefixMatcher.isExposedPath( + "/user/claims/http://wso2.org/claims/email", leafPaths)); + assertTrue(HierarchicalPrefixMatcher.isExposedPath("/flow/tenantDomain", leafPaths)); + assertTrue(HierarchicalPrefixMatcher.isExposedPath("/user/userId", leafPaths)); } @Test - public void testMatchesAnyExposeNullList() { + public void testIsExposedPathNoMatch() { - assertFalse(HierarchicalPrefixMatcher.matchesAnyExpose("/user/claims/email", null)); + List leafPaths = Arrays.asList("/flow/tenantDomain", "/user/userId"); + assertFalse(HierarchicalPrefixMatcher.isExposedPath( + "/user/claims/http://wso2.org/claims/email", leafPaths)); } @Test - public void testMatchesAnyExposeMultiplePrefixesOneMatches() { + public void testIsExposedPathNullPath() { - List expose = Arrays.asList("/flow/", "/properties/"); - assertTrue(HierarchicalPrefixMatcher.matchesAnyExpose("/properties/score", expose)); + assertFalse(HierarchicalPrefixMatcher.isExposedPath(null, + Arrays.asList("/user/userId"))); } - // ========================= ContextArea enum ========================= + @Test + public void testIsExposedPathNullList() { + + assertFalse(HierarchicalPrefixMatcher.isExposedPath("/user/userId", null)); + } @Test - public void testContextAreaPrefix() { + public void testIsExposedPathEmptyList() { - assertEquals(ContextArea.USER_CLAIMS.getPrefix(), "/user/claims/"); - assertEquals(ContextArea.PROPERTIES.getPrefix(), "/properties/"); - assertEquals(ContextArea.INPUT.getPrefix(), "/input/"); - assertEquals(ContextArea.FLOW.getPrefix(), "/flow/"); + assertFalse(HierarchicalPrefixMatcher.isExposedPath("/user/userId", + Collections.emptyList())); } @Test - public void testContextAreaHasSystemConfiguredKeys() { - - assertTrue(ContextArea.USER_CLAIMS.hasSystemConfiguredKeys()); - assertTrue(ContextArea.USER_CREDENTIALS.hasSystemConfiguredKeys()); - assertTrue(ContextArea.USER_FEDERATED.hasSystemConfiguredKeys()); - assertFalse(ContextArea.USER_SCALAR.hasSystemConfiguredKeys()); - assertFalse(ContextArea.PROPERTIES.hasSystemConfiguredKeys()); - assertFalse(ContextArea.INPUT.hasSystemConfiguredKeys()); - assertFalse(ContextArea.FLOW.hasSystemConfiguredKeys()); + public void testIsExposedPathPrefixNotSufficient() { + + // An area prefix "/user/claims/" must NOT match when the list only has a leaf under it. + List leafPaths = Collections.singletonList("/user/claims/http://wso2.org/claims/email"); + assertFalse(HierarchicalPrefixMatcher.isExposedPath("/user/claims/", leafPaths)); } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java index e31c6e8d8010..5be65a794ec3 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java @@ -32,6 +32,7 @@ import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; +import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; @@ -69,6 +70,7 @@ public class InFlowExtensionExecutorTest { private AutoCloseable mocks; private MockedStatic holderMock; + private MockedStatic loggerUtilsMock; @BeforeMethod public void setUp() { @@ -82,11 +84,15 @@ public void setUp() { holderMock = mockStatic(FlowExecutionEngineDataHolder.class); holderMock.when(FlowExecutionEngineDataHolder::getInstance).thenReturn(holderInstance); + + loggerUtilsMock = mockStatic(LoggerUtils.class); + loggerUtilsMock.when(LoggerUtils::isDiagnosticLogsEnabled).thenReturn(false); } @AfterMethod public void tearDown() throws Exception { + loggerUtilsMock.close(); holderMock.close(); mocks.close(); } @@ -125,7 +131,7 @@ public void testExecuteNoActionId() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorStatus.STATUS_COMPLETE); + assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); verify(actionExecutorService, never()) .execute(any(ActionType.class), anyString(), any(FlowContext.class), anyString()); } @@ -139,7 +145,7 @@ public void testExecuteEmptyActionId() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorStatus.STATUS_COMPLETE); + assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); } // ========================= execute — execution disabled ========================= @@ -444,8 +450,8 @@ public void testExecuteNoNodeConfig() throws Exception { ExecutorResponse response = executor.execute(context); - // actionId is null → COMPLETE. - assertEquals(response.getResult(), ExecutorStatus.STATUS_COMPLETE); + // actionId is null → missing configuration → ERROR. + assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); } @Test @@ -459,7 +465,7 @@ public void testExecuteNoExecutorDTO() throws Exception { ExecutorResponse response = executor.execute(context); - assertEquals(response.getResult(), ExecutorStatus.STATUS_COMPLETE); + assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); } // ========================= Helper methods ========================= diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java index 672f159e79de..3a2fc272ac46 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java @@ -23,6 +23,7 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionRequestBuilderException; +import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequest; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequestContext; import org.wso2.carbon.identity.action.execution.api.model.ActionType; @@ -66,6 +67,7 @@ public class InFlowExtensionRequestBuilderTest { private InFlowExtensionRequestBuilder requestBuilder; private MockedStatic identityTenantUtilMock; + private MockedStatic loggerUtilsMock; @BeforeMethod public void setUp() { @@ -73,11 +75,14 @@ public void setUp() { requestBuilder = new InFlowExtensionRequestBuilder(); identityTenantUtilMock = mockStatic(IdentityTenantUtil.class); identityTenantUtilMock.when(() -> IdentityTenantUtil.getTenantId(anyString())).thenReturn(1); + loggerUtilsMock = mockStatic(LoggerUtils.class); + loggerUtilsMock.when(LoggerUtils::isDiagnosticLogsEnabled).thenReturn(false); } @AfterMethod public void tearDown() { + loggerUtilsMock.close(); identityTenantUtilMock.close(); } @@ -130,8 +135,8 @@ public void testBuildRequestUsesEmptyExposeWhenExposeIsNull() InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); assertNotNull(event); assertNull(event.getFlowType()); - // userInputs defaults to emptyMap() in the builder — verify it is empty. - assertTrue(event.getUserInputs().isEmpty()); + // flowProperties defaults to emptyMap() in the builder — verify it is empty. + assertTrue(event.getFlowProperties().isEmpty()); } // ========================= buildAllowedOperations (from modify) ========================= @@ -444,7 +449,7 @@ public void testPropertiesEncryptedWhenExposePathMarkedEncrypted() } @Test - public void testUserInputsEncryptedWhenExposePathMarkedEncrypted() + public void testClaimEncryptedWhenExposePathMarkedEncrypted() throws ActionExecutionRequestBuilderException { try (MockedStatic jweUtilMock = mockStatic(JWEEncryptionUtil.class)) { @@ -453,11 +458,11 @@ public void testUserInputsEncryptedWhenExposePathMarkedEncrypted() FlowExecutionContext execCtx = createFullFlowExecutionContext(); - // consent is expose-encrypted; username is exposed plaintext. + // email claim is expose-encrypted; country claim is exposed plaintext. AccessConfig accessConfig = new AccessConfig( Arrays.asList( - new ContextPath("/input/consent", true), - new ContextPath("/input/username", false)), + new ContextPath("/user/claims/http://wso2.org/claims/email", true), + new ContextPath("/user/claims/http://wso2.org/claims/country", false)), null); Encryption encryption = new Encryption( @@ -471,14 +476,68 @@ public void testUserInputsEncryptedWhenExposePathMarkedEncrypted() flowContext, mockReqCtx(accessConfig, encryption)); InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); - assertNotNull(event.getUserInputs()); - // consent should be encrypted. - String consentValue = event.getUserInputs().get("consent"); - assertNotNull(consentValue); - assertTrue(consentValue.startsWith("encrypted."), - "Input value should be encrypted when expose path is marked encrypted"); - // username input is NOT marked encrypted — should remain plaintext. - assertEquals(event.getUserInputs().get("username"), "testuser"); + assertNotNull(event.getUser()); + List claims = event.getUser().getClaims(); + assertEquals(claims.size(), 2); + + org.wso2.carbon.identity.action.execution.api.model.UserClaim emailClaim = null; + org.wso2.carbon.identity.action.execution.api.model.UserClaim countryClaim = null; + for (Object c : claims) { + org.wso2.carbon.identity.action.execution.api.model.UserClaim uc = + (org.wso2.carbon.identity.action.execution.api.model.UserClaim) c; + if ("http://wso2.org/claims/email".equals(uc.getUri())) { + emailClaim = uc; + } else if ("http://wso2.org/claims/country".equals(uc.getUri())) { + countryClaim = uc; + } + } + + assertNotNull(emailClaim, "email claim should be present"); + assertTrue(emailClaim.getValue().toString().startsWith("encrypted."), + "email claim should be encrypted when expose path is marked encrypted"); + + assertNotNull(countryClaim, "country claim should be present"); + assertEquals(countryClaim.getValue().toString(), "US", + "country claim should remain plaintext when expose path is not marked encrypted"); + } + } + + @Test + public void testCredentialEncryptedWhenExposePathMarkedEncrypted() + throws ActionExecutionRequestBuilderException { + + try (MockedStatic jweUtilMock = mockStatic(JWEEncryptionUtil.class)) { + jweUtilMock.when(() -> JWEEncryptionUtil.encrypt(anyString(), anyString())) + .thenAnswer(inv -> "encrypted." + inv.getArgument(0) + ".jwe.part.four"); + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + Map creds = new java.util.HashMap<>(); + creds.put("password", "secret123".toCharArray()); + execCtx.getFlowUser().setUserCredentials(creds); + + // password credential is expose-encrypted. + AccessConfig accessConfig = new AccessConfig( + Arrays.asList(new ContextPath("/user/credentials/password", true)), + null); + + Encryption encryption = new Encryption( + new Certificate.Builder().id("cert-1").name("test") + .certificateContent("test-cert-pem").build()); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, encryption)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event.getUser()); + Map eventCreds = event.getUser().getUserCredentials(); + assertNotNull(eventCreds); + assertTrue(eventCreds.containsKey("password")); + String credValue = new String(eventCreds.get("password")); + assertTrue(credValue.startsWith("encrypted."), + "credential should be encrypted when expose path is marked encrypted"); } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java index 2238f48f4d30..0a030acd04ff 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java @@ -72,6 +72,7 @@ public class InFlowExtensionResponseProcessorTest { private MockedStatic loggerUtilsMock; private MockedStatic holderMock; private ClaimMetadataManagementService claimService; + private FlowContext capturedFlowContext; @BeforeMethod public void setUp() throws Exception { @@ -99,6 +100,7 @@ public void tearDown() { holderMock.close(); loggerUtilsMock.close(); + capturedFlowContext = null; } // ========================= getSupportedActionType ========================= @@ -121,7 +123,10 @@ public void testPropertyReplaceFlatExists() throws ActionExecutionResponseProces ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, Collections.emptyMap()); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - assertEquals(execCtx.getProperties().get("riskScore"), "80"); + Map pendingProps = + capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + assertNotNull(pendingProps); + assertEquals(pendingProps.get("riskScore"), "80"); } @Test @@ -134,7 +139,10 @@ public void testPropertyReplaceCreatesIfMissing() throws ActionExecutionResponse ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, Collections.emptyMap()); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - assertEquals(execCtx.getProperties().get("riskScore"), "80"); + Map pendingProps = + capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + assertNotNull(pendingProps); + assertEquals(pendingProps.get("riskScore"), "80"); } @Test @@ -149,7 +157,10 @@ public void testPropertyReplaceCoercesToStringByDefault() execCtx, replaceOp, Collections.emptyMap()); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - assertEquals(execCtx.getProperties().get("riskScore"), "75"); + Map pendingProps = + capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + assertNotNull(pendingProps); + assertEquals(pendingProps.get("riskScore"), "75"); } @Test @@ -165,7 +176,10 @@ public void testPropertyReplaceWithMultivaluedAnnotation() ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, annotations); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - Object stored = execCtx.getProperties().get("riskFactors"); + Map pendingProps = + capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + assertNotNull(pendingProps); + Object stored = pendingProps.get("riskFactors"); assertTrue(stored instanceof List); assertEquals(((List) stored).size(), 2); } @@ -182,7 +196,10 @@ public void testPropertyReplaceMultivaluedSingleValueWrapped() PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/tags", "singleTag"); executeSuccessResponse(execCtx, replaceOp, annotations); - Object stored = execCtx.getProperties().get("tags"); + Map pendingProps = + capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + assertNotNull(pendingProps); + Object stored = pendingProps.get("tags"); assertTrue(stored instanceof List); assertEquals(((List) stored).size(), 1); assertEquals(((List) stored).get(0), "singleTag"); @@ -204,8 +221,11 @@ public void testPropertyReplaceWithComplexAnnotation() ActionExecutionStatus status = executeSuccessResponse(execCtx, replaceOp, annotations); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + Map pendingProps = + capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + assertNotNull(pendingProps); // Complex annotation → value passed through as-is. - Object stored = execCtx.getProperties().get("item"); + Object stored = pendingProps.get("item"); assertTrue(stored instanceof Map); } @@ -221,7 +241,10 @@ public void testPropertyReplaceWithPrimaryTypeAnnotation() PerformableOperation replaceOp = createOperation(Operation.REPLACE, "/properties/score", 95); executeSuccessResponse(execCtx, replaceOp, annotations); - assertEquals(execCtx.getProperties().get("score"), "95"); + Map pendingProps = + capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + assertNotNull(pendingProps); + assertEquals(pendingProps.get("score"), "95"); } @Test @@ -330,7 +353,10 @@ public void testUserClaimReplace() throws ActionExecutionResponseProcessorExcept Operation.REPLACE, "/user/claims/http://wso2.org/claims/email", "new@example.com"); executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); - assertEquals(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/email"), "new@example.com"); + Map pendingClaims = + capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_CLAIMS_KEY, Map.class); + assertNotNull(pendingClaims); + assertEquals(pendingClaims.get("http://wso2.org/claims/email"), "new@example.com"); } @Test @@ -342,7 +368,10 @@ public void testUserClaimReplaceCreatesNewClaim() throws ActionExecutionResponse Operation.REPLACE, "/user/claims/http://wso2.org/claims/country", "US"); executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); - assertEquals(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/country"), "US"); + Map pendingClaims = + capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_CLAIMS_KEY, Map.class); + assertNotNull(pendingClaims); + assertEquals(pendingClaims.get("http://wso2.org/claims/country"), "US"); } @Test @@ -355,7 +384,10 @@ public void testUserClaimReplaceStringifiesValue() throws ActionExecutionRespons Operation.REPLACE, "/user/claims/http://wso2.org/claims/country", 42); executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); - assertEquals(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/country"), "42"); + Map pendingClaims = + capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_CLAIMS_KEY, Map.class); + assertNotNull(pendingClaims); + assertEquals(pendingClaims.get("http://wso2.org/claims/country"), "42"); } @Test @@ -458,24 +490,30 @@ public void testUserClaimReplaceNoFlowUser() throws ActionExecutionResponseProce @Test public void testUserInputReplace() throws ActionExecutionResponseProcessorException { + // /input/ paths are no longer a modify target; operations on them are silently ignored. FlowExecutionContext execCtx = createFlowExecutionContext(); execCtx.addUserInputData("consent", "false"); PerformableOperation inputOp = createOperation(Operation.REPLACE, "/input/consent", "true"); - executeSuccessResponse(execCtx, inputOp, Collections.emptyMap()); + ActionExecutionStatus status = executeSuccessResponse(execCtx, inputOp, Collections.emptyMap()); - assertEquals(execCtx.getUserInputData().get("consent"), "true"); + // Operation succeeds overall but /input/ is not a modify target — input data unchanged. + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + assertEquals(execCtx.getUserInputData().get("consent"), "false"); } @Test public void testUserInputReplaceCreatesNew() throws ActionExecutionResponseProcessorException { + // /input/ paths are no longer a modify target; operations on them are silently ignored. FlowExecutionContext execCtx = createFlowExecutionContext(); PerformableOperation inputOp = createOperation(Operation.REPLACE, "/input/consent", "true"); - executeSuccessResponse(execCtx, inputOp, Collections.emptyMap()); + ActionExecutionStatus status = executeSuccessResponse(execCtx, inputOp, Collections.emptyMap()); - assertEquals(execCtx.getUserInputData().get("consent"), "true"); + // Operation succeeds overall but /input/ is not a modify target — value is not created. + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + assertNull(execCtx.getUserInputData().get("consent")); } @Test @@ -549,8 +587,11 @@ public void testMultipleOperationsMixedResults() throws ActionExecutionResponseP ActionExecutionStatus status = executeSuccessResponse(execCtx, operations, Collections.emptyMap()); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - assertEquals(execCtx.getProperties().get("newProp"), "newValue"); - assertEquals(execCtx.getProperties().get("existingProp"), "updated"); + Map pendingProps = + capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + assertNotNull(pendingProps); + assertEquals(pendingProps.get("newProp"), "newValue"); + assertEquals(pendingProps.get("existingProp"), "updated"); } @Test @@ -761,6 +802,8 @@ private ActionExecutionStatus executeSuccessResponse( flowContext.add(InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); } + capturedFlowContext = flowContext; + ActionInvocationSuccessResponse successResponse = mock(ActionInvocationSuccessResponse.class); when(successResponse.getOperations()).thenReturn(operations); @@ -787,6 +830,8 @@ private ActionExecutionStatus executeSuccessResponseWithModifyPaths( flowContext.add(InFlowExtensionRequestBuilder.MODIFY_PATHS_KEY, modifyPaths); } + capturedFlowContext = flowContext; + ActionInvocationSuccessResponse successResponse = mock(ActionInvocationSuccessResponse.class); when(successResponse.getOperations()).thenReturn(operations); @@ -835,6 +880,9 @@ public void testNonStringValueForEncryptedPathHandledGracefully() assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); // Value should be coerced to String (default behavior for properties). - assertEquals(execCtx.getProperties().get("data"), "42"); + Map pendingProps = + capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + assertNotNull(pendingProps); + assertEquals(pendingProps.get("data"), "42"); } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java index 825e43dcb93c..17ce19decfe5 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java @@ -21,12 +21,12 @@ import org.testng.annotations.Test; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionEvent; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; /** @@ -37,57 +37,41 @@ public class InFlowExtensionEventTest { @Test public void testBuilderWithAllFields() { - Map userInputs = new HashMap<>(); - userInputs.put("email", "test@example.com"); - Map flowProperties = new HashMap<>(); flowProperties.put("riskScore", 85); InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() .flowType("REGISTRATION") - .userInputs(userInputs) + .flowId("flow-id-123") + .callbackUrl("https://example.com/callback") + .portalUrl("https://example.com/portal") .flowProperties(flowProperties) .build(); assertEquals(event.getFlowType(), "REGISTRATION"); - assertEquals(event.getUserInputs().get("email"), "test@example.com"); + assertEquals(event.getFlowId(), "flow-id-123"); + assertEquals(event.getCallbackUrl(), "https://example.com/callback"); + assertEquals(event.getPortalUrl(), "https://example.com/portal"); assertEquals(event.getFlowProperties().get("riskScore"), 85); } @Test - public void testBuilderWithNullMaps() { + public void testOptionalFieldsDefaultToNull() { InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() .flowType("LOGIN") - .userInputs(null) + .flowId("flow-id-456") .flowProperties(null) .build(); assertEquals(event.getFlowType(), "LOGIN"); - assertNotNull(event.getUserInputs()); - assertTrue(event.getUserInputs().isEmpty()); + assertEquals(event.getFlowId(), "flow-id-456"); + assertNull(event.getCallbackUrl()); + assertNull(event.getPortalUrl()); assertNotNull(event.getFlowProperties()); assertTrue(event.getFlowProperties().isEmpty()); } - @Test - public void testUserInputsAreUnmodifiable() { - - Map userInputs = new HashMap<>(); - userInputs.put("field", "value"); - - InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() - .userInputs(userInputs) - .build(); - - try { - event.getUserInputs().put("hack", "value"); - assertTrue(false, "Expected UnsupportedOperationException"); - } catch (UnsupportedOperationException e) { - // Expected — map is unmodifiable - } - } - @Test public void testFlowPropertiesAreUnmodifiable() { @@ -109,15 +93,15 @@ public void testFlowPropertiesAreUnmodifiable() { @Test public void testBuilderDoesNotShareMapReferences() { - Map userInputs = new HashMap<>(); - userInputs.put("email", "original@test.com"); + Map flowProperties = new HashMap<>(); + flowProperties.put("score", "original"); InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() - .userInputs(userInputs) + .flowProperties(flowProperties) .build(); // Mutating the original map should not affect the event - userInputs.put("email", "modified@test.com"); - assertEquals(event.getUserInputs().get("email"), "original@test.com"); + flowProperties.put("score", "modified"); + assertEquals(event.getFlowProperties().get("score"), "original"); } } From 74d6b83270804dff54a6fcd035e445126977b85d Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Mon, 27 Apr 2026 15:56:10 +0530 Subject: [PATCH 15/41] Adding Dynamic error handling and diagnostic logs. --- .../flow/execution/engine/Constants.java | 3 + .../executor/InFlowExtensionExecutor.java | 56 ++++++++++++------- .../InFlowExtensionResponseProcessor.java | 36 +++++++----- .../executor/InFlowExtensionExecutorTest.java | 13 ++++- .../InFlowExtensionResponseProcessorTest.java | 22 +++++--- 5 files changed, 85 insertions(+), 45 deletions(-) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java index c32891aff9f3..b5115d80c832 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java @@ -162,6 +162,9 @@ public enum ErrorMessages { ERROR_CODE_GET_CLAIM_META_DATA_FAILURE("65032", "Error while loading claim metadata.", "Error occurred loading the claim metadata for tenant: %s."), + ERROR_CODE_INFLOW_EXTENSION_ERROR("65033", + "InFlow extension reported an error.", + "%s"), // Client errors. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java index e7c726f1f2d4..086a736acfb1 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java @@ -76,6 +76,9 @@ public class InFlowExtensionExecutor implements Executor { private static final String ACTION_ID_METADATA_KEY = "actionId"; private static final String ERROR_TYPE_KEY = "errorType"; private static final String EXTENSION_ERROR_TYPE = "EXTENSION_ERROR"; + public static final String ERROR_MESSAGE_KEY = "errorMessage"; + public static final String ERROR_DESCRIPTION_KEY = "errorDescription"; + public static final String EXTENSION_ERROR_CODE = "65033"; @Override public String getName() { @@ -229,6 +232,14 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt response.setResult(ExecutorStatus.STATUS_RETRY); Failure failure = (Failure) executionStatus.getResponse(); if (failure != null) { + Map failureInfo = new HashMap<>(); + if (failure.getFailureReason() != null) { + failureInfo.put(ERROR_MESSAGE_KEY, failure.getFailureReason()); + } + if (failure.getFailureDescription() != null) { + failureInfo.put(ERROR_DESCRIPTION_KEY, failure.getFailureDescription()); + } + response.setAdditionalInfo(failureInfo); response.setErrorMessage(buildUserFacingErrorMessage(failure)); } break; @@ -236,8 +247,18 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt case ERROR: response.setResult(ExecutorStatus.STATUS_ERROR); Error error = (Error) executionStatus.getResponse(); + response.setErrorCode(EXTENSION_ERROR_CODE); if (error != null) { - response.setErrorMessage(buildUserFacingErrorMessage(error)); + Map errorInfo = new HashMap<>(); + if (error.getErrorMessage() != null) { + errorInfo.put(ERROR_MESSAGE_KEY, error.getErrorMessage()); + } + if (error.getErrorDescription() != null) { + errorInfo.put(ERROR_DESCRIPTION_KEY, error.getErrorDescription()); + } + response.setAdditionalInfo(errorInfo); + response.setErrorMessage(stripI18nBraces(error.getErrorMessage())); + response.setErrorDescription(stripI18nBraces(error.getErrorDescription())); } break; @@ -274,30 +295,25 @@ private String buildUserFacingErrorMessage(Failure failure) { return "The operation could not be completed due to an external service failure."; } + private ActionExecutorService getActionExecutorService() { + + return FlowExecutionEngineDataHolder.getInstance().getActionExecutorService(); + } + /** - * Build a user-facing error message from the error details returned by the external service. - * Prefers the errorDescription (human-readable). Falls back to errorMessage if description is absent. - * - * @param error The error object from the external service. - * @return A display-ready error message string. + * Strip the {@code {{...}}} wrapper from an i18n key so the JSP error page can resolve it + * via {@code AuthenticationEndpointUtil.i18n(resourceBundle, key)}. Raw text values (without + * the wrapper) and {@code null} are returned unchanged. */ - private String buildUserFacingErrorMessage(Error error) { - - String description = error.getErrorDescription(); - String message = error.getErrorMessage(); + private static String stripI18nBraces(String value) { - if (description != null && !description.isEmpty()) { - return description; + if (value == null) { + return null; } - if (message != null && !message.isEmpty()) { - return message; + if (value.startsWith("{{") && value.endsWith("}}") && value.length() > 4) { + return value.substring(2, value.length() - 2); } - return "An unexpected error occurred in the external service."; - } - - private ActionExecutorService getActionExecutorService() { - - return FlowExecutionEngineDataHolder.getInstance().getActionExecutorService(); + return value; } /** diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java index 96a462f50ae9..18457a430c50 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java @@ -53,7 +53,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.regex.Matcher; import java.util.regex.Pattern; /** @@ -80,7 +79,8 @@ public class InFlowExtensionResponseProcessor implements ActionExecutionResponse private static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; private static final String USER_CREDENTIALS_PATH_PREFIX = "/user/credentials/"; private static final String I18N_KEY_PREFIX = "inflow.extension."; - private static final Pattern I18N_KEY_PATTERN = Pattern.compile("^\\{\\{(.+)\\}\\}$"); + private static final Pattern SHORT_I18N_KEY_PATTERN = + Pattern.compile("^[a-z][a-z0-9]*(?:\\.[a-z0-9]+)+$"); @Override public ActionType getSupportedActionType() { @@ -328,6 +328,12 @@ public ActionExecutionStatus processErrorResponse(FlowContext flowContext LOG.debug("Processing error response from In-Flow Extension. Error: " + errorMessage + ", Description: " + errorDescription); } + + // Apply i18n key prefixing so the frontend can locate extension-specific error messages. + String actionName = flowContext.getValue(InFlowExtensionRequestBuilder.ACTION_NAME_KEY, String.class); + errorMessage = applyI18nKeyPrefix(errorMessage, actionName); + errorDescription = applyI18nKeyPrefix(errorDescription, actionName); + return new ErrorStatus(new Error(errorMessage, errorDescription)); } @@ -353,9 +359,14 @@ public ActionExecutionStatus processFailureResponse(FlowContext flowCon } /** - * Transform a short i18n key (e.g., {@code {{denied}}}) into a fully qualified key - * (e.g., {@code {{inflow.extension.risk.assessment.extension.denied}}}) using the - * sanitized action name as the prefix. Already-qualified keys are left unchanged. + * Transform a bare dot-separated short i18n key (e.g. {@code account.restricted.message}) + * into a fully qualified, frontend-ready wrapped key + * (e.g. {@code {{inflow.extension.risk.assessment.extension.account.restricted.message}}}), + * using the sanitized action name as the prefix. + * + *

      Plain text (sentences, single words, anything with spaces or special characters) is + * returned as-is. Keys that already start with {@value #I18N_KEY_PREFIX} are also left + * unchanged so the processor is idempotent.

      * * @param message The error message or reason string. May be null. * @param actionName The action display name. May be null. @@ -366,15 +377,14 @@ private String applyI18nKeyPrefix(String message, String actionName) { if (message == null || actionName == null) { return message; } - Matcher matcher = I18N_KEY_PATTERN.matcher(message); - if (matcher.matches()) { - String shortKey = matcher.group(1); - if (!shortKey.startsWith(I18N_KEY_PREFIX)) { - String sanitizedName = sanitizeConnectionName(actionName); - return "{{" + I18N_KEY_PREFIX + sanitizedName + "." + shortKey + "}}"; - } + if (message.startsWith(I18N_KEY_PREFIX)) { + return message; + } + if (!SHORT_I18N_KEY_PATTERN.matcher(message).matches()) { + return message; } - return message; + String sanitizedName = sanitizeConnectionName(actionName); + return "{{" + I18N_KEY_PREFIX + sanitizedName + "." + message + "}}"; } /** diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java index 5be65a794ec3..c95c36c11a8f 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java @@ -296,7 +296,10 @@ public void testExecuteError() throws Exception { ExecutorResponse response = executor.execute(context); assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); - assertEquals(response.getErrorMessage(), "DB connection failed"); + assertEquals(response.getErrorCode(), InFlowExtensionExecutor.EXTENSION_ERROR_CODE); + // errorMessage carries the Error reason/code field; errorDescription carries the human-readable text. + assertEquals(response.getErrorMessage(), "internal_error"); + assertEquals(response.getErrorDescription(), "DB connection failed"); } @Test @@ -321,7 +324,9 @@ public void testExecuteErrorNoDescription() throws Exception { ExecutorResponse response = executor.execute(context); assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); + assertEquals(response.getErrorCode(), InFlowExtensionExecutor.EXTENSION_ERROR_CODE); assertEquals(response.getErrorMessage(), "internal_error"); + assertNull(response.getErrorDescription()); } @Test @@ -346,8 +351,10 @@ public void testExecuteErrorBothNull() throws Exception { ExecutorResponse response = executor.execute(context); assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); - assertEquals(response.getErrorMessage(), - "An unexpected error occurred in the external service."); + assertEquals(response.getErrorCode(), InFlowExtensionExecutor.EXTENSION_ERROR_CODE); + // Both fields null → errorMessage and errorDescription remain null; errorCode alone triggers FE-65033 routing. + assertNull(response.getErrorMessage()); + assertNull(response.getErrorDescription()); } // ========================= execute — INCOMPLETE ========================= diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java index 0a030acd04ff..3fd4794cb929 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java @@ -636,9 +636,10 @@ public void testProcessFailureResponse() throws ActionExecutionResponseProcessor public void testProcessFailureResponseWithI18nKey() throws ActionExecutionResponseProcessorException { + // Extension sends a bare dot-separated short key ({{}} wrapping is rejected by JSON parsers). ActionInvocationFailureResponse failureResponse = mock(ActionInvocationFailureResponse.class); when(failureResponse.getFailureReason()).thenReturn("high_risk"); - when(failureResponse.getFailureDescription()).thenReturn("{{denied}}"); + when(failureResponse.getFailureDescription()).thenReturn("user.access.denied"); @SuppressWarnings("unchecked") ActionExecutionResponseContext responseContext = @@ -652,9 +653,10 @@ public void testProcessFailureResponseWithI18nKey() flowContext, responseContext); assertEquals(status.getStatus(), ActionExecutionStatus.Status.FAILED); + // Bare short key is prefixed and wrapped for frontend i18n resolution. assertEquals(status.getResponse().getFailureDescription(), - "{{inflow.extension.risk.assessment.extension.denied}}"); - // Plain reason string — not an i18n key, passes through unchanged. + "{{inflow.extension.risk.assessment.extension.user.access.denied}}"); + // Reason contains underscore — not a dot-separated key, passes through unchanged. assertEquals(status.getResponse().getFailureReason(), "high_risk"); } @@ -686,10 +688,12 @@ public void testProcessFailureResponseWithPlainText() public void testProcessFailureResponseWithFullyQualifiedKey() throws ActionExecutionResponseProcessorException { + // A string that does not match the bare short-key pattern (contains braces) must + // pass through untouched — the processor is not responsible for stripping artefacts. ActionInvocationFailureResponse failureResponse = mock(ActionInvocationFailureResponse.class); when(failureResponse.getFailureReason()).thenReturn("high_risk"); when(failureResponse.getFailureDescription()) - .thenReturn("{{inflow.extension.risk.assessment.extension.denied}}"); + .thenReturn("inflow.extension.risk.assessment.extension.user.access.denied"); @SuppressWarnings("unchecked") ActionExecutionResponseContext responseContext = @@ -702,9 +706,9 @@ public void testProcessFailureResponseWithFullyQualifiedKey() ActionExecutionStatus status = responseProcessor.processFailureResponse( flowContext, responseContext); - // Already-qualified key must not be double-prefixed. + // Already starts with "inflow.extension." — must not be double-prefixed. assertEquals(status.getResponse().getFailureDescription(), - "{{inflow.extension.risk.assessment.extension.denied}}"); + "inflow.extension.risk.assessment.extension.user.access.denied"); } @Test @@ -713,20 +717,20 @@ public void testProcessFailureResponseWithoutActionName() ActionInvocationFailureResponse failureResponse = mock(ActionInvocationFailureResponse.class); when(failureResponse.getFailureReason()).thenReturn("high_risk"); - when(failureResponse.getFailureDescription()).thenReturn("{{denied}}"); + when(failureResponse.getFailureDescription()).thenReturn("user.access.denied"); @SuppressWarnings("unchecked") ActionExecutionResponseContext responseContext = mock(ActionExecutionResponseContext.class); when(responseContext.getActionInvocationResponse()).thenReturn(failureResponse); - // No ACTION_NAME_KEY in FlowContext — no prefix should be applied. + // No ACTION_NAME_KEY in FlowContext — no prefix should be applied; value passes through. FlowContext flowContext = FlowContext.create(); ActionExecutionStatus status = responseProcessor.processFailureResponse( flowContext, responseContext); - assertEquals(status.getResponse().getFailureDescription(), "{{denied}}"); + assertEquals(status.getResponse().getFailureDescription(), "user.access.denied"); } // ========================= processErrorResponse ========================= From a51ef48e45d078efa1e2b7821eb8db65e924b9f9 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Tue, 28 Apr 2026 10:41:28 +0530 Subject: [PATCH 16/41] Redirect operation support --- .../executor/InFlowExtensionExecutor.java | 45 ++++++- .../InFlowExtensionRequestBuilder.java | 93 ++++++------- .../InFlowExtensionResponseProcessor.java | 47 +++++++ .../executor/InFlowExtensionExecutorTest.java | 124 +++++++++++++++++- .../InFlowExtensionRequestBuilderTest.java | 101 +++++++++++--- .../InFlowExtensionResponseProcessorTest.java | 103 +++++++++++++++ 6 files changed, 445 insertions(+), 68 deletions(-) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java index 086a736acfb1..5ff815428bcd 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java @@ -29,6 +29,7 @@ import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; +import org.wso2.carbon.identity.flow.execution.engine.Constants; import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; import org.wso2.carbon.utils.DiagnosticLog; import org.wso2.carbon.identity.flow.execution.engine.exception.FlowEngineException; @@ -43,6 +44,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; /** * This class is responsible for executing In-Flow Extension actions during flow execution. @@ -73,6 +75,7 @@ public class InFlowExtensionExecutor implements Executor { public static final String PENDING_CLAIMS_KEY = "pendingClaims"; public static final String PENDING_CREDENTIALS_KEY = "pendingCredentials"; public static final String PENDING_PROPERTIES_KEY = "pendingProperties"; + public static final String PENDING_REDIRECT_URL_KEY = "pendingRedirectUrl"; private static final String ACTION_ID_METADATA_KEY = "actionId"; private static final String ERROR_TYPE_KEY = "errorType"; private static final String EXTENSION_ERROR_TYPE = "EXTENSION_ERROR"; @@ -141,9 +144,7 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE ActionExecutionStatus executionStatus = actionExecutorService.execute( ActionType.IN_FLOW_EXTENSION, actionId, flowContext, context.getTenantDomain()); - // TODO: Handle the redirection as well, currently we are treating it as an error since the flow execution engine doesn't support it yet - - ExecutorResponse executionResponse = mapExecutionStatus(executionStatus); + ExecutorResponse executionResponse = mapExecutionStatus(executionStatus, flowContext, context); // On success, extract pending context updates collected by the response processor // and forward them to TaskExecutionNode via ExecutorResponse fields. @@ -209,12 +210,16 @@ public ExecutorResponse rollback(FlowExecutionContext context) { /** * Map the {@link ActionExecutionStatus} to an {@link ExecutorResponse}. - * Only performs status translation — context updates are handled by the response processor. + * Performs status translation and (for INCOMPLETE/redirect) generates the OTFI used by + * {@code FlowExecutionService} to swap caches on resume — same pattern as {@code MagicLinkExecutor}. * * @param executionStatus The status returned by ActionExecutorService. + * @param flowContext The action {@link FlowContext} where the response processor stashed the redirect URL. + * @param context The engine {@link FlowExecutionContext} (used for OTFI collision-guard). * @return The ExecutorResponse for the flow execution engine. */ - private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionStatus) { + private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionStatus, + FlowContext flowContext, FlowExecutionContext context) { ExecutorResponse response = new ExecutorResponse(); @@ -262,9 +267,35 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt } break; - case INCOMPLETE: - response.setResult(ExecutorStatus.STATUS_ERROR); + case INCOMPLETE: { + String redirectUrl = flowContext.getValue(PENDING_REDIRECT_URL_KEY, String.class); + if (redirectUrl == null || redirectUrl.isEmpty()) { + // Defensive: response processor should have rejected this earlier. + response.setResult(ExecutorStatus.STATUS_ERROR); + response.setErrorMessage("Extension returned INCOMPLETE without a redirect URL."); + break; + } + + // Generate OTFI exactly like MagicLinkExecutor — keeps the cache-swap mechanism + // in FlowExecutionService unchanged across executors. + String otfi = UUID.randomUUID().toString(); + while (otfi.equals(context.getContextIdentifier())) { + otfi = UUID.randomUUID().toString(); + } + Map redirectProps = new HashMap<>(); + redirectProps.put(Constants.OTFI, otfi); + response.setContextProperty(redirectProps); + + String separator = redirectUrl.contains("?") ? "&" : "?"; + String urlWithFlowId = redirectUrl + separator + "flowId=" + otfi; + + Map redirectInfo = new HashMap<>(); + redirectInfo.put(Constants.REDIRECT_URL, urlWithFlowId); + response.setAdditionalInfo(redirectInfo); + + response.setResult(ExecutorStatus.STATUS_EXTERNAL_REDIRECTION); break; + } default: LOG.warn("Unknown execution status: " + executionStatus.getStatus()); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java index 185959fa5f76..da6a552f0a95 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java @@ -133,9 +133,8 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex flowContext.add(ACTION_NAME_KEY, actionName); } - // Build allowed operations from modify paths. - List allowedOperations = buildAllowedOperationsFromModify( - accessConfig, flowContext); + // Build allowed operations: REPLACE (from modify paths, if any) + REDIRECT (always). + List allowedOperations = buildAllowedOperations(accessConfig, flowContext); // Determine certificate PEM for outbound encryption. String certificatePEM = null; @@ -154,63 +153,69 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex } /** - * Build allowed operations from the modify paths in {@link AccessConfig}. - * All modify paths are mapped to a single REPLACE {@link AllowedOperation}. - * Path type annotations (e.g. {@code []} or {@code [schema]}) are stripped and stored - * in the {@link FlowContext} under {@link InFlowExtensionExecutor#PATH_TYPE_ANNOTATIONS_KEY} + * Build the allowed operations advertised to the external extension service. Always + * includes a REDIRECT operation so the extension may signal external redirection. A + * REPLACE operation is added only when the access config defines modify paths. Path + * type annotations (e.g. {@code []} or {@code [schema]}) are stripped and stored in + * the {@link FlowContext} under {@link InFlowExtensionExecutor#PATH_TYPE_ANNOTATIONS_KEY} * for the response processor. * * @param accessConfig The access config containing modify paths (may be null). * @param flowContext The FlowContext to store path annotations. - * @return A singleton list containing one REPLACE operation, or empty list if no modify paths. + * @return List of allowed operations (REPLACE if applicable, plus REDIRECT). */ - private List buildAllowedOperationsFromModify(AccessConfig accessConfig, - FlowContext flowContext) { + private List buildAllowedOperations(AccessConfig accessConfig, + FlowContext flowContext) { - if (accessConfig == null || accessConfig.getModify() == null || accessConfig.getModify().isEmpty()) { - return Collections.emptyList(); - } - - List cleanPaths = new ArrayList<>(); - Map pathTypeAnnotations = new HashMap<>(); + List allowedOps = new ArrayList<>(); - for (ContextPath modifyPath : accessConfig.getModify()) { - String rawPath = modifyPath.getPath(); - if (rawPath == null) { - continue; - } + if (accessConfig != null && accessConfig.getModify() != null && !accessConfig.getModify().isEmpty()) { + List cleanPaths = new ArrayList<>(); + Map pathTypeAnnotations = new HashMap<>(); - String[] stripped = PathTypeAnnotationUtil.stripAnnotation(rawPath); - String cleanPath = stripped[0]; - String annotation = stripped[1]; - if (annotation != null) { - if (!PathTypeAnnotationUtil.validateAnnotationLimits(annotation)) { - LOG.warn("Annotation for path " + cleanPath - + " exceeds maximum attribute limit. Skipping path."); + for (ContextPath modifyPath : accessConfig.getModify()) { + String rawPath = modifyPath.getPath(); + if (rawPath == null) { continue; } - pathTypeAnnotations.put(cleanPath, annotation); + + String[] stripped = PathTypeAnnotationUtil.stripAnnotation(rawPath); + String cleanPath = stripped[0]; + String annotation = stripped[1]; + if (annotation != null) { + if (!PathTypeAnnotationUtil.validateAnnotationLimits(annotation)) { + LOG.warn("Annotation for path " + cleanPath + + " exceeds maximum attribute limit. Skipping path."); + continue; + } + pathTypeAnnotations.put(cleanPath, annotation); + } + cleanPaths.add(cleanPath); } - cleanPaths.add(cleanPath); - } - if (cleanPaths.isEmpty()) { - return Collections.emptyList(); - } + if (!cleanPaths.isEmpty()) { + AllowedOperation replaceOp = new AllowedOperation(); + replaceOp.setOp(Operation.REPLACE); + replaceOp.setPaths(cleanPaths); + allowedOps.add(replaceOp); - AllowedOperation replaceOp = new AllowedOperation(); - replaceOp.setOp(Operation.REPLACE); - replaceOp.setPaths(cleanPaths); + if (!pathTypeAnnotations.isEmpty()) { + flowContext.add(InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); + } - if (!pathTypeAnnotations.isEmpty()) { - flowContext.add(InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); + if (LOG.isDebugEnabled()) { + LOG.debug("Built REPLACE allowed operation with " + cleanPaths.size() + + " modify paths. Path annotations: " + pathTypeAnnotations.size()); + } + } } - if (LOG.isDebugEnabled()) { - LOG.debug("Built REPLACE allowed operation with " + cleanPaths.size() - + " modify paths. Path annotations: " + pathTypeAnnotations.size()); - } - return Collections.singletonList(replaceOp); + // REDIRECT is always advertised so any extension may signal mid-flow external redirection. + AllowedOperation redirectOp = new AllowedOperation(); + redirectOp.setOp(Operation.REDIRECT); + allowedOps.add(redirectOp); + + return allowedOps; } /** diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java index 18457a430c50..f4abc8a063f8 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java @@ -29,6 +29,7 @@ import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionStatus; import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationErrorResponse; import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationFailureResponse; +import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationIncompleteResponse; import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationSuccessResponse; import org.wso2.carbon.identity.action.execution.api.model.ActionType; import org.wso2.carbon.identity.action.execution.api.model.Error; @@ -36,6 +37,9 @@ import org.wso2.carbon.identity.action.execution.api.model.FailedStatus; import org.wso2.carbon.identity.action.execution.api.model.Failure; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.action.execution.api.model.Incomplete; +import org.wso2.carbon.identity.action.execution.api.model.IncompleteStatus; +import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.action.execution.api.model.SuccessStatus; @@ -316,6 +320,49 @@ private String extractNameFromPath(String path, String prefix) { return remaining; } + @Override + public ActionExecutionStatus processIncompleteResponse(FlowContext flowContext, + ActionExecutionResponseContext responseContext) + throws ActionExecutionResponseProcessorException { + + List operations = + responseContext.getActionInvocationResponse().getOperations(); + + // Contract: INCOMPLETE must carry a REDIRECT op. If present, every other op is + // intentionally discarded — the extension is expected to resend (possibly with + // different values) on the resume call after the user returns from the redirect. + // REDIRECT operations carry their target in the dedicated `url` field + // (PerformableOperation rejects path/value for REDIRECT and rejects url for everything else). + String redirectUrl = null; + int ignoredOpCount = 0; + if (operations != null) { + for (PerformableOperation op : operations) { + if (op.getOp() == Operation.REDIRECT) { + redirectUrl = op.getUrl(); + } else { + ignoredOpCount++; + } + } + } + + if (redirectUrl == null || redirectUrl.isEmpty()) { + throw new ActionExecutionResponseProcessorException( + "INCOMPLETE response from In-Flow Extension must contain a REDIRECT operation."); + } + + if (ignoredOpCount > 0 && LOG.isDebugEnabled()) { + LOG.debug("Ignored " + ignoredOpCount + " non-REDIRECT operation(s) on INCOMPLETE response. " + + "REPLACE ops on the redirect call are by-contract dropped — the extension is " + + "expected to resend them on the resume call after callback."); + } + + flowContext.add(InFlowExtensionExecutor.PENDING_REDIRECT_URL_KEY, redirectUrl); + + return new IncompleteStatus.Builder() + .responseContext(Collections.emptyMap()) + .build(); + } + @Override public ActionExecutionStatus processErrorResponse(FlowContext flowContext, ActionExecutionResponseContext responseContext) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java index c95c36c11a8f..26c4f4cf1c39 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java @@ -54,6 +54,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; @@ -361,8 +362,11 @@ public void testExecuteErrorBothNull() throws Exception { @Test @SuppressWarnings("unchecked") - public void testExecuteIncomplete() throws Exception { + public void testExecuteIncompleteWithoutRedirectUrlReturnsError() throws Exception { + // INCOMPLETE without a stashed redirect URL is a contract violation — + // the response processor should normally have thrown, but the executor + // defends against it as well by returning STATUS_ERROR with a clear message. Map metadata = new HashMap<>(); metadata.put("actionId", "test-action-001"); FlowExecutionContext context = createContextWithMetadata(metadata); @@ -379,6 +383,124 @@ public void testExecuteIncomplete() throws Exception { ExecutorResponse response = executor.execute(context); assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); + assertEquals(response.getErrorMessage(), + "Extension returned INCOMPLETE without a redirect URL."); + } + + @Test + @SuppressWarnings("unchecked") + public void testExecuteIncompleteWithRedirectUrlReturnsExternalRedirection() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + // Set a context identifier so the OTFI collision-guard has something to compare against. + context.setContextIdentifier("original-flow-id"); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + + ActionExecutionStatus incompleteStatus = mock(ActionExecutionStatus.class); + when(incompleteStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.INCOMPLETE); + + // Simulate the response processor stashing the redirect URL into the FlowContext + // during the actionExecutorService.execute call. + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenAnswer(invocation -> { + FlowContext fc = invocation.getArgument(2); + fc.add(InFlowExtensionExecutor.PENDING_REDIRECT_URL_KEY, + "https://example.com/step-up"); + return incompleteStatus; + }); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorStatus.STATUS_EXTERNAL_REDIRECTION); + + // OTFI must be set on contextProperties so FlowExecutionService can swap caches on resume. + Map ctxProps = response.getContextProperties(); + assertNotNull(ctxProps); + Object otfi = ctxProps.get(org.wso2.carbon.identity.flow.execution.engine.Constants.OTFI); + assertNotNull(otfi); + assertTrue(otfi instanceof String); + assertFalse(((String) otfi).isEmpty()); + // OTFI must not collide with the original context identifier. + assertFalse("original-flow-id".equals(otfi)); + + // Redirect URL must carry the OTFI as a flowId query parameter. + Map additionalInfo = response.getAdditionalInfo(); + assertNotNull(additionalInfo); + String redirectUrl = additionalInfo.get( + org.wso2.carbon.identity.flow.execution.engine.Constants.REDIRECT_URL); + assertNotNull(redirectUrl); + assertEquals(redirectUrl, "https://example.com/step-up?flowId=" + otfi); + } + + @Test + @SuppressWarnings("unchecked") + public void testExecuteIncompleteRedirectAppendsFlowIdWithAmpersandWhenUrlHasQuery() + throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + context.setContextIdentifier("original-flow-id"); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + + ActionExecutionStatus incompleteStatus = mock(ActionExecutionStatus.class); + when(incompleteStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.INCOMPLETE); + + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenAnswer(invocation -> { + FlowContext fc = invocation.getArgument(2); + // URL already has a query string — the executor must use & not ?. + fc.add(InFlowExtensionExecutor.PENDING_REDIRECT_URL_KEY, + "https://example.com/step-up?ref=abc"); + return incompleteStatus; + }); + + ExecutorResponse response = executor.execute(context); + + assertEquals(response.getResult(), ExecutorStatus.STATUS_EXTERNAL_REDIRECTION); + String otfi = (String) response.getContextProperties() + .get(org.wso2.carbon.identity.flow.execution.engine.Constants.OTFI); + String redirectUrl = response.getAdditionalInfo() + .get(org.wso2.carbon.identity.flow.execution.engine.Constants.REDIRECT_URL); + assertEquals(redirectUrl, "https://example.com/step-up?ref=abc&flowId=" + otfi); + } + + @Test + @SuppressWarnings("unchecked") + public void testExecuteIncompleteRedirectEmptyUrlReturnsError() throws Exception { + + Map metadata = new HashMap<>(); + metadata.put("actionId", "test-action-001"); + FlowExecutionContext context = createContextWithMetadata(metadata); + + when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + + ActionExecutionStatus incompleteStatus = mock(ActionExecutionStatus.class); + when(incompleteStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.INCOMPLETE); + + when(actionExecutorService.execute( + eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + any(FlowContext.class), eq("carbon.super"))) + .thenAnswer(invocation -> { + FlowContext fc = invocation.getArgument(2); + fc.add(InFlowExtensionExecutor.PENDING_REDIRECT_URL_KEY, ""); + return incompleteStatus; + }); + + ExecutorResponse response = executor.execute(context); + + // Empty URL is treated the same as missing — defensive STATUS_ERROR. + assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); + assertEquals(response.getErrorMessage(), + "Extension returned INCOMPLETE without a redirect URL."); } // ========================= execute — null status ========================= diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java index 3a2fc272ac46..5c16807ca616 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java @@ -157,9 +157,13 @@ public void testBuildRequestWithValidModifyPaths() List ops = request.getAllowedOperations(); assertNotNull(ops); - assertEquals(ops.size(), 1); - assertEquals(ops.get(0).getOp(), Operation.REPLACE); - assertTrue(ops.get(0).getPaths().contains("/properties/riskScore")); + // REPLACE + REDIRECT (REDIRECT is always advertised). + assertEquals(ops.size(), 2); + AllowedOperation replaceOp = findOperation(ops, Operation.REPLACE); + assertNotNull(replaceOp, "REPLACE should be present when modify paths are configured"); + assertTrue(replaceOp.getPaths().contains("/properties/riskScore")); + assertNotNull(findOperation(ops, Operation.REDIRECT), + "REDIRECT should always be present"); } @Test @@ -170,13 +174,14 @@ public void testBuildRequestWithNoAccessConfig() FlowContext flowContext = FlowContext.create() .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); - // No action → no access config → no modify paths → empty allowed ops. + // No action → no access config → no modify paths → only REDIRECT (always present). ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); List ops = request.getAllowedOperations(); assertNotNull(ops); - assertTrue(ops.isEmpty()); + assertEquals(ops.size(), 1); + assertEquals(ops.get(0).getOp(), Operation.REDIRECT); } @Test @@ -194,7 +199,49 @@ public void testBuildRequestWithEmptyModifyPaths() List ops = request.getAllowedOperations(); assertNotNull(ops); - assertTrue(ops.isEmpty()); + // No modify paths → only REDIRECT. + assertEquals(ops.size(), 1); + assertEquals(ops.get(0).getOp(), Operation.REDIRECT); + } + + // ========================= REDIRECT always advertised ========================= + + @Test + public void testRedirectIsAdvertisedAlongsideReplace() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + AccessConfig accessConfig = new AccessConfig(null, + Arrays.asList(new ContextPath("/properties/riskScore", false))); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + AllowedOperation redirectOp = findOperation(request.getAllowedOperations(), Operation.REDIRECT); + assertNotNull(redirectOp); + // REDIRECT does not target a path — paths must be null or empty. + assertTrue(redirectOp.getPaths() == null || redirectOp.getPaths().isEmpty(), + "REDIRECT must not carry any paths"); + } + + @Test + public void testRedirectIsAdvertisedWhenNoAccessConfig() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + // REDIRECT must be available even without any modify config so extensions can always + // signal mid-flow redirects. + AllowedOperation redirectOp = findOperation(request.getAllowedOperations(), Operation.REDIRECT); + assertNotNull(redirectOp); } // ========================= Path annotation stripping ========================= @@ -215,7 +262,8 @@ public void testPathAnnotationStrippingSimpleArray() flowContext, mockReqCtx(accessConfig, null)); // AllowedOperation should have clean path (without {[String]}). - AllowedOperation op = request.getAllowedOperations().get(0); + AllowedOperation op = findOperation(request.getAllowedOperations(), Operation.REPLACE); + assertNotNull(op); assertTrue(op.getPaths().contains("/properties/riskFactors")); assertFalse(op.getPaths().contains("/properties/riskFactors{[String]}")); @@ -241,7 +289,8 @@ public void testPathAnnotationStrippingSchemaAnnotation() ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); - AllowedOperation op = request.getAllowedOperations().get(0); + AllowedOperation op = findOperation(request.getAllowedOperations(), Operation.REPLACE); + assertNotNull(op); assertTrue(op.getPaths().contains("/properties/items")); assertFalse(op.getPaths().contains("/properties/items{name: String, count: Integer}")); @@ -287,7 +336,8 @@ public void testMultipleAnnotatedPaths() ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); - AllowedOperation op = request.getAllowedOperations().get(0); + AllowedOperation op = findOperation(request.getAllowedOperations(), Operation.REPLACE); + assertNotNull(op); assertEquals(op.getPaths().size(), 3); assertTrue(op.getPaths().contains("/properties/riskScore")); assertTrue(op.getPaths().contains("/properties/riskFactors")); @@ -323,10 +373,11 @@ public void testModifyPathsDoNotAffectExpose() ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); - // Modify paths should produce REPLACE allowed operation. + // Modify paths should produce a REPLACE allowed op alongside the always-present REDIRECT. List ops = request.getAllowedOperations(); - assertEquals(ops.size(), 1); - assertEquals(ops.get(0).getOp(), Operation.REPLACE); + assertEquals(ops.size(), 2); + assertNotNull(findOperation(ops, Operation.REPLACE)); + assertNotNull(findOperation(ops, Operation.REDIRECT)); // Properties should NOT be in event — expose and modify are independent. InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); @@ -349,11 +400,13 @@ public void testMultipleModifyPathsProduceSingleReplaceOperation() ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); - // All modify paths should be grouped into a single REPLACE operation. + // All modify paths grouped into a single REPLACE op (REDIRECT is always added too). List ops = request.getAllowedOperations(); - assertEquals(ops.size(), 1); - assertEquals(ops.get(0).getOp(), Operation.REPLACE); - assertEquals(ops.get(0).getPaths().size(), 3); + assertEquals(ops.size(), 2); + AllowedOperation replaceOp = findOperation(ops, Operation.REPLACE); + assertNotNull(replaceOp); + assertEquals(replaceOp.getPaths().size(), 3); + assertNotNull(findOperation(ops, Operation.REDIRECT)); } // ========================= Expose filtering ========================= @@ -571,6 +624,22 @@ public void testEncryptionFailureThrowsException() // ========================= Helper methods ========================= + /** + * Locate the first AllowedOperation in {@code ops} matching {@code op}, or null if absent. + */ + private AllowedOperation findOperation(List ops, Operation op) { + + if (ops == null) { + return null; + } + for (AllowedOperation candidate : ops) { + if (candidate.getOp() == op) { + return candidate; + } + } + return null; + } + /** * Create a mock ActionExecutionRequestContext whose getAction() returns an InFlowExtensionAction * configured with the given access config and encryption. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java index 3fd4794cb929..1a5119797da0 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java @@ -27,11 +27,13 @@ import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionStatus; import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationErrorResponse; import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationFailureResponse; +import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationIncompleteResponse; import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationSuccessResponse; import org.wso2.carbon.identity.action.execution.api.model.ActionType; import org.wso2.carbon.identity.action.execution.api.model.Error; import org.wso2.carbon.identity.action.execution.api.model.Failure; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.action.execution.api.model.Incomplete; import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; import org.wso2.carbon.identity.action.execution.api.model.Success; @@ -758,6 +760,97 @@ public void testProcessErrorResponse() throws ActionExecutionResponseProcessorEx assertEquals(status.getResponse().getErrorDescription(), "Database connection failed"); } + // ========================= processIncompleteResponse ========================= + + @Test + public void testProcessIncompleteResponseWithRedirectStashesUrlAndReturnsIncomplete() + throws ActionExecutionResponseProcessorException { + + PerformableOperation redirect = createRedirectOperation("https://example.com/step-up"); + + FlowContext flowContext = FlowContext.create(); + ActionExecutionStatus status = executeIncompleteResponse( + flowContext, Collections.singletonList(redirect)); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.INCOMPLETE); + // URL must be stashed under the key the executor reads. + assertEquals(flowContext.getValue(InFlowExtensionExecutor.PENDING_REDIRECT_URL_KEY, String.class), + "https://example.com/step-up"); + } + + @Test + public void testProcessIncompleteResponseIgnoresNonRedirectOperations() + throws ActionExecutionResponseProcessorException { + + // Contract: REPLACE ops sent alongside REDIRECT on an INCOMPLETE response are dropped + // — the extension is expected to resend them on the resume call. + PerformableOperation redirect = createRedirectOperation("https://example.com/step-up"); + PerformableOperation replace = createOperation( + Operation.REPLACE, "/properties/riskScore", "42"); + + FlowContext flowContext = FlowContext.create(); + ActionExecutionStatus status = executeIncompleteResponse( + flowContext, Arrays.asList(redirect, replace)); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.INCOMPLETE); + // Redirect URL is captured, but the REPLACE must NOT have produced any pending props. + assertEquals(flowContext.getValue(InFlowExtensionExecutor.PENDING_REDIRECT_URL_KEY, String.class), + "https://example.com/step-up"); + assertNull(flowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class)); + assertNull(flowContext.getValue(InFlowExtensionExecutor.PENDING_CLAIMS_KEY, Map.class)); + assertNull(flowContext.getValue(InFlowExtensionExecutor.PENDING_CREDENTIALS_KEY, Map.class)); + } + + @Test(expectedExceptions = ActionExecutionResponseProcessorException.class) + public void testProcessIncompleteResponseWithoutRedirectThrows() + throws ActionExecutionResponseProcessorException { + + // INCOMPLETE without a REDIRECT op is a contract violation. + PerformableOperation replace = createOperation( + Operation.REPLACE, "/properties/riskScore", "42"); + + executeIncompleteResponse(FlowContext.create(), Collections.singletonList(replace)); + } + + @Test(expectedExceptions = ActionExecutionResponseProcessorException.class) + public void testProcessIncompleteResponseWithEmptyOperationsThrows() + throws ActionExecutionResponseProcessorException { + + executeIncompleteResponse(FlowContext.create(), Collections.emptyList()); + } + + @Test(expectedExceptions = ActionExecutionResponseProcessorException.class) + public void testProcessIncompleteResponseWithNullOperationsThrows() + throws ActionExecutionResponseProcessorException { + + executeIncompleteResponse(FlowContext.create(), null); + } + + @Test(expectedExceptions = ActionExecutionResponseProcessorException.class) + public void testProcessIncompleteResponseWithEmptyRedirectUrlThrows() + throws ActionExecutionResponseProcessorException { + + PerformableOperation emptyRedirect = createRedirectOperation(""); + + executeIncompleteResponse(FlowContext.create(), Collections.singletonList(emptyRedirect)); + } + + @SuppressWarnings("unchecked") + private ActionExecutionStatus executeIncompleteResponse( + FlowContext flowContext, List operations) + throws ActionExecutionResponseProcessorException { + + ActionInvocationIncompleteResponse incompleteResponse = + mock(ActionInvocationIncompleteResponse.class); + when(incompleteResponse.getOperations()).thenReturn(operations); + + ActionExecutionResponseContext responseContext = + mock(ActionExecutionResponseContext.class); + when(responseContext.getActionInvocationResponse()).thenReturn(incompleteResponse); + + return responseProcessor.processIncompleteResponse(flowContext, responseContext); + } + // ========================= Helper methods ========================= private FlowExecutionContext createFlowExecutionContext() { @@ -785,6 +878,16 @@ private PerformableOperation createOperation(Operation op, String path, Object v return operation; } + private PerformableOperation createRedirectOperation(String url) { + + // PerformableOperation rejects setPath/setValue for REDIRECT and rejects setUrl for + // every other op — REDIRECT carries its target in the dedicated `url` field. + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REDIRECT); + operation.setUrl(url); + return operation; + } + private ActionExecutionStatus executeSuccessResponse( FlowExecutionContext execCtx, PerformableOperation operation, Map pathTypeAnnotations) From a52f600d07ebe3d599001cd7fc58a7463cf5d934 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Thu, 30 Apr 2026 11:22:13 +0530 Subject: [PATCH 17/41] Flow execution context handover filtering and flow context tree metadata service via deployment.toml configuration. --- .../executor/InFlowExtensionExecutor.java | 16 ++- .../InFlowExtensionRequestBuilder.java | 23 +++- .../InFlowExtensionResponseProcessor.java | 1 - .../FlowExecutionEngineDataHolder.java | 29 +++++ .../HierarchicalPrefixMatcherTest.java | 3 +- .../executor/InFlowExtensionExecutorTest.java | 12 +- .../InFlowExtensionRequestBuilderTest.java | 68 +++++++++++- .../InFlowExtensionResponseProcessorTest.java | 6 +- .../executor/PathTypeAnnotationUtilTest.java | 3 +- .../extension}/model/AccessConfigTest.java | 4 +- .../model/InFlowExtensionEventTest.java | 2 +- .../model/OperationExecutionResultTest.java | 2 +- .../src/test/resources/testng.xml | 22 ++-- .../resources/identity.xml.j2 | 104 ++++++++++++++++++ 14 files changed, 259 insertions(+), 36 deletions(-) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/{ => inflow/extension}/executor/HierarchicalPrefixMatcherTest.java (96%) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/{ => inflow/extension}/executor/InFlowExtensionExecutorTest.java (97%) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/{ => inflow/extension}/executor/InFlowExtensionRequestBuilderTest.java (92%) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/{ => inflow/extension}/executor/InFlowExtensionResponseProcessorTest.java (98%) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/{ => inflow/extension}/executor/PathTypeAnnotationUtilTest.java (99%) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/{ => inflow/extension}/model/AccessConfigTest.java (96%) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/{ => inflow/extension}/model/InFlowExtensionEventTest.java (97%) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/{ => inflow/extension}/model/OperationExecutionResultTest.java (96%) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java index 5ff815428bcd..2948dde6e511 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java @@ -33,6 +33,9 @@ import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; import org.wso2.carbon.utils.DiagnosticLog; import org.wso2.carbon.identity.flow.execution.engine.exception.FlowEngineException; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverPolicy; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowExecutionContextFilter; import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; @@ -71,6 +74,7 @@ public class InFlowExtensionExecutor implements Executor { private static final String EXECUTOR_NAME = "InFlowExtensionExecutor"; public static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; + public static final String HANDOVER_POLICY_KEY = "handoverPolicy"; public static final String PATH_TYPE_ANNOTATIONS_KEY = "pathTypeAnnotations"; public static final String PENDING_CLAIMS_KEY = "pendingClaims"; public static final String PENDING_CREDENTIALS_KEY = "pendingCredentials"; @@ -137,8 +141,18 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE } try { + // Resolve the per-flow-type handover policy and hand the action framework only a + // FILTERED copy of the FlowExecutionContext (non-whitelisted fields nulled out). + // The original `context` is untouched and continues to drive the engine's own + // bookkeeping (OTFI cache swap, TaskExecutionNode pending-update propagation). + FlowContextHandoverConfig handoverConfig = FlowExecutionEngineDataHolder.getInstance() + .getFlowContextHandoverConfig(); + FlowContextHandoverPolicy policy = handoverConfig.resolve(context.getFlowType()); + FlowExecutionContext filteredContext = FlowExecutionContextFilter.filter(context, policy); + FlowContext flowContext = FlowContext.create() - .add(FLOW_EXECUTION_CONTEXT_KEY, context); + .add(FLOW_EXECUTION_CONTEXT_KEY, filteredContext) + .add(HANDOVER_POLICY_KEY, policy); // TODO: Have and switch-case ActionExecutionStatus executionStatus = actionExecutorService.execute( diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java index da6a552f0a95..f8bfe7cebb91 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java @@ -38,6 +38,7 @@ import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionRequestBuilder; import org.wso2.carbon.identity.action.management.api.model.Action; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverPolicy; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; @@ -210,10 +211,16 @@ private List buildAllowedOperations(AccessConfig accessConfig, } } - // REDIRECT is always advertised so any extension may signal mid-flow external redirection. - AllowedOperation redirectOp = new AllowedOperation(); - redirectOp.setOp(Operation.REDIRECT); - allowedOps.add(redirectOp); + // REDIRECT is advertised when the per-flow-type handover policy allows it. A null + // policy (test fixtures or unconfigured deployments) preserves the prior behaviour of + // always permitting REDIRECT. + FlowContextHandoverPolicy policy = flowContext.getValue( + InFlowExtensionExecutor.HANDOVER_POLICY_KEY, FlowContextHandoverPolicy.class); + if (policy == null || policy.isRedirectionEnabled()) { + AllowedOperation redirectOp = new AllowedOperation(); + redirectOp.setOp(Operation.REDIRECT); + allowedOps.add(redirectOp); + } return allowedOps; } @@ -284,6 +291,14 @@ private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List properties = context.getProperties(); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java index f4abc8a063f8..c0259686f768 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java @@ -47,7 +47,6 @@ import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; -import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.utils.DiagnosticLog; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java index 8a38b6a0bc3f..099280ce5391 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java @@ -25,6 +25,7 @@ import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverConfig; import org.wso2.carbon.identity.flow.execution.engine.listener.FlowExecutionListener; import org.wso2.carbon.identity.flow.mgt.FlowMgtService; import org.wso2.carbon.identity.input.validation.mgt.services.InputValidationManagementService; @@ -53,6 +54,7 @@ public class FlowExecutionEngineDataHolder { private ActionExecutorService actionExecutorService; private ActionManagementService actionManagementService; private CertificateManagementService certificateManagementService; + private FlowContextHandoverConfig flowContextHandoverConfig; private List flowExecutionListeners = new ArrayList<>(); private FlowExecutionEngineDataHolder() { @@ -284,4 +286,31 @@ public void setCertificateManagementService(CertificateManagementService certifi this.certificateManagementService = certificateManagementService; } + + /** + * Get the In-Flow Extension context handover config. Lazily initialised on first access + * (the constructor reads from {@code IdentityUtil} which requires the carbon configuration + * to be loaded — keeping this lazy avoids ordering issues with OSGi component activation). + * + * @return The handover config, never null. + */ + public FlowContextHandoverConfig getFlowContextHandoverConfig() { + + if (flowContextHandoverConfig == null) { + synchronized (this) { + if (flowContextHandoverConfig == null) { + flowContextHandoverConfig = new FlowContextHandoverConfig(); + } + } + } + return flowContextHandoverConfig; + } + + /** + * Override the handover config. Intended for tests only. + */ + public void setFlowContextHandoverConfig(FlowContextHandoverConfig flowContextHandoverConfig) { + + this.flowContextHandoverConfig = flowContextHandoverConfig; + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcherTest.java similarity index 96% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcherTest.java index 1d40efa00f4d..7af9f74e1488 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/HierarchicalPrefixMatcherTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcherTest.java @@ -16,11 +16,10 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.executor; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.HierarchicalPrefixMatcher; import java.util.Arrays; import java.util.Collections; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java similarity index 97% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java index 26c4f4cf1c39..48eabe5cf521 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.executor; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -34,9 +34,9 @@ import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverPolicy; import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionResponseProcessor; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; @@ -83,6 +83,12 @@ public void setUp() { mock(FlowExecutionEngineDataHolder.class); when(holderInstance.getActionExecutorService()).thenReturn(actionExecutorService); + // The executor consults the handover config on every run. Stub it to a permissive + // policy so the existing tests remain focused on status mapping rather than filtering. + FlowContextHandoverConfig handoverConfig = mock(FlowContextHandoverConfig.class); + when(handoverConfig.resolve(any())).thenReturn(FlowContextHandoverPolicy.permissive()); + when(holderInstance.getFlowContextHandoverConfig()).thenReturn(handoverConfig); + holderMock = mockStatic(FlowExecutionEngineDataHolder.class); holderMock.when(FlowExecutionEngineDataHolder::getInstance).thenReturn(holderInstance); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilderTest.java similarity index 92% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilderTest.java index 5c16807ca616..74709e518a81 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilderTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.executor; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; import org.mockito.MockedStatic; import org.testng.annotations.AfterMethod; @@ -31,10 +31,6 @@ import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionEvent; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionRequestBuilder; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.JWEEncryptionUtil; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; @@ -436,6 +432,68 @@ public void testExposeFilteringOnlyExposedAreaIncluded() assertNotNull(event.getTenant()); } + @Test + public void testFlowPortalUrlExposed() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + execCtx.setPortalUrl("https://localhost:9443/accounts/recovery"); + + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/flow/portalUrl", false)), null); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertEquals(event.getPortalUrl(), "https://localhost:9443/accounts/recovery"); + } + + @Test + public void testFlowPortalUrlNotExposedYieldsNull() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + execCtx.setPortalUrl("https://localhost:9443/accounts/recovery"); + + // /flow/portalUrl NOT in expose list — must be omitted from the event even when + // the context has a value, mirroring the rest of the expose-gated paths. + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/flow/flowType", false)), null); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNull(event.getPortalUrl()); + } + + @Test + public void testFlowCallbackUrlExposed() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + execCtx.setCallbackUrl("https://example.com/callback"); + + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/flow/callbackUrl", false)), null); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertEquals(event.getCallbackUrl(), "https://example.com/callback"); + } + @Test public void testExposeFilteringSpecificClaim() throws ActionExecutionRequestBuilderException { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessorTest.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessorTest.java index 1a5119797da0..e73c058d2e4f 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessorTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.executor; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; import org.mockito.MockedStatic; import org.testng.annotations.AfterMethod; @@ -38,10 +38,6 @@ import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionRequestBuilder; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionResponseProcessor; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.JWEEncryptionUtil; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/PathTypeAnnotationUtilTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtilTest.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/PathTypeAnnotationUtilTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtilTest.java index e2f7be53fbdf..91a41108a2ae 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/executor/PathTypeAnnotationUtilTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtilTest.java @@ -16,10 +16,9 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.executor; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; import org.testng.annotations.Test; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.PathTypeAnnotationUtil; import java.util.Arrays; import java.util.Collections; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/AccessConfigTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfigTest.java similarity index 96% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/AccessConfigTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfigTest.java index 8d752e6d4c15..e60fbc2b3180 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/AccessConfigTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfigTest.java @@ -16,11 +16,9 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.model; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; import org.testng.annotations.Test; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; import java.util.Arrays; import java.util.Collections; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEventTest.java similarity index 97% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEventTest.java index 17ce19decfe5..6ade5c4354e5 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/InFlowExtensionEventTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEventTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.model; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; import org.testng.annotations.Test; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionEvent; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/OperationExecutionResultTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResultTest.java similarity index 96% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/OperationExecutionResultTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResultTest.java index ea5d47c7eb52..ba2ff451f798 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/model/OperationExecutionResultTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResultTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.model; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; import org.testng.annotations.Test; import org.wso2.carbon.identity.action.execution.api.model.Operation; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml index 028dc97b4e70..f583dba3beaf 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml @@ -30,20 +30,26 @@ + + + + + + - - - - - + + + + + - - - + + + diff --git a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 index 4510052e7929..feb5fe63014b 100644 --- a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 +++ b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 @@ -2527,6 +2527,110 @@ + + + + + {{identity.in_flow_extension.context.default.redirection.enable | default('true')}} + + + {{identity.in_flow_extension.context.default.user.enable | default('true')}} + {{identity.in_flow_extension.context.default.user.userId | default('false')}} + {{identity.in_flow_extension.context.default.user.userStoreDomain | default('true')}} + {{identity.in_flow_extension.context.default.user.claims | default('true')}} + {{identity.in_flow_extension.context.default.user.credentials | default('false')}} + + + {{identity.in_flow_extension.context.default.flow.enable | default('true')}} + {{identity.in_flow_extension.context.default.flow.tenantDomain | default('true')}} + {{identity.in_flow_extension.context.default.flow.applicationId | default('true')}} + {{identity.in_flow_extension.context.default.flow.flowType | default('true')}} + {{identity.in_flow_extension.context.default.flow.callbackUrl | default('true')}} + {{identity.in_flow_extension.context.default.flow.portalUrl | default('true')}} + + + {{identity.in_flow_extension.context.default.properties.enable | default('true')}} + + + + + {{identity.in_flow_extension.context.registration.redirection.enable | default(identity.in_flow_extension.context.default.redirection.enable | default('true'))}} + + + {{identity.in_flow_extension.context.registration.user.enable | default(identity.in_flow_extension.context.default.user.enable | default('true'))}} + {{identity.in_flow_extension.context.registration.user.userId | default(identity.in_flow_extension.context.default.user.userId | default('false'))}} + {{identity.in_flow_extension.context.registration.user.userStoreDomain | default(identity.in_flow_extension.context.default.user.userStoreDomain | default('true'))}} + {{identity.in_flow_extension.context.registration.user.claims | default(identity.in_flow_extension.context.default.user.claims | default('true'))}} + {{identity.in_flow_extension.context.registration.user.credentials | default(identity.in_flow_extension.context.default.user.credentials | default('false'))}} + + + {{identity.in_flow_extension.context.registration.flow.enable | default(identity.in_flow_extension.context.default.flow.enable | default('true'))}} + {{identity.in_flow_extension.context.registration.flow.tenantDomain | default(identity.in_flow_extension.context.default.flow.tenantDomain | default('true'))}} + {{identity.in_flow_extension.context.registration.flow.applicationId | default(identity.in_flow_extension.context.default.flow.applicationId | default('true'))}} + {{identity.in_flow_extension.context.registration.flow.flowType | default(identity.in_flow_extension.context.default.flow.flowType | default('true'))}} + {{identity.in_flow_extension.context.registration.flow.callbackUrl | default(identity.in_flow_extension.context.default.flow.callbackUrl | default('true'))}} + {{identity.in_flow_extension.context.registration.flow.portalUrl | default(identity.in_flow_extension.context.default.flow.portalUrl | default('true'))}} + + + {{identity.in_flow_extension.context.registration.properties.enable | default(identity.in_flow_extension.context.default.properties.enable | default('true'))}} + + + + + {{identity.in_flow_extension.context.password_recovery.redirection.enable | default(identity.in_flow_extension.context.default.redirection.enable | default('true'))}} + + + {{identity.in_flow_extension.context.password_recovery.user.enable | default(identity.in_flow_extension.context.default.user.enable | default('true'))}} + {{identity.in_flow_extension.context.password_recovery.user.userId | default(identity.in_flow_extension.context.default.user.userId | default('false'))}} + {{identity.in_flow_extension.context.password_recovery.user.userStoreDomain | default(identity.in_flow_extension.context.default.user.userStoreDomain | default('true'))}} + {{identity.in_flow_extension.context.password_recovery.user.claims | default(identity.in_flow_extension.context.default.user.claims | default('true'))}} + {{identity.in_flow_extension.context.password_recovery.user.credentials | default(identity.in_flow_extension.context.default.user.credentials | default('false'))}} + + + {{identity.in_flow_extension.context.password_recovery.flow.enable | default(identity.in_flow_extension.context.default.flow.enable | default('true'))}} + {{identity.in_flow_extension.context.password_recovery.flow.tenantDomain | default(identity.in_flow_extension.context.default.flow.tenantDomain | default('true'))}} + {{identity.in_flow_extension.context.password_recovery.flow.applicationId | default(identity.in_flow_extension.context.default.flow.applicationId | default('true'))}} + {{identity.in_flow_extension.context.password_recovery.flow.flowType | default(identity.in_flow_extension.context.default.flow.flowType | default('true'))}} + {{identity.in_flow_extension.context.password_recovery.flow.callbackUrl | default(identity.in_flow_extension.context.default.flow.callbackUrl | default('true'))}} + {{identity.in_flow_extension.context.password_recovery.flow.portalUrl | default(identity.in_flow_extension.context.default.flow.portalUrl | default('true'))}} + + + {{identity.in_flow_extension.context.password_recovery.properties.enable | default(identity.in_flow_extension.context.default.properties.enable | default('true'))}} + + + + + {{identity.in_flow_extension.context.invited_user_registration.redirection.enable | default(identity.in_flow_extension.context.default.redirection.enable | default('true'))}} + + + {{identity.in_flow_extension.context.invited_user_registration.user.enable | default(identity.in_flow_extension.context.default.user.enable | default('true'))}} + {{identity.in_flow_extension.context.invited_user_registration.user.userId | default(identity.in_flow_extension.context.default.user.userId | default('false'))}} + {{identity.in_flow_extension.context.invited_user_registration.user.userStoreDomain | default(identity.in_flow_extension.context.default.user.userStoreDomain | default('true'))}} + {{identity.in_flow_extension.context.invited_user_registration.user.claims | default(identity.in_flow_extension.context.default.user.claims | default('true'))}} + {{identity.in_flow_extension.context.invited_user_registration.user.credentials | default(identity.in_flow_extension.context.default.user.credentials | default('false'))}} + + + {{identity.in_flow_extension.context.invited_user_registration.flow.enable | default(identity.in_flow_extension.context.default.flow.enable | default('true'))}} + {{identity.in_flow_extension.context.invited_user_registration.flow.tenantDomain | default(identity.in_flow_extension.context.default.flow.tenantDomain | default('true'))}} + {{identity.in_flow_extension.context.invited_user_registration.flow.applicationId | default(identity.in_flow_extension.context.default.flow.applicationId | default('true'))}} + {{identity.in_flow_extension.context.invited_user_registration.flow.flowType | default(identity.in_flow_extension.context.default.flow.flowType | default('true'))}} + {{identity.in_flow_extension.context.invited_user_registration.flow.callbackUrl | default(identity.in_flow_extension.context.default.flow.callbackUrl | default('true'))}} + {{identity.in_flow_extension.context.invited_user_registration.flow.portalUrl | default(identity.in_flow_extension.context.default.flow.portalUrl | default('true'))}} + + + {{identity.in_flow_extension.context.invited_user_registration.properties.enable | default(identity.in_flow_extension.context.default.properties.enable | default('true'))}} + + + + {{external_api_client.http_client.connection_timeout}} From c4debd23d4e30448473a4e2470b1318afdfd9633 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Thu, 30 Apr 2026 15:25:40 +0530 Subject: [PATCH 18/41] Adding handover policy and metadata service. and remove i18n key prefixing with the screen key and sanitized action name from the response processor. now extension needs to send the full key. --- .../config/FlowContextHandoverConfig.java | 128 ++++++++ .../config/FlowContextHandoverPolicy.java | 287 ++++++++++++++++++ .../config/FlowExecutionContextFilter.java | 125 ++++++++ .../InFlowExtensionRequestBuilder.java | 6 - .../InFlowExtensionResponseProcessor.java | 63 ---- .../InFlowExtensionContextTreeBuilder.java | 249 +++++++++++++++ .../InFlowExtensionContextTreeMetadata.java | 70 +++++ .../InFlowExtensionContextTreeNode.java | 205 +++++++++++++ .../InFlowExtensionContextTreeService.java | 78 +++++ .../config/FlowContextHandoverPolicyTest.java | 106 +++++++ .../FlowExecutionContextFilterTest.java | 255 ++++++++++++++++ .../executor/InFlowExtensionExecutorTest.java | 35 --- .../InFlowExtensionResponseProcessorTest.java | 70 +---- 13 files changed, 1512 insertions(+), 165 deletions(-) create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverConfig.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverPolicy.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowExecutionContextFilter.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeBuilder.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeMetadata.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeNode.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeService.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverPolicyTest.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowExecutionContextFilterTest.java diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverConfig.java new file mode 100644 index 000000000000..9af590a470ca --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverConfig.java @@ -0,0 +1,128 @@ +/* + * 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.inflow.extension.config; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.core.util.IdentityUtil; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Loads and exposes the {@link FlowContextHandoverPolicy} for each known flow type from + * {@code identity.xml} (which is itself templated from {@code deployment.toml}). The j2 + * template performs deep-merge inheritance from the {@code default} section using chained + * {@code default(...)} Jinja filters, so by the time this loader runs every leaf is fully + * resolved and the loader just reads the literal value. + * + *

      Per-flow-type policies are keyed by {@code FlowTypes.getType()} (e.g., + * {@code "REGISTRATION"}). Unknown flow types fall through to {@link #getDefaultPolicy()}.

      + */ +public class FlowContextHandoverConfig { + + private static final Log LOG = LogFactory.getLog(FlowContextHandoverConfig.class); + + // Section names under in identity.xml. + private static final String ROOT = "InFlowExtensionContextHandover"; + private static final String SECTION_DEFAULT = "Default"; + private static final String SECTION_REGISTRATION = "Registration"; + private static final String SECTION_PASSWORD_RECOVERY = "PasswordRecovery"; + private static final String SECTION_INVITED_USER_REGISTRATION = "InvitedUserRegistration"; + + // Mapping XML section name → FlowTypes enum value (the keys we'll look up at runtime). + private static final Map XML_SECTION_TO_FLOW_TYPE; + + static { + Map map = new HashMap<>(); + map.put(SECTION_REGISTRATION, "REGISTRATION"); + map.put(SECTION_PASSWORD_RECOVERY, "PASSWORD_RECOVERY"); + map.put(SECTION_INVITED_USER_REGISTRATION, "INVITED_USER_REGISTRATION"); + XML_SECTION_TO_FLOW_TYPE = Collections.unmodifiableMap(map); + } + + private final FlowContextHandoverPolicy defaultPolicy; + private final Map perFlowTypePolicies; + + /** + * Eager-load all policies from {@code IdentityUtil}. Call once at component startup. + */ + public FlowContextHandoverConfig() { + + this.defaultPolicy = readPolicy(SECTION_DEFAULT); + Map perType = new HashMap<>(); + for (Map.Entry entry : XML_SECTION_TO_FLOW_TYPE.entrySet()) { + perType.put(entry.getValue(), readPolicy(entry.getKey())); + } + this.perFlowTypePolicies = Collections.unmodifiableMap(perType); + + if (LOG.isDebugEnabled()) { + LOG.debug("Loaded In-Flow Extension context handover policies: default + " + + perFlowTypePolicies.keySet()); + } + } + + /** + * Resolve the policy for the given flow type. Falls back to the default policy if the + * flow type is unknown or null. + */ + public FlowContextHandoverPolicy resolve(String flowType) { + + if (flowType == null) { + return defaultPolicy; + } + FlowContextHandoverPolicy policy = perFlowTypePolicies.get(flowType); + return policy != null ? policy : defaultPolicy; + } + + public FlowContextHandoverPolicy getDefaultPolicy() { + + return defaultPolicy; + } + + private FlowContextHandoverPolicy readPolicy(String section) { + + String prefix = ROOT + "." + section + "."; + return FlowContextHandoverPolicy.builder() + .redirectionEnabled(readBool(prefix + "Redirection.Enable", true)) + .userEnabled(readBool(prefix + "User.Enable", true)) + .userUserId(readBool(prefix + "User.UserId", false)) + .userUserStoreDomain(readBool(prefix + "User.UserStoreDomain", true)) + .userClaims(readBool(prefix + "User.Claims", true)) + .userCredentials(readBool(prefix + "User.Credentials", false)) + .flowEnabled(readBool(prefix + "Flow.Enable", true)) + .flowTenantDomain(readBool(prefix + "Flow.TenantDomain", true)) + .flowApplicationId(readBool(prefix + "Flow.ApplicationId", true)) + .flowFlowType(readBool(prefix + "Flow.FlowType", true)) + .flowCallbackUrl(readBool(prefix + "Flow.CallbackUrl", true)) + .flowPortalUrl(readBool(prefix + "Flow.PortalUrl", true)) + .propertiesEnabled(readBool(prefix + "Properties.Enable", true)) + .build(); + } + + private static boolean readBool(String key, boolean fallback) { + + String value = IdentityUtil.getProperty(key); + if (value == null || value.isEmpty()) { + return fallback; + } + return Boolean.parseBoolean(value.trim()); + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverPolicy.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverPolicy.java new file mode 100644 index 000000000000..e97cfe448942 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverPolicy.java @@ -0,0 +1,287 @@ +/* + * 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.inflow.extension.config; + +/** + * Immutable per-flow-type policy controlling which {@code FlowExecutionContext} fields the + * In-Flow Extension executor hands over to the action framework, plus whether REDIRECT is + * advertised in {@code allowedOperations}. + * + *

      Sourced from {@code [identity.in_flow_extension.context.{flowType}]} in + * {@code deployment.toml}. A flow-type-specific policy {@link Builder#startingFrom inherits} + * from the {@code default} policy for unspecified keys.

      + */ +public final class FlowContextHandoverPolicy { + + private final boolean redirectionEnabled; + + private final boolean userEnabled; + private final boolean userUserId; + private final boolean userUserStoreDomain; + private final boolean userClaims; + private final boolean userCredentials; + + private final boolean flowEnabled; + private final boolean flowTenantDomain; + private final boolean flowApplicationId; + private final boolean flowFlowType; + private final boolean flowCallbackUrl; + private final boolean flowPortalUrl; + + private final boolean propertiesEnabled; + + private FlowContextHandoverPolicy(Builder b) { + + this.redirectionEnabled = b.redirectionEnabled; + this.userEnabled = b.userEnabled; + this.userUserId = b.userUserId; + this.userUserStoreDomain = b.userUserStoreDomain; + this.userClaims = b.userClaims; + this.userCredentials = b.userCredentials; + this.flowEnabled = b.flowEnabled; + this.flowTenantDomain = b.flowTenantDomain; + this.flowApplicationId = b.flowApplicationId; + this.flowFlowType = b.flowFlowType; + this.flowCallbackUrl = b.flowCallbackUrl; + this.flowPortalUrl = b.flowPortalUrl; + this.propertiesEnabled = b.propertiesEnabled; + } + + public boolean isRedirectionEnabled() { + + return redirectionEnabled; + } + + public boolean isUserEnabled() { + + return userEnabled; + } + + public boolean isUserUserId() { + + return userUserId; + } + + public boolean isUserUserStoreDomain() { + + return userUserStoreDomain; + } + + public boolean isUserClaims() { + + return userClaims; + } + + public boolean isUserCredentials() { + + return userCredentials; + } + + public boolean isFlowEnabled() { + + return flowEnabled; + } + + public boolean isFlowTenantDomain() { + + return flowTenantDomain; + } + + public boolean isFlowApplicationId() { + + return flowApplicationId; + } + + public boolean isFlowFlowType() { + + return flowFlowType; + } + + public boolean isFlowCallbackUrl() { + + return flowCallbackUrl; + } + + public boolean isFlowPortalUrl() { + + return flowPortalUrl; + } + + public boolean isPropertiesEnabled() { + + return propertiesEnabled; + } + + /** + * Permissive default — every field allowed. Used as the seed for building the + * deployment-configured "default" policy and as the safety net when no config is loaded. + */ + public static FlowContextHandoverPolicy permissive() { + + return new Builder().allowAll().build(); + } + + public static Builder builder() { + + return new Builder(); + } + + /** + * Builder for {@link FlowContextHandoverPolicy}. + * + *

      Defaults: every field disabled. Call {@link #allowAll()} to start from the permissive + * baseline, or {@link #startingFrom(FlowContextHandoverPolicy)} to inherit from another + * policy (used for per-flow-type overrides that fill unspecified keys from {@code default}).

      + */ + public static final class Builder { + + private boolean redirectionEnabled; + private boolean userEnabled; + private boolean userUserId; + private boolean userUserStoreDomain; + private boolean userClaims; + private boolean userCredentials; + private boolean flowEnabled; + private boolean flowTenantDomain; + private boolean flowApplicationId; + private boolean flowFlowType; + private boolean flowCallbackUrl; + private boolean flowPortalUrl; + private boolean propertiesEnabled; + + public Builder allowAll() { + + this.redirectionEnabled = true; + this.userEnabled = true; + this.userUserId = true; + this.userUserStoreDomain = true; + this.userClaims = true; + this.userCredentials = true; + this.flowEnabled = true; + this.flowTenantDomain = true; + this.flowApplicationId = true; + this.flowFlowType = true; + this.flowCallbackUrl = true; + this.flowPortalUrl = true; + this.propertiesEnabled = true; + return this; + } + + public Builder startingFrom(FlowContextHandoverPolicy base) { + + this.redirectionEnabled = base.redirectionEnabled; + this.userEnabled = base.userEnabled; + this.userUserId = base.userUserId; + this.userUserStoreDomain = base.userUserStoreDomain; + this.userClaims = base.userClaims; + this.userCredentials = base.userCredentials; + this.flowEnabled = base.flowEnabled; + this.flowTenantDomain = base.flowTenantDomain; + this.flowApplicationId = base.flowApplicationId; + this.flowFlowType = base.flowFlowType; + this.flowCallbackUrl = base.flowCallbackUrl; + this.flowPortalUrl = base.flowPortalUrl; + this.propertiesEnabled = base.propertiesEnabled; + return this; + } + + public Builder redirectionEnabled(boolean v) { + + this.redirectionEnabled = v; + return this; + } + + public Builder userEnabled(boolean v) { + + this.userEnabled = v; + return this; + } + + public Builder userUserId(boolean v) { + + this.userUserId = v; + return this; + } + + public Builder userUserStoreDomain(boolean v) { + + this.userUserStoreDomain = v; + return this; + } + + public Builder userClaims(boolean v) { + + this.userClaims = v; + return this; + } + + public Builder userCredentials(boolean v) { + + this.userCredentials = v; + return this; + } + + public Builder flowEnabled(boolean v) { + + this.flowEnabled = v; + return this; + } + + public Builder flowTenantDomain(boolean v) { + + this.flowTenantDomain = v; + return this; + } + + public Builder flowApplicationId(boolean v) { + + this.flowApplicationId = v; + return this; + } + + public Builder flowFlowType(boolean v) { + + this.flowFlowType = v; + return this; + } + + public Builder flowCallbackUrl(boolean v) { + + this.flowCallbackUrl = v; + return this; + } + + public Builder flowPortalUrl(boolean v) { + + this.flowPortalUrl = v; + return this; + } + + public Builder propertiesEnabled(boolean v) { + + this.propertiesEnabled = v; + return this; + } + + public FlowContextHandoverPolicy build() { + + return new FlowContextHandoverPolicy(this); + } + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowExecutionContextFilter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowExecutionContextFilter.java new file mode 100644 index 000000000000..80978d0ec625 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowExecutionContextFilter.java @@ -0,0 +1,125 @@ +/* + * 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.inflow.extension.config; + +import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; +import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; + +import java.util.HashMap; +import java.util.Map; + +/** + * Builds a filtered defensive copy of a {@link FlowExecutionContext} containing only the + * fields permitted by the supplied {@link FlowContextHandoverPolicy}. Used by + * {@code InFlowExtensionExecutor} to bound the surface area visible to the action framework. + * + *

      Non-permitted fields are left null/empty on the copy. The original context is never + * mutated. Only public getters/setters are used so the existing request-builder and + * response-processor code that reads via {@code execCtx.getFoo()} continues to work — it + * just gets {@code null} (or empty maps) for fields the deployment.toml whitelist omits.

      + */ +public final class FlowExecutionContextFilter { + + private FlowExecutionContextFilter() { + + } + + /** + * Build a filtered copy of {@code original} according to {@code policy}. + * + * @param original the original context (untouched). + * @param policy the per-flow handover policy. + * @return a new {@link FlowExecutionContext} carrying only whitelisted fields. + */ + public static FlowExecutionContext filter(FlowExecutionContext original, + FlowContextHandoverPolicy policy) { + + if (original == null) { + return null; + } + if (policy == null) { + // Defensive: no policy → permissive (preserve current behaviour). + policy = FlowContextHandoverPolicy.permissive(); + } + + FlowExecutionContext copy = new FlowExecutionContext(); + + // contextIdentifier is engine-internal and always copied — not a user-data field. + copy.setContextIdentifier(original.getContextIdentifier()); + + // ---- Flow block ---- + if (policy.isFlowEnabled()) { + if (policy.isFlowTenantDomain()) { + copy.setTenantDomain(original.getTenantDomain()); + } + if (policy.isFlowApplicationId()) { + copy.setApplicationId(original.getApplicationId()); + } + if (policy.isFlowFlowType()) { + copy.setFlowType(original.getFlowType()); + } + if (policy.isFlowCallbackUrl()) { + copy.setCallbackUrl(original.getCallbackUrl()); + } + if (policy.isFlowPortalUrl()) { + copy.setPortalUrl(original.getPortalUrl()); + } + } + + // ---- User block ---- + // Always set a non-null FlowUser on the copy. Pending claims/credentials/properties + // collected by the response processor are stashed in FlowContext keys (not on FlowUser) + // and applied to the *original* context by TaskExecutionNode after the executor returns, + // so this empty FlowUser never leaks back into engine state. The empty instance exists + // only as a defensive convenience: it lets the request builder call getClaims()/ + // getUserCredentials() without null-guarding the FlowUser itself. + FlowUser filteredUser = new FlowUser(); + FlowUser originalUser = original.getFlowUser(); + if (policy.isUserEnabled() && originalUser != null) { + if (policy.isUserUserId()) { + filteredUser.setUserId(originalUser.getUserId()); + } + if (policy.isUserUserStoreDomain()) { + filteredUser.setUserStoreDomain(originalUser.getUserStoreDomain()); + } + if (policy.isUserClaims() && originalUser.getClaims() != null) { + // Defensive copy of the claims map. + filteredUser.addClaims(new HashMap<>(originalUser.getClaims())); + } + if (policy.isUserCredentials() && originalUser.getUserCredentials() != null) { + // Defensive copy of credentials — clone each char[] so the request builder's + // post-extraction wipe (Arrays.fill(...,'\0')) zeroes the *copy*, not the original. + Map credentialsCopy = new HashMap<>(); + for (Map.Entry entry : originalUser.getUserCredentials().entrySet()) { + char[] value = entry.getValue(); + credentialsCopy.put(entry.getKey(), value == null ? null : value.clone()); + } + filteredUser.setUserCredentials(credentialsCopy); + } + } + copy.setFlowUser(filteredUser); + + // ---- Properties block ---- + if (policy.isPropertiesEnabled() && original.getProperties() != null) { + copy.setProperties(new HashMap<>(original.getProperties())); + } + + return copy; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java index f8bfe7cebb91..55a0da080e80 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java @@ -67,7 +67,6 @@ public class InFlowExtensionRequestBuilder implements ActionExecutionRequestBuil private static final Log LOG = LogFactory.getLog(InFlowExtensionRequestBuilder.class); public static final String MODIFY_PATHS_KEY = "modifyPaths"; - public static final String ACTION_NAME_KEY = "actionName"; @Override public ActionType getSupportedActionType() { @@ -129,11 +128,6 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex .resultStatus(DiagnosticLog.ResultStatus.SUCCESS)); } - // Store action name for i18n error key prefixing in the response processor. - if (actionName != null) { - flowContext.add(ACTION_NAME_KEY, actionName); - } - // Build allowed operations: REPLACE (from modify paths, if any) + REDIRECT (always). List allowedOperations = buildAllowedOperations(accessConfig, flowContext); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java index c0259686f768..2e74fc9a6577 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java @@ -54,9 +54,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.regex.Pattern; /** * This class is responsible for processing the response from In-Flow Extension actions. @@ -81,9 +79,6 @@ public class InFlowExtensionResponseProcessor implements ActionExecutionResponse private static final String PROPERTIES_PATH_PREFIX = "/properties/"; private static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; private static final String USER_CREDENTIALS_PATH_PREFIX = "/user/credentials/"; - private static final String I18N_KEY_PREFIX = "inflow.extension."; - private static final Pattern SHORT_I18N_KEY_PATTERN = - Pattern.compile("^[a-z][a-z0-9]*(?:\\.[a-z0-9]+)+$"); @Override public ActionType getSupportedActionType() { @@ -375,11 +370,6 @@ public ActionExecutionStatus processErrorResponse(FlowContext flowContext ", Description: " + errorDescription); } - // Apply i18n key prefixing so the frontend can locate extension-specific error messages. - String actionName = flowContext.getValue(InFlowExtensionRequestBuilder.ACTION_NAME_KEY, String.class); - errorMessage = applyI18nKeyPrefix(errorMessage, actionName); - errorDescription = applyI18nKeyPrefix(errorDescription, actionName); - return new ErrorStatus(new Error(errorMessage, errorDescription)); } @@ -396,62 +386,9 @@ public ActionExecutionStatus processFailureResponse(FlowContext flowCon ", Description: " + failureDescription); } - // Apply i18n key prefixing so the frontend can locate extension-specific error messages. - String actionName = flowContext.getValue(InFlowExtensionRequestBuilder.ACTION_NAME_KEY, String.class); - failureDescription = applyI18nKeyPrefix(failureDescription, actionName); - failureReason = applyI18nKeyPrefix(failureReason, actionName); - return new FailedStatus(new Failure(failureReason, failureDescription)); } - /** - * Transform a bare dot-separated short i18n key (e.g. {@code account.restricted.message}) - * into a fully qualified, frontend-ready wrapped key - * (e.g. {@code {{inflow.extension.risk.assessment.extension.account.restricted.message}}}), - * using the sanitized action name as the prefix. - * - *

      Plain text (sentences, single words, anything with spaces or special characters) is - * returned as-is. Keys that already start with {@value #I18N_KEY_PREFIX} are also left - * unchanged so the processor is idempotent.

      - * - * @param message The error message or reason string. May be null. - * @param actionName The action display name. May be null. - * @return The transformed message, or the original if no transformation applies. - */ - private String applyI18nKeyPrefix(String message, String actionName) { - - if (message == null || actionName == null) { - return message; - } - if (message.startsWith(I18N_KEY_PREFIX)) { - return message; - } - if (!SHORT_I18N_KEY_PATTERN.matcher(message).matches()) { - return message; - } - String sanitizedName = sanitizeConnectionName(actionName); - return "{{" + I18N_KEY_PREFIX + sanitizedName + "." + message + "}}"; - } - - /** - * Sanitize a connection name into a dot-separated lowercase key segment. - * Non-alphanumeric characters are replaced with dots, consecutive dots are collapsed, - * and leading/trailing dots are trimmed. - * - * @param name The connection display name. - * @return The sanitized key segment (e.g., "Risk Assessment Extension" → "risk.assessment.extension"). - */ - public static String sanitizeConnectionName(String name) { - - if (name == null || name.isEmpty()) { - return ""; - } - String sanitized = name.toLowerCase(Locale.ENGLISH).replaceAll("[^a-z0-9]", "."); - sanitized = sanitized.replaceAll("\\.{2,}", "."); - sanitized = sanitized.replaceAll("^\\.+|\\.+$", ""); - return sanitized; - } - /** * Log operation execution results for diagnostics and debugging. */ diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeBuilder.java new file mode 100644 index 000000000000..f535edc836e1 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeBuilder.java @@ -0,0 +1,249 @@ +/* + * 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.inflow.extension.metadata; + +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverPolicy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Builds the controlled In-Flow Extension context tree returned by the metadata endpoint. + * The canonical tree shape lives in code here (rather than a static resource) so it can + * evolve alongside the {@link FlowContextHandoverPolicy} without an extra file to keep in + * sync. Nodes are pruned when the policy disables them, so the Console UI never offers the + * admin a checkbox for a field the deployment has switched off at the server level. + */ +public class InFlowExtensionContextTreeBuilder { + + // Flow type identifiers — must match the values produced by FlowTypes.getType(). + private static final String FLOW_REGISTRATION = "REGISTRATION"; + private static final String FLOW_INVITED_USER_REGISTRATION = "INVITED_USER_REGISTRATION"; + private static final String FLOW_PASSWORD_RECOVERY = "PASSWORD_RECOVERY"; + + // Node-type sentinels matching the tree component's NodeType enum on the Console side. + private static final String NODE_OBJECT = "OBJECT"; + private static final String NODE_LEAF = "LEAF"; + private static final String NODE_MAP = "MAP"; + private static final String NODE_COMPLEX_MAP = "COMPLEX_MAP"; + + private static final String OP_EXPOSE = "EXPOSE"; + private static final String OP_MODIFY = "MODIFY"; + + private final FlowContextHandoverConfig handoverConfig; + + public InFlowExtensionContextTreeBuilder(FlowContextHandoverConfig handoverConfig) { + + this.handoverConfig = handoverConfig; + } + + /** + * Build the metadata response for the given flow type. + * + * @param flowType the flow type (null → default tree). + * @return a fully populated metadata DTO. + */ + public InFlowExtensionContextTreeMetadata build(String flowType) { + + FlowContextHandoverPolicy policy = handoverConfig.resolve(flowType); + + List tree = new ArrayList<>(); + InFlowExtensionContextTreeNode userNode = buildUserNode(policy); + if (userNode != null) { + tree.add(userNode); + } + InFlowExtensionContextTreeNode flowNode = buildFlowNode(policy); + if (flowNode != null) { + tree.add(flowNode); + } + InFlowExtensionContextTreeNode propsNode = buildPropertiesNode(policy); + if (propsNode != null) { + tree.add(propsNode); + } + + return new InFlowExtensionContextTreeMetadata( + flowType, + tree, + policy.isRedirectionEnabled(), + resolveAllowReadOnlyClaimsModification(flowType)); + } + + /** + * Whether the Console UI may permit MODIFY on read-only claims for this flow type. + * Hardcoded enumerative mapping (rather than {@code != PASSWORD_RECOVERY}) so that any + * future flow type defaults to the safe value (false) until explicitly added here. + * + *

      The default tree (null flowType) inherits the registration-style permissive default + * — that matches today's behaviour for the connection-level access-config editor, which + * doesn't yet know which flow the action will be wired into.

      + */ + static boolean resolveAllowReadOnlyClaimsModification(String flowType) { + + if (flowType == null) { + return true; + } + switch (flowType) { + case FLOW_REGISTRATION: + case FLOW_INVITED_USER_REGISTRATION: + return true; + case FLOW_PASSWORD_RECOVERY: + return false; + default: + return false; + } + } + + private InFlowExtensionContextTreeNode buildUserNode(FlowContextHandoverPolicy policy) { + + if (!policy.isUserEnabled()) { + return null; + } + List children = new ArrayList<>(); + if (policy.isUserUserId()) { + children.add(InFlowExtensionContextTreeNode.builder() + .key("userId") + .title("User ID") + .path("/user/userId") + .dataType("String") + .nodeType(NODE_LEAF) + .allowedOperations(Collections.singletonList(OP_EXPOSE)) + .replaceable(false) + .build()); + } + if (policy.isUserUserStoreDomain()) { + children.add(InFlowExtensionContextTreeNode.builder() + .key("userStoreDomain") + .title("User Store Domain") + .path("/user/userStoreDomain") + .dataType("String") + .nodeType(NODE_LEAF) + .allowedOperations(Collections.singletonList(OP_EXPOSE)) + .replaceable(false) + .build()); + } + if (policy.isUserClaims()) { + children.add(InFlowExtensionContextTreeNode.builder() + .key("claims") + .title("Claims") + .path("/user/claims/") + .dataType("Map") + .nodeType(NODE_MAP) + .allowedOperations(Arrays.asList(OP_EXPOSE, OP_MODIFY)) + .dynamicEntryAllowed(true) + .dynamicEntryType("String") + .children(Collections.emptyList()) + .build()); + } + if (policy.isUserCredentials()) { + children.add(InFlowExtensionContextTreeNode.builder() + .key("credentials") + .title("Credentials") + .path("/user/credentials/") + .dataType("Map") + .nodeType(NODE_MAP) + .allowedOperations(Arrays.asList(OP_EXPOSE, OP_MODIFY)) + .dynamicEntryAllowed(true) + .dynamicEntryType("char[]") + .children(Collections.emptyList()) + .build()); + } + if (children.isEmpty()) { + return null; + } + return InFlowExtensionContextTreeNode.builder() + .key("user") + .title("User") + .path("/user/") + .dataType("") + .nodeType(NODE_OBJECT) + .allowedOperations(Collections.singletonList(OP_EXPOSE)) + .children(children) + .build(); + } + + private InFlowExtensionContextTreeNode buildFlowNode(FlowContextHandoverPolicy policy) { + + if (!policy.isFlowEnabled()) { + return null; + } + List children = new ArrayList<>(); + if (policy.isFlowTenantDomain()) { + children.add(flowLeaf("tenantDomain", "Tenant Domain", "/flow/tenantDomain")); + } + if (policy.isFlowApplicationId()) { + children.add(flowLeaf("applicationId", "Application ID", "/flow/applicationId")); + } + if (policy.isFlowFlowType()) { + children.add(flowLeaf("flowType", "Flow Type", "/flow/flowType")); + } + if (policy.isFlowCallbackUrl()) { + children.add(flowLeaf("callbackUrl", "Callback URL", "/flow/callbackUrl")); + } + if (policy.isFlowPortalUrl()) { + children.add(flowLeaf("portalUrl", "Portal URL", "/flow/portalUrl")); + } + if (children.isEmpty()) { + return null; + } + return InFlowExtensionContextTreeNode.builder() + .key("flow") + .title("Flow") + .path("/flow/") + .dataType("") + .nodeType(NODE_OBJECT) + .allowedOperations(Collections.singletonList(OP_EXPOSE)) + .readOnly(true) + .children(children) + .build(); + } + + private InFlowExtensionContextTreeNode flowLeaf(String key, String title, String path) { + + return InFlowExtensionContextTreeNode.builder() + .key(key) + .title(title) + .path(path) + .dataType("String") + .nodeType(NODE_LEAF) + .allowedOperations(Collections.singletonList(OP_EXPOSE)) + .readOnly(true) + .build(); + } + + private InFlowExtensionContextTreeNode buildPropertiesNode(FlowContextHandoverPolicy policy) { + + if (!policy.isPropertiesEnabled()) { + return null; + } + return InFlowExtensionContextTreeNode.builder() + .key("properties") + .title("Properties") + .path("/properties/") + .dataType("Map") + .nodeType(NODE_COMPLEX_MAP) + .allowedOperations(Arrays.asList(OP_EXPOSE, OP_MODIFY)) + .dynamicEntryAllowed(true) + .dynamicEntryType("Object") + .children(Collections.emptyList()) + .build(); + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeMetadata.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeMetadata.java new file mode 100644 index 000000000000..2b97377972ea --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeMetadata.java @@ -0,0 +1,70 @@ +/* + * 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.inflow.extension.metadata; + +import java.util.Collections; +import java.util.List; + +/** + * Metadata response served by the {@code GET /flow/in-flow-extension/context-tree} endpoint. + * Carries the controlled context tree (filtered by {@code FlowContextHandoverPolicy}) plus + * per-flow-type policy flags that the Console UI uses to gate the access-config editor. + */ +public class InFlowExtensionContextTreeMetadata { + + private final String flowType; + private final List contextTree; + private final boolean redirectionEnabled; + private final boolean allowReadOnlyClaimsModification; + + public InFlowExtensionContextTreeMetadata(String flowType, + List contextTree, + boolean redirectionEnabled, + boolean allowReadOnlyClaimsModification) { + + this.flowType = flowType; + this.contextTree = contextTree != null ? Collections.unmodifiableList(contextTree) + : Collections.emptyList(); + this.redirectionEnabled = redirectionEnabled; + this.allowReadOnlyClaimsModification = allowReadOnlyClaimsModification; + } + + /** + * @return The flow type this metadata applies to. {@code null} indicates the default tree. + */ + public String getFlowType() { + + return flowType; + } + + public List getContextTree() { + + return contextTree; + } + + public boolean isRedirectionEnabled() { + + return redirectionEnabled; + } + + public boolean isAllowReadOnlyClaimsModification() { + + return allowReadOnlyClaimsModification; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeNode.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeNode.java new file mode 100644 index 000000000000..ee20519ae5d9 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeNode.java @@ -0,0 +1,205 @@ +/* + * 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.inflow.extension.metadata; + +import java.util.Collections; +import java.util.List; + +/** + * Single node in the In-Flow Extension context tree returned by the metadata service. + * Mirrors the shape of {@code default-flow-context-tree.json} on the Console side so the + * existing tree component can render this without translation. + */ +public class InFlowExtensionContextTreeNode { + + private final String key; + private final String title; + private final String path; + private final String dataType; + private final String nodeType; + private final List allowedOperations; + private final boolean readOnly; + private final boolean replaceable; + private final boolean dynamicEntryAllowed; + private final String dynamicEntryType; + private final List children; + + private InFlowExtensionContextTreeNode(Builder b) { + + this.key = b.key; + this.title = b.title; + this.path = b.path; + this.dataType = b.dataType; + this.nodeType = b.nodeType; + this.allowedOperations = b.allowedOperations != null + ? Collections.unmodifiableList(b.allowedOperations) : Collections.emptyList(); + this.readOnly = b.readOnly; + this.replaceable = b.replaceable; + this.dynamicEntryAllowed = b.dynamicEntryAllowed; + this.dynamicEntryType = b.dynamicEntryType; + this.children = b.children != null + ? Collections.unmodifiableList(b.children) : Collections.emptyList(); + } + + public String getKey() { + + return key; + } + + public String getTitle() { + + return title; + } + + public String getPath() { + + return path; + } + + public String getDataType() { + + return dataType; + } + + public String getNodeType() { + + return nodeType; + } + + public List getAllowedOperations() { + + return allowedOperations; + } + + public boolean isReadOnly() { + + return readOnly; + } + + public boolean isReplaceable() { + + return replaceable; + } + + public boolean isDynamicEntryAllowed() { + + return dynamicEntryAllowed; + } + + public String getDynamicEntryType() { + + return dynamicEntryType; + } + + public List getChildren() { + + return children; + } + + public static Builder builder() { + + return new Builder(); + } + + public static final class Builder { + + private String key; + private String title; + private String path; + private String dataType = ""; + private String nodeType; + private List allowedOperations; + private boolean readOnly; + private boolean replaceable; + private boolean dynamicEntryAllowed; + private String dynamicEntryType; + private List children; + + public Builder key(String v) { + + this.key = v; + return this; + } + + public Builder title(String v) { + + this.title = v; + return this; + } + + public Builder path(String v) { + + this.path = v; + return this; + } + + public Builder dataType(String v) { + + this.dataType = v; + return this; + } + + public Builder nodeType(String v) { + + this.nodeType = v; + return this; + } + + public Builder allowedOperations(List v) { + + this.allowedOperations = v; + return this; + } + + public Builder readOnly(boolean v) { + + this.readOnly = v; + return this; + } + + public Builder replaceable(boolean v) { + + this.replaceable = v; + return this; + } + + public Builder dynamicEntryAllowed(boolean v) { + + this.dynamicEntryAllowed = v; + return this; + } + + public Builder dynamicEntryType(String v) { + + this.dynamicEntryType = v; + return this; + } + + public Builder children(List v) { + + this.children = v; + return this; + } + + public InFlowExtensionContextTreeNode build() { + + return new InFlowExtensionContextTreeNode(this); + } + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeService.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeService.java new file mode 100644 index 000000000000..ea847312806f --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeService.java @@ -0,0 +1,78 @@ +/* + * 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.inflow.extension.metadata; + +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverConfig; + +/** + * Public-API entry point for retrieving the controlled In-Flow Extension context tree. + * Lives in the {@code metadata} package (which is exported by the engine OSGi bundle), so + * external bundles such as the flow-management API server can call it without depending on + * the engine's {@code internal} package. + * + *

      Singleton with lazy-init {@link FlowContextHandoverConfig} — the config reads from + * {@code IdentityUtil} which requires the carbon configuration to be loaded, so we defer + * construction until first use to avoid OSGi activation-order issues.

      + */ +public final class InFlowExtensionContextTreeService { + + private static final InFlowExtensionContextTreeService INSTANCE = new InFlowExtensionContextTreeService(); + + private volatile FlowContextHandoverConfig handoverConfig; + + private InFlowExtensionContextTreeService() { + + } + + public static InFlowExtensionContextTreeService getInstance() { + + return INSTANCE; + } + + /** + * Build the controlled context tree for the given flow type. + * + * @param flowType the flow type, or null for the default tree. + * @return the metadata DTO carrying the pruned tree + per-flow-type policy flags. + */ + public InFlowExtensionContextTreeMetadata buildContextTree(String flowType) { + + return new InFlowExtensionContextTreeBuilder(getHandoverConfig()).build(flowType); + } + + /** + * Override the handover config. Intended for tests only. + */ + public void setHandoverConfig(FlowContextHandoverConfig handoverConfig) { + + this.handoverConfig = handoverConfig; + } + + private FlowContextHandoverConfig getHandoverConfig() { + + if (handoverConfig == null) { + synchronized (this) { + if (handoverConfig == null) { + handoverConfig = new FlowContextHandoverConfig(); + } + } + } + return handoverConfig; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverPolicyTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverPolicyTest.java new file mode 100644 index 000000000000..24049b88b55a --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverPolicyTest.java @@ -0,0 +1,106 @@ +/* + * 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.inflow.extension.config; + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class FlowContextHandoverPolicyTest { + + @Test + public void testDefaultBuilderHasEverythingDisabled() { + + FlowContextHandoverPolicy p = FlowContextHandoverPolicy.builder().build(); + + assertFalse(p.isRedirectionEnabled()); + assertFalse(p.isUserEnabled()); + assertFalse(p.isUserUserId()); + assertFalse(p.isUserUserStoreDomain()); + assertFalse(p.isUserClaims()); + assertFalse(p.isUserCredentials()); + assertFalse(p.isFlowEnabled()); + assertFalse(p.isFlowTenantDomain()); + assertFalse(p.isFlowApplicationId()); + assertFalse(p.isFlowFlowType()); + assertFalse(p.isFlowCallbackUrl()); + assertFalse(p.isFlowPortalUrl()); + assertFalse(p.isPropertiesEnabled()); + } + + @Test + public void testPermissiveAllowsEverything() { + + FlowContextHandoverPolicy p = FlowContextHandoverPolicy.permissive(); + + assertTrue(p.isRedirectionEnabled()); + assertTrue(p.isUserEnabled()); + assertTrue(p.isUserUserId()); + assertTrue(p.isUserUserStoreDomain()); + assertTrue(p.isUserClaims()); + assertTrue(p.isUserCredentials()); + assertTrue(p.isFlowEnabled()); + assertTrue(p.isFlowTenantDomain()); + assertTrue(p.isFlowApplicationId()); + assertTrue(p.isFlowFlowType()); + assertTrue(p.isFlowCallbackUrl()); + assertTrue(p.isFlowPortalUrl()); + assertTrue(p.isPropertiesEnabled()); + } + + @Test + public void testStartingFromInheritsThenOverrides() { + + // Mimic deployment.toml: per-flow-type override that says "redirection.enable = false" + // but inherits everything else from the permissive default. + FlowContextHandoverPolicy base = FlowContextHandoverPolicy.permissive(); + FlowContextHandoverPolicy override = FlowContextHandoverPolicy.builder() + .startingFrom(base) + .redirectionEnabled(false) + .userClaims(false) + .build(); + + // Overridden: + assertFalse(override.isRedirectionEnabled()); + assertFalse(override.isUserClaims()); + + // Inherited: + assertTrue(override.isUserEnabled()); + assertTrue(override.isUserUserStoreDomain()); + assertTrue(override.isFlowFlowType()); + assertTrue(override.isFlowPortalUrl()); + assertTrue(override.isPropertiesEnabled()); + } + + @Test + public void testBuilderIndependentOfBaseAfterStartingFrom() { + + // Calling startingFrom must copy values, not retain a reference. Mutating the builder + // afterwards must not affect the base policy. + FlowContextHandoverPolicy base = FlowContextHandoverPolicy.permissive(); + FlowContextHandoverPolicy.Builder b = FlowContextHandoverPolicy.builder().startingFrom(base); + + b.redirectionEnabled(false); + FlowContextHandoverPolicy mutated = b.build(); + + assertFalse(mutated.isRedirectionEnabled()); + assertTrue(base.isRedirectionEnabled()); // base unchanged + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowExecutionContextFilterTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowExecutionContextFilterTest.java new file mode 100644 index 000000000000..4fc589e52840 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowExecutionContextFilterTest.java @@ -0,0 +1,255 @@ +/* + * 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.inflow.extension.config; + +import org.testng.annotations.Test; +import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; +import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNotSame; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +public class FlowExecutionContextFilterTest { + + @Test + public void testNullOriginalReturnsNull() { + + assertNull(FlowExecutionContextFilter.filter(null, FlowContextHandoverPolicy.permissive())); + } + + @Test + public void testNullPolicyFallsBackToPermissive() { + + FlowExecutionContext orig = createFullyPopulatedContext(); + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, null); + + // Permissive fallback — every field should round-trip. + assertEquals(copy.getTenantDomain(), "carbon.super"); + assertEquals(copy.getApplicationId(), "app-1"); + assertEquals(copy.getFlowType(), "REGISTRATION"); + assertEquals(copy.getCallbackUrl(), "https://callback"); + assertEquals(copy.getPortalUrl(), "https://portal"); + assertEquals(copy.getFlowUser().getUserId(), "user-1"); + assertEquals(copy.getFlowUser().getClaims().get("http://wso2.org/claims/email"), "u@e.com"); + assertEquals(copy.getProperties().get("p1"), "v1"); + } + + @Test + public void testContextIdentifierAlwaysCopied() { + + FlowExecutionContext orig = createFullyPopulatedContext(); + + // Even with the most restrictive policy, contextIdentifier must round-trip — it's the + // engine-internal flow id, not user data. + FlowContextHandoverPolicy strict = FlowContextHandoverPolicy.builder().build(); + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, strict); + + assertEquals(copy.getContextIdentifier(), "ctx-1"); + } + + @Test + public void testFlowDisabledOmitsAllFlowFields() { + + FlowExecutionContext orig = createFullyPopulatedContext(); + FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.builder() + .startingFrom(FlowContextHandoverPolicy.permissive()) + .flowEnabled(false) + .build(); + + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + + assertNull(copy.getTenantDomain()); + assertNull(copy.getApplicationId()); + assertNull(copy.getFlowType()); + assertNull(copy.getCallbackUrl()); + assertNull(copy.getPortalUrl()); + } + + @Test + public void testPerLeafFlowToggles() { + + FlowExecutionContext orig = createFullyPopulatedContext(); + FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.builder() + .flowEnabled(true) + .flowTenantDomain(true) + .flowApplicationId(false) // off + .flowFlowType(true) + .flowCallbackUrl(false) // off + .flowPortalUrl(true) + .build(); + + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + + assertEquals(copy.getTenantDomain(), "carbon.super"); + assertNull(copy.getApplicationId()); + assertEquals(copy.getFlowType(), "REGISTRATION"); + assertNull(copy.getCallbackUrl()); + assertEquals(copy.getPortalUrl(), "https://portal"); + } + + @Test + public void testUserDisabledYieldsEmptyFlowUser() { + + FlowExecutionContext orig = createFullyPopulatedContext(); + FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.builder() + .startingFrom(FlowContextHandoverPolicy.permissive()) + .userEnabled(false) + .build(); + + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + + // Must be a fresh non-null FlowUser — the response processor relies on this when + // applying pendingClaims/pendingCredentials on second-call SUCCESS responses. + assertNotNull(copy.getFlowUser()); + assertNull(copy.getFlowUser().getUserId()); + assertNull(copy.getFlowUser().getUserStoreDomain()); + assertTrue(copy.getFlowUser().getClaims().isEmpty()); + assertTrue(copy.getFlowUser().getUserCredentials().isEmpty()); + } + + @Test + public void testPerLeafUserToggles() { + + FlowExecutionContext orig = createFullyPopulatedContext(); + FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.builder() + .userEnabled(true) + .userUserId(false) // off + .userUserStoreDomain(true) + .userClaims(true) + .userCredentials(false) // off + .build(); + + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + + assertNull(copy.getFlowUser().getUserId()); + assertEquals(copy.getFlowUser().getUserStoreDomain(), "PRIMARY"); + assertEquals(copy.getFlowUser().getClaims().get("http://wso2.org/claims/email"), "u@e.com"); + assertTrue(copy.getFlowUser().getUserCredentials().isEmpty()); + } + + @Test + public void testCredentialsAreDeepCopied() { + + FlowExecutionContext orig = createFullyPopulatedContext(); + FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.permissive(); + + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + + char[] origPwd = orig.getFlowUser().getUserCredentials().get("password"); + char[] copyPwd = copy.getFlowUser().getUserCredentials().get("password"); + + assertNotSame(origPwd, copyPwd); + assertEquals(copyPwd, "secret".toCharArray()); + + // Wiping the copy must not affect the original — this is the desired post-extraction + // sanitisation behaviour: the request builder's Arrays.fill(...,'\0') zeroes the copy, + // not the original on the live FlowExecutionContext. + Arrays.fill(copyPwd, '\0'); + assertEquals(origPwd, "secret".toCharArray()); + } + + @Test + public void testClaimsMapIsDefensivelyCopied() { + + FlowExecutionContext orig = createFullyPopulatedContext(); + FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.permissive(); + + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + + // Mutating the copy's claims must not leak to the original. + copy.getFlowUser().addClaim("http://wso2.org/claims/leak", "oops"); + assertNull(orig.getFlowUser().getClaims().get("http://wso2.org/claims/leak")); + } + + @Test + public void testPropertiesDisabledOmitsAll() { + + FlowExecutionContext orig = createFullyPopulatedContext(); + FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.builder() + .startingFrom(FlowContextHandoverPolicy.permissive()) + .propertiesEnabled(false) + .build(); + + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + + // Properties default to an empty map on a fresh FlowExecutionContext. + assertTrue(copy.getProperties() == null || copy.getProperties().isEmpty()); + } + + @Test + public void testPropertiesAreDefensivelyCopied() { + + FlowExecutionContext orig = createFullyPopulatedContext(); + FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.permissive(); + + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + + copy.setProperty("p2", "leak"); + assertNull(orig.getProperty("p2")); + } + + @Test + public void testOriginalUntouchedAcrossAllToggles() { + + FlowExecutionContext orig = createFullyPopulatedContext(); + FlowContextHandoverPolicy strict = FlowContextHandoverPolicy.builder().build(); + + FlowExecutionContextFilter.filter(orig, strict); + + // Original must be byte-for-byte equivalent after filtering. + assertEquals(orig.getTenantDomain(), "carbon.super"); + assertEquals(orig.getApplicationId(), "app-1"); + assertEquals(orig.getFlowType(), "REGISTRATION"); + assertEquals(orig.getCallbackUrl(), "https://callback"); + assertEquals(orig.getPortalUrl(), "https://portal"); + assertEquals(orig.getFlowUser().getUserId(), "user-1"); + assertEquals(orig.getProperty("p1"), "v1"); + } + + private FlowExecutionContext createFullyPopulatedContext() { + + FlowExecutionContext ctx = new FlowExecutionContext(); + ctx.setContextIdentifier("ctx-1"); + ctx.setTenantDomain("carbon.super"); + ctx.setApplicationId("app-1"); + ctx.setFlowType("REGISTRATION"); + ctx.setCallbackUrl("https://callback"); + ctx.setPortalUrl("https://portal"); + + FlowUser user = new FlowUser(); + user.setUserId("user-1"); + user.setUserStoreDomain("PRIMARY"); + user.addClaim("http://wso2.org/claims/email", "u@e.com"); + + Map creds = new HashMap<>(); + creds.put("password", "secret".toCharArray()); + user.setUserCredentials(creds); + + ctx.setFlowUser(user); + ctx.setProperty("p1", "v1"); + return ctx; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java index 48eabe5cf521..2e27a4a70041 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java @@ -618,39 +618,4 @@ private FlowExecutionContext createContextWithMetadata(Map metad return context; } - - // ========================= sanitizeConnectionName ========================= - - @Test - public void testSanitizeConnectionNameBasic() { - - assertEquals(InFlowExtensionResponseProcessor.sanitizeConnectionName("Risk Assessment Extension"), - "risk.assessment.extension"); - } - - @Test - public void testSanitizeConnectionNameSpecialChars() { - - assertEquals(InFlowExtensionResponseProcessor.sanitizeConnectionName("My-Extension_v2 (test)"), - "my.extension.v2.test"); - } - - @Test - public void testSanitizeConnectionNameAlreadyClean() { - - assertEquals(InFlowExtensionResponseProcessor.sanitizeConnectionName("simpleext"), - "simpleext"); - } - - @Test - public void testSanitizeConnectionNameEmpty() { - - assertEquals(InFlowExtensionResponseProcessor.sanitizeConnectionName(""), ""); - } - - @Test - public void testSanitizeConnectionNameNull() { - - assertEquals(InFlowExtensionResponseProcessor.sanitizeConnectionName(null), ""); - } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessorTest.java index e73c058d2e4f..2b3876f32269 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessorTest.java @@ -628,66 +628,13 @@ public void testProcessFailureResponse() throws ActionExecutionResponseProcessor assertEquals(status.getResponse().getFailureDescription(), "Risk score exceeds threshold"); } - // ========================= processFailureResponse — i18n key prefixing ========================= + // ========================= processFailureResponse — full-key pass-through ========================= @Test - public void testProcessFailureResponseWithI18nKey() + public void testProcessFailureResponseWithFullKeyPassesThrough() throws ActionExecutionResponseProcessorException { - // Extension sends a bare dot-separated short key ({{}} wrapping is rejected by JSON parsers). - ActionInvocationFailureResponse failureResponse = mock(ActionInvocationFailureResponse.class); - when(failureResponse.getFailureReason()).thenReturn("high_risk"); - when(failureResponse.getFailureDescription()).thenReturn("user.access.denied"); - - @SuppressWarnings("unchecked") - ActionExecutionResponseContext responseContext = - mock(ActionExecutionResponseContext.class); - when(responseContext.getActionInvocationResponse()).thenReturn(failureResponse); - - FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionRequestBuilder.ACTION_NAME_KEY, "Risk Assessment Extension"); - - ActionExecutionStatus status = responseProcessor.processFailureResponse( - flowContext, responseContext); - - assertEquals(status.getStatus(), ActionExecutionStatus.Status.FAILED); - // Bare short key is prefixed and wrapped for frontend i18n resolution. - assertEquals(status.getResponse().getFailureDescription(), - "{{inflow.extension.risk.assessment.extension.user.access.denied}}"); - // Reason contains underscore — not a dot-separated key, passes through unchanged. - assertEquals(status.getResponse().getFailureReason(), "high_risk"); - } - - @Test - public void testProcessFailureResponseWithPlainText() - throws ActionExecutionResponseProcessorException { - - ActionInvocationFailureResponse failureResponse = mock(ActionInvocationFailureResponse.class); - when(failureResponse.getFailureReason()).thenReturn("high_risk_detected"); - when(failureResponse.getFailureDescription()).thenReturn("Risk score exceeds threshold"); - - @SuppressWarnings("unchecked") - ActionExecutionResponseContext responseContext = - mock(ActionExecutionResponseContext.class); - when(responseContext.getActionInvocationResponse()).thenReturn(failureResponse); - - FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionRequestBuilder.ACTION_NAME_KEY, "Risk Assessment Extension"); - - ActionExecutionStatus status = responseProcessor.processFailureResponse( - flowContext, responseContext); - - // Plain text passes through unchanged — no {{}} wrapper present. - assertEquals(status.getResponse().getFailureDescription(), "Risk score exceeds threshold"); - assertEquals(status.getResponse().getFailureReason(), "high_risk_detected"); - } - - @Test - public void testProcessFailureResponseWithFullyQualifiedKey() - throws ActionExecutionResponseProcessorException { - - // A string that does not match the bare short-key pattern (contains braces) must - // pass through untouched — the processor is not responsible for stripping artefacts. + // Extension now sends the full i18n key directly — processor must pass it through unchanged. ActionInvocationFailureResponse failureResponse = mock(ActionInvocationFailureResponse.class); when(failureResponse.getFailureReason()).thenReturn("high_risk"); when(failureResponse.getFailureDescription()) @@ -698,21 +645,22 @@ public void testProcessFailureResponseWithFullyQualifiedKey() mock(ActionExecutionResponseContext.class); when(responseContext.getActionInvocationResponse()).thenReturn(failureResponse); - FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionRequestBuilder.ACTION_NAME_KEY, "Risk Assessment Extension"); + FlowContext flowContext = FlowContext.create(); ActionExecutionStatus status = responseProcessor.processFailureResponse( flowContext, responseContext); - // Already starts with "inflow.extension." — must not be double-prefixed. + // Full key passes through unchanged — no wrapping or prefixing. assertEquals(status.getResponse().getFailureDescription(), "inflow.extension.risk.assessment.extension.user.access.denied"); + assertEquals(status.getResponse().getFailureReason(), "high_risk"); } @Test - public void testProcessFailureResponseWithoutActionName() + public void testProcessFailureResponseWithShortKeyPassesThrough() throws ActionExecutionResponseProcessorException { + // Short dot-separated key also passes through as-is — IS no longer adds a prefix. ActionInvocationFailureResponse failureResponse = mock(ActionInvocationFailureResponse.class); when(failureResponse.getFailureReason()).thenReturn("high_risk"); when(failureResponse.getFailureDescription()).thenReturn("user.access.denied"); @@ -722,13 +670,13 @@ public void testProcessFailureResponseWithoutActionName() mock(ActionExecutionResponseContext.class); when(responseContext.getActionInvocationResponse()).thenReturn(failureResponse); - // No ACTION_NAME_KEY in FlowContext — no prefix should be applied; value passes through. FlowContext flowContext = FlowContext.create(); ActionExecutionStatus status = responseProcessor.processFailureResponse( flowContext, responseContext); assertEquals(status.getResponse().getFailureDescription(), "user.access.denied"); + assertEquals(status.getResponse().getFailureReason(), "high_risk"); } // ========================= processErrorResponse ========================= From 98be83cd7a0b4058d506f1dca4a68145a2898508 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Fri, 1 May 2026 20:10:57 +0530 Subject: [PATCH 19/41] adding missing export packages --- .../org.wso2.carbon.identity.flow.execution.engine/pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml index 04588fca4ac4..9a8cf09a777a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml @@ -213,6 +213,8 @@ org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor, org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model, org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management, + org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config, + org.wso2.carbon.identity.flow.execution.engine.inflow.extension.metadata, org.wso2.carbon.identity.flow.execution.engine.graph; version="${carbon.identity.package.export.version}"
      From 944de1887e90e73036354e759247b7ff6df03791 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Sat, 2 May 2026 01:03:43 +0530 Subject: [PATCH 20/41] Removing action name fetching for prefixing and moved the synchronization to method level in flow context handover getter and setter. --- .../executor/InFlowExtensionRequestBuilder.java | 2 -- .../engine/internal/FlowExecutionEngineDataHolder.java | 10 +++------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java index 55a0da080e80..2404926cde04 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java @@ -90,12 +90,10 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex Action rawAction = actionExecutionContext.getAction(); AccessConfig accessConfig = null; Encryption encryption = null; - String actionName = null; if (rawAction instanceof InFlowExtensionAction) { InFlowExtensionAction ext = (InFlowExtensionAction) rawAction; accessConfig = ext.resolveAccessConfig(execCtx.getFlowType()); encryption = ext.getEncryption(); - actionName = ext.getName(); } else { if (LOG.isDebugEnabled()) { LOG.debug("No InFlowExtensionAction resolved. Proceeding with empty access configuration."); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java index 099280ce5391..28be71d9acf8 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java @@ -294,14 +294,10 @@ public void setCertificateManagementService(CertificateManagementService certifi * * @return The handover config, never null. */ - public FlowContextHandoverConfig getFlowContextHandoverConfig() { + public synchronized FlowContextHandoverConfig getFlowContextHandoverConfig() { if (flowContextHandoverConfig == null) { - synchronized (this) { - if (flowContextHandoverConfig == null) { - flowContextHandoverConfig = new FlowContextHandoverConfig(); - } - } + flowContextHandoverConfig = new FlowContextHandoverConfig(); } return flowContextHandoverConfig; } @@ -309,7 +305,7 @@ public FlowContextHandoverConfig getFlowContextHandoverConfig() { /** * Override the handover config. Intended for tests only. */ - public void setFlowContextHandoverConfig(FlowContextHandoverConfig flowContextHandoverConfig) { + public synchronized void setFlowContextHandoverConfig(FlowContextHandoverConfig flowContextHandoverConfig) { this.flowContextHandoverConfig = flowContextHandoverConfig; } From 6f3d94b44c783121511d3b07cfa58b5c44d9d6b8 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Mon, 4 May 2026 07:17:48 +0530 Subject: [PATCH 21/41] Improved diagnostic logs and fixed the fail fast behaviour when certificate is not available --- .../executor/InFlowExtensionExecutor.java | 78 +++++++++++++++---- .../executor/InFlowExtensionLogConstants.java | 44 +++++++++++ .../InFlowExtensionRequestBuilder.java | 29 +++++-- .../InFlowExtensionResponseProcessor.java | 50 +++++++++++- 4 files changed, 176 insertions(+), 25 deletions(-) create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionLogConstants.java diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java index 2948dde6e511..5d1fd61958a2 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java @@ -20,8 +20,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.wso2.carbon.identity.action.execution.api.constant.ActionExecutionLogConstants; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionException; +import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionRequestBuilderException; +import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionResponseProcessorException; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionStatus; import org.wso2.carbon.identity.action.execution.api.model.ActionType; import org.wso2.carbon.identity.action.execution.api.model.Error; @@ -104,8 +105,8 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE LOG.warn("No action ID configured for In-Flow Extension executor. Cannot execute."); if (LoggerUtils.isDiagnosticLogsEnabled()) { LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( - ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, - ActionExecutionLogConstants.ActionIDs.EXECUTE_ACTION) + InFlowExtensionLogConstants.COMPONENT_ID, + InFlowExtensionLogConstants.ActionIDs.EXECUTE) .resultMessage("In-Flow Extension action execution failed: action ID is not configured.") .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) @@ -116,11 +117,14 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE } if (LOG.isDebugEnabled()) { - LOG.debug("Executing In-Flow Extension action with ID: " + actionId); + LOG.debug("Executing In-Flow Extension action. actionId: " + actionId + + ", flowType: " + context.getFlowType() + + ", tenant: " + context.getTenantDomain()); } ActionExecutorService actionExecutorService = getActionExecutorService(); if (actionExecutorService == null) { + LOG.error("ActionExecutorService is not available. In-Flow Extension cannot execute. actionId: " + actionId); throw new FlowEngineException("ActionExecutorService is not available."); } @@ -128,8 +132,8 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE LOG.debug("In-Flow Extension action execution is disabled."); if (LoggerUtils.isDiagnosticLogsEnabled()) { LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( - ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, - ActionExecutionLogConstants.ActionIDs.EXECUTE_ACTION) + InFlowExtensionLogConstants.COMPONENT_ID, + InFlowExtensionLogConstants.ActionIDs.EXECUTE) .resultMessage("In-Flow Extension action execution failed: action type is disabled.") .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) .configParam("actionId", actionId) @@ -143,8 +147,6 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE try { // Resolve the per-flow-type handover policy and hand the action framework only a // FILTERED copy of the FlowExecutionContext (non-whitelisted fields nulled out). - // The original `context` is untouched and continues to drive the engine's own - // bookkeeping (OTFI cache swap, TaskExecutionNode pending-update propagation). FlowContextHandoverConfig handoverConfig = FlowExecutionEngineDataHolder.getInstance() .getFlowContextHandoverConfig(); FlowContextHandoverPolicy policy = handoverConfig.resolve(context.getFlowType()); @@ -154,7 +156,6 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE .add(FLOW_EXECUTION_CONTEXT_KEY, filteredContext) .add(HANDOVER_POLICY_KEY, policy); - // TODO: Have and switch-case ActionExecutionStatus executionStatus = actionExecutorService.execute( ActionType.IN_FLOW_EXTENSION, actionId, flowContext, context.getTenantDomain()); @@ -178,9 +179,14 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE if (pendingProperties != null && !pendingProperties.isEmpty()) { executionResponse.setContextProperty(pendingProperties); } + if (LOG.isDebugEnabled()) { + LOG.debug("In-Flow Extension action succeeded. actionId: " + actionId + + ", pendingClaims: " + (pendingClaims != null ? pendingClaims.size() : 0) + + ", pendingCredentials: " + (pendingCredentials != null ? pendingCredentials.size() : 0) + + ", pendingProperties: " + (pendingProperties != null ? pendingProperties.size() : 0)); + } } - // Tag RETRY responses with errorType so the frontend can identify extension errors. if (ExecutorStatus.STATUS_RETRY.equals(executionResponse.getResult())) { Map additionalInfo = executionResponse.getAdditionalInfo(); if (additionalInfo == null) { @@ -188,16 +194,20 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE } additionalInfo.put(ERROR_TYPE_KEY, EXTENSION_ERROR_TYPE); executionResponse.setAdditionalInfo(additionalInfo); + if (LOG.isDebugEnabled()) { + LOG.debug("In-Flow Extension action returned FAILED. actionId: " + actionId + + ", reason: " + additionalInfo.get(ERROR_MESSAGE_KEY)); + } } return executionResponse; - } catch (ActionExecutionException e) { // TODO: replace with a action execution server exception - LOG.error("Error executing In-Flow Extension action.", e); + } catch (ActionExecutionException e) { + logActionExecutionException(e, actionId); if (LoggerUtils.isDiagnosticLogsEnabled()) { LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( - ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, - ActionExecutionLogConstants.ActionIDs.EXECUTE_ACTION) + InFlowExtensionLogConstants.COMPONENT_ID, + InFlowExtensionLogConstants.ActionIDs.EXECUTE) .resultMessage("In-Flow Extension action execution failed: " + e.getMessage()) .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) .configParam("actionId", actionId) @@ -285,13 +295,22 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt String redirectUrl = flowContext.getValue(PENDING_REDIRECT_URL_KEY, String.class); if (redirectUrl == null || redirectUrl.isEmpty()) { // Defensive: response processor should have rejected this earlier. + LOG.warn("In-Flow Extension returned INCOMPLETE without a redirect URL."); + if (LoggerUtils.isDiagnosticLogsEnabled()) { + LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( + InFlowExtensionLogConstants.COMPONENT_ID, + InFlowExtensionLogConstants.ActionIDs.PROCESS_RESPONSE) + .resultMessage("In-Flow Extension returned INCOMPLETE without a redirect URL.") + .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED)); + } response.setResult(ExecutorStatus.STATUS_ERROR); response.setErrorMessage("Extension returned INCOMPLETE without a redirect URL."); break; } - // Generate OTFI exactly like MagicLinkExecutor — keeps the cache-swap mechanism - // in FlowExecutionService unchanged across executors. + // Generate OTFI — keeps the cache-swap mechanism String otfi = UUID.randomUUID().toString(); while (otfi.equals(context.getContextIdentifier())) { otfi = UUID.randomUUID().toString(); @@ -308,6 +327,15 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt response.setAdditionalInfo(redirectInfo); response.setResult(ExecutorStatus.STATUS_EXTERNAL_REDIRECTION); + if (LOG.isDebugEnabled()) { + try { + String host = new java.net.URI(redirectUrl).getHost(); + LOG.debug("In-Flow Extension returned INCOMPLETE. Redirecting to: " + host + + ", flowId (OTFI) generated."); + } catch (java.net.URISyntaxException ignored) { + LOG.debug("In-Flow Extension returned INCOMPLETE with a redirect URL."); + } + } break; } @@ -345,6 +373,24 @@ private ActionExecutorService getActionExecutorService() { return FlowExecutionEngineDataHolder.getInstance().getActionExecutorService(); } + /** + * Log an {@link ActionExecutionException} at the appropriate level based on its root cause. + * Config and contract violations are logged at WARN; infrastructure and unexpected failures at ERROR. + */ + private void logActionExecutionException(ActionExecutionException e, String actionId) { + + Throwable cause = e.getCause(); + if (cause instanceof ActionExecutionRequestBuilderException) { + LOG.warn("In-Flow Extension action '" + actionId + + "' request build failed. Check action access configuration: " + e.getMessage()); + } else if (cause instanceof ActionExecutionResponseProcessorException) { + LOG.error("In-Flow Extension action '" + actionId + + "' response processing failed (extension contract violation or internal error).", e); + } else { + LOG.error("Error executing In-Flow Extension action '" + actionId + "'.", e); + } + } + /** * Strip the {@code {{...}}} wrapper from an i18n key so the JSP error page can resolve it * via {@code AuthenticationEndpointUtil.i18n(resourceBundle, key)}. Raw text values (without diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionLogConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionLogConstants.java new file mode 100644 index 000000000000..22592fc4f21e --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionLogConstants.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025, 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.inflow.extension.executor; + +/** + * Diagnostic log constants for the In-Flow Extension layer. + * + *

      These constants are intentionally separate from + * {@link org.wso2.carbon.identity.action.execution.api.constant.ActionExecutionLogConstants} + * because the events tracked here are flow-engine concerns (executor lifecycle, operation routing, + * JWE decryption) rather than action HTTP-call concerns (which remain under "action-execution").

      + */ +public class InFlowExtensionLogConstants { + + private InFlowExtensionLogConstants() { + } + + public static final String COMPONENT_ID = "inflow-extension"; + + /** + * Action IDs for diagnostic log events emitted by the In-Flow Extension layer. + */ + public static class ActionIDs { + + public static final String EXECUTE = "execute-inflow-extension"; + public static final String PROCESS_RESPONSE = "process-inflow-extension-response"; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java index 2404926cde04..0a49d1dea45d 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java @@ -86,7 +86,7 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex "FlowExecutionContext not found in FlowContext."); } - // Resolve action-specific config. The action is already resolved by ActionExecutorServiceImpl. + // Resolve action-specific config. Action rawAction = actionExecutionContext.getAction(); AccessConfig accessConfig = null; Encryption encryption = null; @@ -107,6 +107,10 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex expose = Collections.emptyList(); } + if (expose.isEmpty() && LOG.isDebugEnabled()) { + LOG.debug("No expose paths configured for In-Flow Extension action. Event will contain no data fields."); + } + // Store resolved modify paths (with encryption flags) for the response processor. List modifyPaths = (accessConfig != null && accessConfig.getModify() != null) ? accessConfig.getModify() : Collections.emptyList(); @@ -126,7 +130,7 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex .resultStatus(DiagnosticLog.ResultStatus.SUCCESS)); } - // Build allowed operations: REPLACE (from modify paths, if any) + REDIRECT (always). + // Build allowed operations: REPLACE (from modify paths, if any) + REDIRECT (if enabled). List allowedOperations = buildAllowedOperations(accessConfig, flowContext); // Determine certificate PEM for outbound encryption. @@ -135,6 +139,20 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex certificatePEM = encryption.getCertificate().getCertificateContent(); } + // Fail fast: if any expose path is marked encrypted but no certificate is configured, + // proceeding would silently leak sensitive data as plaintext. + if (certificatePEM == null && accessConfig != null) { + for (String exposePath : expose) { + if (accessConfig.isExposePathEncrypted(exposePath)) { + LOG.error("Outbound encryption required for expose path '" + exposePath + + "' but no certificate is configured for this In-Flow Extension action."); + throw new ActionExecutionRequestBuilderException( + "Outbound encryption is configured for expose path '" + exposePath + + "' but no outbound certificate is configured for this action."); + } + } + } + InFlowExtensionEvent event = buildEvent(execCtx, expose, accessConfig, certificatePEM); return new ActionExecutionRequest.Builder() @@ -204,8 +222,7 @@ private List buildAllowedOperations(AccessConfig accessConfig, } // REDIRECT is advertised when the per-flow-type handover policy allows it. A null - // policy (test fixtures or unconfigured deployments) preserves the prior behaviour of - // always permitting REDIRECT. + // policy always permitting REDIRECT. FlowContextHandoverPolicy policy = flowContext.getValue( InFlowExtensionExecutor.HANDOVER_POLICY_KEY, FlowContextHandoverPolicy.class); if (policy == null || policy.isRedirectionEnabled()) { @@ -332,7 +349,7 @@ private User buildUser(FlowUser flowUser, List expose, if (isLeafExposed(claimPath, expose)) { String claimValue = claim.getValue(); // Encrypt claim value if the expose path is marked as encrypted. - if (shouldEncrypt(claimPath, accessConfig, certificatePEM)) { + if (claimValue != null && shouldEncrypt(claimPath, accessConfig, certificatePEM)) { claimValue = encryptValue(claimValue, certificatePEM); } userClaims.add(new UserClaim(claim.getKey(), claimValue)); @@ -396,7 +413,7 @@ private Map filterMap(Map map, String areaPrefix, List String fullPath = areaPrefix + entry.getKey(); if (isLeafExposed(fullPath, expose)) { T value = entry.getValue(); - if (shouldEncrypt(fullPath, accessConfig, certificatePEM)) { + if (value != null && shouldEncrypt(fullPath, accessConfig, certificatePEM)) { value = (T) encryptValue(String.valueOf(value), certificatePEM); } filtered.put(entry.getKey(), value); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java index 2e74fc9a6577..6fa1ddda9049 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java @@ -126,6 +126,10 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon results.add(processOperation( operation, pathTypeAnnotations, pendingClaims, pendingCredentials, pendingProperties)); } + } else { + if (LOG.isDebugEnabled()) { + LOG.debug("In-Flow Extension SUCCESS response contained no operations. No context updates applied."); + } } // Store non-empty pending maps in FlowContext for the executor to forward to TaskExecutionNode. @@ -340,6 +344,16 @@ public ActionExecutionStatus processIncompleteResponse(FlowContext f } if (redirectUrl == null || redirectUrl.isEmpty()) { + LOG.warn("In-Flow Extension INCOMPLETE response is missing a REDIRECT operation."); + if (LoggerUtils.isDiagnosticLogsEnabled()) { + LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( + InFlowExtensionLogConstants.COMPONENT_ID, + InFlowExtensionLogConstants.ActionIDs.PROCESS_RESPONSE) + .resultMessage("INCOMPLETE response from In-Flow Extension is missing a REDIRECT operation.") + .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED)); + } throw new ActionExecutionResponseProcessorException( "INCOMPLETE response from In-Flow Extension must contain a REDIRECT operation."); } @@ -352,6 +366,24 @@ public ActionExecutionStatus processIncompleteResponse(FlowContext f flowContext.add(InFlowExtensionExecutor.PENDING_REDIRECT_URL_KEY, redirectUrl); + if (LOG.isDebugEnabled()) { + try { + String host = new java.net.URI(redirectUrl).getHost(); + LOG.debug("In-Flow Extension INCOMPLETE: redirect URL host resolved: " + host); + } catch (java.net.URISyntaxException ignored) { + LOG.debug("In-Flow Extension INCOMPLETE: redirect URL stored in flow context."); + } + } + if (LoggerUtils.isDiagnosticLogsEnabled()) { + LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( + InFlowExtensionLogConstants.COMPONENT_ID, + InFlowExtensionLogConstants.ActionIDs.PROCESS_RESPONSE) + .resultMessage("In-Flow Extension INCOMPLETE response processed. Redirect URL stored in flow context.") + .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.SUCCESS)); + } + return new IncompleteStatus.Builder() .responseContext(Collections.emptyMap()) .build(); @@ -409,17 +441,17 @@ private void logOperationExecutionResults(List results operationDetailsList.add(details); }); - DiagnosticLog.DiagnosticLogBuilder diagLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, ActionExecutionLogConstants.ActionIDs.PROCESS_ACTION_RESPONSE); - diagLogBuilder + diagnosticLogBuilder .inputParam("executedOperations", operationDetailsList) .resultMessage("Processed operations for " + getSupportedActionType().getDisplayName() + " action.") .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) .resultStatus(DiagnosticLog.ResultStatus.SUCCESS) .build(); - LoggerUtils.triggerDiagnosticLogEvent(diagLogBuilder); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); } if (LOG.isDebugEnabled()) { @@ -501,6 +533,18 @@ private PerformableOperation decryptOperationValueIfNeeded(PerformableOperation } return decryptedOp; } catch (Exception e) { + LOG.error("Failed to decrypt inbound JWE value for path '" + operation.getPath() + + "', tenant: " + tenantDomain, e); + if (LoggerUtils.isDiagnosticLogsEnabled()) { + LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( + InFlowExtensionLogConstants.COMPONENT_ID, + InFlowExtensionLogConstants.ActionIDs.PROCESS_RESPONSE) + .resultMessage("Failed to decrypt inbound JWE value for modify path.") + .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .inputParam("path", operation.getPath()) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED)); + } throw new ActionExecutionResponseProcessorException( "Failed to decrypt inbound JWE value for path: " + operation.getPath(), e); } From 0eaf61cf619430e4f08a20a54397c64154e370df Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Mon, 4 May 2026 13:35:36 +0530 Subject: [PATCH 22/41] Moved all Inflow extension Constants to flow execution Constants --- .../flow/execution/engine/Constants.java | 39 +++++++++++++ .../executor/InFlowExtensionExecutor.java | 48 +++++++-------- .../InFlowExtensionRequestBuilder.java | 20 +++---- .../InFlowExtensionResponseProcessor.java | 40 +++++++------ .../InFlowExtensionEvent.java | 2 +- .../OperationExecutionResult.java | 2 +- .../executor/InFlowExtensionExecutorTest.java | 13 +++-- .../InFlowExtensionRequestBuilderTest.java | 58 +++++++++---------- .../InFlowExtensionResponseProcessorTest.java | 45 +++++++------- .../model/InFlowExtensionEventTest.java | 1 - .../model/OperationExecutionResultTest.java | 1 - 11 files changed, 149 insertions(+), 120 deletions(-) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/{executor => model}/InFlowExtensionEvent.java (99%) rename components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/{executor => model}/OperationExecutionResult.java (98%) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java index b5115d80c832..897a305db764 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java @@ -271,6 +271,45 @@ private ExecutorStatus() { } } + /** + * Constants for the In-Flow Extension executor pipeline. + * + *

      Keys are shared across the executor, request builder, and response processor + * via the {@link org.wso2.carbon.identity.action.execution.api.model.FlowContext} + * handoff mechanism. Path prefixes drive operation routing in the response processor.

      + */ + public static class InFlowExtensionConstants { + + private InFlowExtensionConstants() { + + } + + // ---- FlowContext pipeline keys ---- + public static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; + public static final String HANDOVER_POLICY_KEY = "handoverPolicy"; + public static final String PATH_TYPE_ANNOTATIONS_KEY = "pathTypeAnnotations"; + public static final String MODIFY_PATHS_KEY = "modifyPaths"; + public static final String PENDING_CLAIMS_KEY = "pendingClaims"; + public static final String PENDING_CREDENTIALS_KEY = "pendingCredentials"; + public static final String PENDING_PROPERTIES_KEY = "pendingProperties"; + public static final String PENDING_REDIRECT_URL_KEY = "pendingRedirectUrl"; + + // ---- Response info keys ---- + public static final String ERROR_TYPE_KEY = "errorType"; + public static final String EXTENSION_ERROR_TYPE = "EXTENSION_ERROR"; + public static final String ERROR_MESSAGE_KEY = "errorMessage"; + public static final String ERROR_DESCRIPTION_KEY = "errorDescription"; + + // ---- Context path prefixes ---- + public static final String PROPERTIES_PATH_PREFIX = "/properties/"; + public static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; + public static final String USER_CREDENTIALS_PATH_PREFIX = "/user/credentials/"; + + // ---- Miscellaneous ---- + public static final String ACTION_ID_METADATA_KEY = "actionId"; + public static final String EXTENSION_ERROR_CODE = "65033"; + } + public static class SQLConstants { private SQLConstants() { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java index 5d1fd61958a2..38faa8d648c3 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java @@ -33,6 +33,7 @@ import org.wso2.carbon.identity.flow.execution.engine.Constants; import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; import org.wso2.carbon.utils.DiagnosticLog; +import org.wso2.carbon.identity.flow.execution.engine.Constants.InFlowExtensionConstants; import org.wso2.carbon.identity.flow.execution.engine.exception.FlowEngineException; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverConfig; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverPolicy; @@ -74,19 +75,7 @@ public class InFlowExtensionExecutor implements Executor { private static final Log LOG = LogFactory.getLog(InFlowExtensionExecutor.class); private static final String EXECUTOR_NAME = "InFlowExtensionExecutor"; - public static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; - public static final String HANDOVER_POLICY_KEY = "handoverPolicy"; - public static final String PATH_TYPE_ANNOTATIONS_KEY = "pathTypeAnnotations"; - public static final String PENDING_CLAIMS_KEY = "pendingClaims"; - public static final String PENDING_CREDENTIALS_KEY = "pendingCredentials"; - public static final String PENDING_PROPERTIES_KEY = "pendingProperties"; - public static final String PENDING_REDIRECT_URL_KEY = "pendingRedirectUrl"; - private static final String ACTION_ID_METADATA_KEY = "actionId"; - private static final String ERROR_TYPE_KEY = "errorType"; - private static final String EXTENSION_ERROR_TYPE = "EXTENSION_ERROR"; - public static final String ERROR_MESSAGE_KEY = "errorMessage"; - public static final String ERROR_DESCRIPTION_KEY = "errorDescription"; - public static final String EXTENSION_ERROR_CODE = "65033"; + @Override public String getName() { @@ -100,7 +89,7 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE ExecutorResponse response = new ExecutorResponse(); - String actionId = getMetadataValue(context, ACTION_ID_METADATA_KEY); + String actionId = getMetadataValue(context, InFlowExtensionConstants.ACTION_ID_METADATA_KEY); if (actionId == null || actionId.isEmpty()) { LOG.warn("No action ID configured for In-Flow Extension executor. Cannot execute."); if (LoggerUtils.isDiagnosticLogsEnabled()) { @@ -153,8 +142,8 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE FlowExecutionContext filteredContext = FlowExecutionContextFilter.filter(context, policy); FlowContext flowContext = FlowContext.create() - .add(FLOW_EXECUTION_CONTEXT_KEY, filteredContext) - .add(HANDOVER_POLICY_KEY, policy); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, filteredContext) + .add(InFlowExtensionConstants.HANDOVER_POLICY_KEY, policy); ActionExecutionStatus executionStatus = actionExecutorService.execute( ActionType.IN_FLOW_EXTENSION, actionId, flowContext, context.getTenantDomain()); @@ -165,17 +154,17 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE // and forward them to TaskExecutionNode via ExecutorResponse fields. if (ExecutorStatus.STATUS_COMPLETE.equals(executionResponse.getResult())) { Map pendingClaims = - (Map) flowContext.getValue(PENDING_CLAIMS_KEY, Map.class); + (Map) flowContext.getValue(InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class); if (pendingClaims != null && !pendingClaims.isEmpty()) { executionResponse.setUpdatedUserClaims(pendingClaims); } Map pendingCredentials = - (Map) flowContext.getValue(PENDING_CREDENTIALS_KEY, Map.class); + (Map) flowContext.getValue(InFlowExtensionConstants.PENDING_CREDENTIALS_KEY, Map.class); if (pendingCredentials != null && !pendingCredentials.isEmpty()) { executionResponse.setUserCredentials(pendingCredentials); } Map pendingProperties = - (Map) flowContext.getValue(PENDING_PROPERTIES_KEY, Map.class); + (Map) flowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); if (pendingProperties != null && !pendingProperties.isEmpty()) { executionResponse.setContextProperty(pendingProperties); } @@ -192,11 +181,12 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE if (additionalInfo == null) { additionalInfo = new HashMap<>(); } - additionalInfo.put(ERROR_TYPE_KEY, EXTENSION_ERROR_TYPE); + additionalInfo.put(InFlowExtensionConstants.ERROR_TYPE_KEY, + InFlowExtensionConstants.EXTENSION_ERROR_TYPE); executionResponse.setAdditionalInfo(additionalInfo); if (LOG.isDebugEnabled()) { LOG.debug("In-Flow Extension action returned FAILED. actionId: " + actionId - + ", reason: " + additionalInfo.get(ERROR_MESSAGE_KEY)); + + ", reason: " + additionalInfo.get(InFlowExtensionConstants.ERROR_MESSAGE_KEY)); } } @@ -263,10 +253,12 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt if (failure != null) { Map failureInfo = new HashMap<>(); if (failure.getFailureReason() != null) { - failureInfo.put(ERROR_MESSAGE_KEY, failure.getFailureReason()); + failureInfo.put(InFlowExtensionConstants.ERROR_MESSAGE_KEY, + failure.getFailureReason()); } if (failure.getFailureDescription() != null) { - failureInfo.put(ERROR_DESCRIPTION_KEY, failure.getFailureDescription()); + failureInfo.put(InFlowExtensionConstants.ERROR_DESCRIPTION_KEY, + failure.getFailureDescription()); } response.setAdditionalInfo(failureInfo); response.setErrorMessage(buildUserFacingErrorMessage(failure)); @@ -276,14 +268,16 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt case ERROR: response.setResult(ExecutorStatus.STATUS_ERROR); Error error = (Error) executionStatus.getResponse(); - response.setErrorCode(EXTENSION_ERROR_CODE); + response.setErrorCode(InFlowExtensionConstants.EXTENSION_ERROR_CODE); if (error != null) { Map errorInfo = new HashMap<>(); if (error.getErrorMessage() != null) { - errorInfo.put(ERROR_MESSAGE_KEY, error.getErrorMessage()); + errorInfo.put(InFlowExtensionConstants.ERROR_MESSAGE_KEY, + error.getErrorMessage()); } if (error.getErrorDescription() != null) { - errorInfo.put(ERROR_DESCRIPTION_KEY, error.getErrorDescription()); + errorInfo.put(InFlowExtensionConstants.ERROR_DESCRIPTION_KEY, + error.getErrorDescription()); } response.setAdditionalInfo(errorInfo); response.setErrorMessage(stripI18nBraces(error.getErrorMessage())); @@ -292,7 +286,7 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt break; case INCOMPLETE: { - String redirectUrl = flowContext.getValue(PENDING_REDIRECT_URL_KEY, String.class); + String redirectUrl = flowContext.getValue(InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, String.class); if (redirectUrl == null || redirectUrl.isEmpty()) { // Defensive: response processor should have rejected this earlier. LOG.warn("In-Flow Extension returned INCOMPLETE without a redirect URL."); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java index 0a49d1dea45d..20a891eca3ae 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java @@ -23,6 +23,7 @@ import org.wso2.carbon.identity.action.execution.api.constant.ActionExecutionLogConstants; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionRequestBuilderException; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.*; import org.wso2.carbon.utils.DiagnosticLog; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequest; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequestContext; @@ -38,11 +39,8 @@ import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionRequestBuilder; import org.wso2.carbon.identity.action.management.api.model.Action; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.flow.execution.engine.Constants.InFlowExtensionConstants; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverPolicy; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; @@ -66,8 +64,6 @@ public class InFlowExtensionRequestBuilder implements ActionExecutionRequestBuil private static final Log LOG = LogFactory.getLog(InFlowExtensionRequestBuilder.class); - public static final String MODIFY_PATHS_KEY = "modifyPaths"; - @Override public ActionType getSupportedActionType() { @@ -80,7 +76,7 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex throws ActionExecutionRequestBuilderException { FlowExecutionContext execCtx = flowContext.getValue( - InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, FlowExecutionContext.class); + InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, FlowExecutionContext.class); if (execCtx == null) { throw new ActionExecutionRequestBuilderException( "FlowExecutionContext not found in FlowContext."); @@ -114,7 +110,7 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex // Store resolved modify paths (with encryption flags) for the response processor. List modifyPaths = (accessConfig != null && accessConfig.getModify() != null) ? accessConfig.getModify() : Collections.emptyList(); - flowContext.add(MODIFY_PATHS_KEY, modifyPaths); + flowContext.add(InFlowExtensionConstants.MODIFY_PATHS_KEY, modifyPaths); if (LoggerUtils.isDiagnosticLogsEnabled()) { LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( @@ -168,7 +164,7 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex * includes a REDIRECT operation so the extension may signal external redirection. A * REPLACE operation is added only when the access config defines modify paths. Path * type annotations (e.g. {@code []} or {@code [schema]}) are stripped and stored in - * the {@link FlowContext} under {@link InFlowExtensionExecutor#PATH_TYPE_ANNOTATIONS_KEY} + * the {@link FlowContext} under {@link InFlowExtensionConstants#PATH_TYPE_ANNOTATIONS_KEY} * for the response processor. * * @param accessConfig The access config containing modify paths (may be null). @@ -211,7 +207,7 @@ private List buildAllowedOperations(AccessConfig accessConfig, allowedOps.add(replaceOp); if (!pathTypeAnnotations.isEmpty()) { - flowContext.add(InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); + flowContext.add(InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); } if (LOG.isDebugEnabled()) { @@ -224,7 +220,7 @@ private List buildAllowedOperations(AccessConfig accessConfig, // REDIRECT is advertised when the per-flow-type handover policy allows it. A null // policy always permitting REDIRECT. FlowContextHandoverPolicy policy = flowContext.getValue( - InFlowExtensionExecutor.HANDOVER_POLICY_KEY, FlowContextHandoverPolicy.class); + InFlowExtensionConstants.HANDOVER_POLICY_KEY, FlowContextHandoverPolicy.class); if (policy == null || policy.isRedirectionEnabled()) { AllowedOperation redirectOp = new AllowedOperation(); redirectOp.setOp(Operation.REDIRECT); @@ -261,7 +257,7 @@ private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List processSuccessResponse(FlowContext flowCon throws ActionExecutionResponseProcessorException { FlowExecutionContext execCtx = flowContext.getValue( - InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, FlowExecutionContext.class); + InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, FlowExecutionContext.class); String tenantDomain = execCtx != null ? execCtx.getTenantDomain() : null; // Read path type annotations set by the request builder. // Maps clean paths to annotation content. Map pathTypeAnnotations = flowContext.getValue( - InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); + InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, Map.class); if (pathTypeAnnotations == null) { pathTypeAnnotations = Collections.emptyMap(); } @@ -107,7 +105,7 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon // Reconstruct AccessConfig from the resolved modify paths stored by the request builder. // This reuses AccessConfig.isModifyPathEncrypted() for canonical prefix-based encryption checking. List modifyPaths = flowContext.getValue( - InFlowExtensionRequestBuilder.MODIFY_PATHS_KEY, List.class); + InFlowExtensionConstants.MODIFY_PATHS_KEY, List.class); AccessConfig accessConfig = modifyPaths != null ? new AccessConfig(null, modifyPaths) : null; // Accumulate pending updates — applied by TaskExecutionNode via ExecutorResponse fields. @@ -134,13 +132,13 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon // Store non-empty pending maps in FlowContext for the executor to forward to TaskExecutionNode. if (!pendingClaims.isEmpty()) { - flowContext.add(InFlowExtensionExecutor.PENDING_CLAIMS_KEY, pendingClaims); + flowContext.add(InFlowExtensionConstants.PENDING_CLAIMS_KEY, pendingClaims); } if (!pendingCredentials.isEmpty()) { - flowContext.add(InFlowExtensionExecutor.PENDING_CREDENTIALS_KEY, pendingCredentials); + flowContext.add(InFlowExtensionConstants.PENDING_CREDENTIALS_KEY, pendingCredentials); } if (!pendingProperties.isEmpty()) { - flowContext.add(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, pendingProperties); + flowContext.add(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, pendingProperties); } logOperationExecutionResults(results); @@ -179,17 +177,18 @@ private OperationExecutionResult processOperation(PerformableOperation operation } // Route to appropriate handler based on path prefix. - if (path.startsWith(PROPERTIES_PATH_PREFIX)) { + if (path.startsWith(InFlowExtensionConstants.PROPERTIES_PATH_PREFIX)) { return handlePropertyOperation(operation, pathTypeAnnotations, pendingProperties); - } else if (path.startsWith(USER_CLAIMS_PATH_PREFIX)) { + } else if (path.startsWith(InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX)) { return handleUserClaimOperation(operation, pendingClaims); - } else if (path.startsWith(USER_CREDENTIALS_PATH_PREFIX)) { + } else if (path.startsWith(InFlowExtensionConstants.USER_CREDENTIALS_PATH_PREFIX)) { return handleUserCredentialOperation(operation, pendingCredentials); } return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Unknown path prefix. Supported: " + PROPERTIES_PATH_PREFIX + - ", " + USER_CLAIMS_PATH_PREFIX + ", " + USER_CREDENTIALS_PATH_PREFIX); + "Unknown path prefix. Supported: " + InFlowExtensionConstants.PROPERTIES_PATH_PREFIX + + ", " + InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX + + ", " + InFlowExtensionConstants.USER_CREDENTIALS_PATH_PREFIX); } /** @@ -206,7 +205,8 @@ private OperationExecutionResult processOperation(PerformableOperation operation private OperationExecutionResult handlePropertyOperation(PerformableOperation operation, Map pathTypeAnnotations, Map pendingProperties) { - String propertyName = extractNameFromPath(operation.getPath(), PROPERTIES_PATH_PREFIX); + String propertyName = extractNameFromPath(operation.getPath(), + InFlowExtensionConstants.PROPERTIES_PATH_PREFIX); if (propertyName == null || propertyName.isEmpty()) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, @@ -254,7 +254,8 @@ private OperationExecutionResult handlePropertyOperation(PerformableOperation op private OperationExecutionResult handleUserClaimOperation(PerformableOperation operation, Map pendingClaims) { - String claimUri = extractNameFromPath(operation.getPath(), USER_CLAIMS_PATH_PREFIX); + String claimUri = extractNameFromPath(operation.getPath(), + InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX); if (claimUri == null || claimUri.isEmpty()) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, @@ -283,7 +284,8 @@ private OperationExecutionResult handleUserClaimOperation(PerformableOperation o private OperationExecutionResult handleUserCredentialOperation(PerformableOperation operation, Map pendingCredentials) { - String credentialKey = extractNameFromPath(operation.getPath(), USER_CREDENTIALS_PATH_PREFIX); + String credentialKey = extractNameFromPath(operation.getPath(), + InFlowExtensionConstants.USER_CREDENTIALS_PATH_PREFIX); if (credentialKey == null || credentialKey.isEmpty()) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, @@ -364,7 +366,7 @@ public ActionExecutionStatus processIncompleteResponse(FlowContext f + "expected to resend them on the resume call after callback."); } - flowContext.add(InFlowExtensionExecutor.PENDING_REDIRECT_URL_KEY, redirectUrl); + flowContext.add(InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, redirectUrl); if (LOG.isDebugEnabled()) { try { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionEvent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEvent.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionEvent.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEvent.java index bdce2942e214..466a15247ea0 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionEvent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEvent.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; import org.wso2.carbon.identity.action.execution.api.model.Application; import org.wso2.carbon.identity.action.execution.api.model.Event; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/OperationExecutionResult.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResult.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/OperationExecutionResult.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResult.java index b8a3faff499b..79517fe81ad1 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/OperationExecutionResult.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResult.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; +package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java index 2e27a4a70041..739bab750f87 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java @@ -33,6 +33,7 @@ import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; +import org.wso2.carbon.identity.flow.execution.engine.Constants; import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverConfig; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverPolicy; @@ -303,7 +304,7 @@ public void testExecuteError() throws Exception { ExecutorResponse response = executor.execute(context); assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); - assertEquals(response.getErrorCode(), InFlowExtensionExecutor.EXTENSION_ERROR_CODE); + assertEquals(response.getErrorCode(), Constants.InFlowExtensionConstants.EXTENSION_ERROR_CODE); // errorMessage carries the Error reason/code field; errorDescription carries the human-readable text. assertEquals(response.getErrorMessage(), "internal_error"); assertEquals(response.getErrorDescription(), "DB connection failed"); @@ -331,7 +332,7 @@ public void testExecuteErrorNoDescription() throws Exception { ExecutorResponse response = executor.execute(context); assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); - assertEquals(response.getErrorCode(), InFlowExtensionExecutor.EXTENSION_ERROR_CODE); + assertEquals(response.getErrorCode(), Constants.InFlowExtensionConstants.EXTENSION_ERROR_CODE); assertEquals(response.getErrorMessage(), "internal_error"); assertNull(response.getErrorDescription()); } @@ -358,7 +359,7 @@ public void testExecuteErrorBothNull() throws Exception { ExecutorResponse response = executor.execute(context); assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); - assertEquals(response.getErrorCode(), InFlowExtensionExecutor.EXTENSION_ERROR_CODE); + assertEquals(response.getErrorCode(), Constants.InFlowExtensionConstants.EXTENSION_ERROR_CODE); // Both fields null → errorMessage and errorDescription remain null; errorCode alone triggers FE-65033 routing. assertNull(response.getErrorMessage()); assertNull(response.getErrorDescription()); @@ -415,7 +416,7 @@ public void testExecuteIncompleteWithRedirectUrlReturnsExternalRedirection() thr any(FlowContext.class), eq("carbon.super"))) .thenAnswer(invocation -> { FlowContext fc = invocation.getArgument(2); - fc.add(InFlowExtensionExecutor.PENDING_REDIRECT_URL_KEY, + fc.add(Constants.InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, "https://example.com/step-up"); return incompleteStatus; }); @@ -464,7 +465,7 @@ public void testExecuteIncompleteRedirectAppendsFlowIdWithAmpersandWhenUrlHasQue .thenAnswer(invocation -> { FlowContext fc = invocation.getArgument(2); // URL already has a query string — the executor must use & not ?. - fc.add(InFlowExtensionExecutor.PENDING_REDIRECT_URL_KEY, + fc.add(Constants.InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, "https://example.com/step-up?ref=abc"); return incompleteStatus; }); @@ -497,7 +498,7 @@ public void testExecuteIncompleteRedirectEmptyUrlReturnsError() throws Exception any(FlowContext.class), eq("carbon.super"))) .thenAnswer(invocation -> { FlowContext fc = invocation.getArgument(2); - fc.add(InFlowExtensionExecutor.PENDING_REDIRECT_URL_KEY, ""); + fc.add(Constants.InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, ""); return incompleteStatus; }); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilderTest.java index 74709e518a81..535b3486d846 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilderTest.java @@ -31,11 +31,9 @@ import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; +import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.*; import org.wso2.carbon.identity.certificate.management.model.Certificate; +import org.wso2.carbon.identity.flow.execution.engine.Constants; 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.mgt.model.NodeConfig; @@ -106,7 +104,7 @@ public void testBuildRequestWithMinimalContext() throws ActionExecutionRequestBu FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -122,7 +120,7 @@ public void testBuildRequestUsesEmptyExposeWhenExposeIsNull() FlowExecutionContext execCtx = createFullFlowExecutionContext(); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -146,7 +144,7 @@ public void testBuildRequestWithValidModifyPaths() Arrays.asList(new ContextPath("/properties/riskScore", false))); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -168,7 +166,7 @@ public void testBuildRequestWithNoAccessConfig() FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); // No action → no access config → no modify paths → only REDIRECT (always present). ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); @@ -188,7 +186,7 @@ public void testBuildRequestWithEmptyModifyPaths() AccessConfig accessConfig = new AccessConfig(null, Arrays.asList()); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -211,7 +209,7 @@ public void testRedirectIsAdvertisedAlongsideReplace() Arrays.asList(new ContextPath("/properties/riskScore", false))); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -229,7 +227,7 @@ public void testRedirectIsAdvertisedWhenNoAccessConfig() FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -252,7 +250,7 @@ public void testPathAnnotationStrippingSimpleArray() Arrays.asList(new ContextPath("/properties/riskFactors{[String]}", false))); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -265,7 +263,7 @@ public void testPathAnnotationStrippingSimpleArray() // Annotations should be stored in FlowContext. Map annotations = flowContext.getValue( - InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); + Constants.InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, Map.class); assertNotNull(annotations); assertEquals(annotations.get("/properties/riskFactors"), "[String]"); } @@ -280,7 +278,7 @@ public void testPathAnnotationStrippingSchemaAnnotation() Arrays.asList(new ContextPath("/properties/items{name: String, count: Integer}", false))); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -291,7 +289,7 @@ public void testPathAnnotationStrippingSchemaAnnotation() assertFalse(op.getPaths().contains("/properties/items{name: String, count: Integer}")); Map annotations = flowContext.getValue( - InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); + Constants.InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, Map.class); assertEquals(annotations.get("/properties/items"), "name: String, count: Integer"); } @@ -305,13 +303,13 @@ public void testPathWithoutAnnotationNotStoredInAnnotationsMap() Arrays.asList(new ContextPath("/properties/riskScore", false))); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); requestBuilder.buildActionExecutionRequest(flowContext, mockReqCtx(accessConfig, null)); // No annotations should be stored when paths have no annotations. Map annotations = flowContext.getValue( - InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); + Constants.InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, Map.class); assertNull(annotations); } @@ -327,7 +325,7 @@ public void testMultipleAnnotatedPaths() new ContextPath("/properties/items{name: String, count: Integer}", false))); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -340,7 +338,7 @@ public void testMultipleAnnotatedPaths() assertTrue(op.getPaths().contains("/properties/items")); Map annotations = flowContext.getValue( - InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, Map.class); + Constants.InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, Map.class); assertEquals(annotations.size(), 2); assertEquals(annotations.get("/properties/riskFactors"), "[String]"); assertEquals(annotations.get("/properties/items"), "name: String, count: Integer"); @@ -364,7 +362,7 @@ public void testModifyPathsDoNotAffectExpose() execCtx.setProperty("riskScore", "50"); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -391,7 +389,7 @@ public void testMultipleModifyPathsProduceSingleReplaceOperation() new ContextPath("/user/claims/http://wso2.org/claims/email", false))); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -418,7 +416,7 @@ public void testExposeFilteringOnlyExposedAreaIncluded() new ContextPath("/flow/flowType", false)), null); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -443,7 +441,7 @@ public void testFlowPortalUrlExposed() new ContextPath("/flow/portalUrl", false)), null); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -465,7 +463,7 @@ public void testFlowPortalUrlNotExposedYieldsNull() new ContextPath("/flow/flowType", false)), null); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -485,7 +483,7 @@ public void testFlowCallbackUrlExposed() new ContextPath("/flow/callbackUrl", false)), null); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -505,7 +503,7 @@ public void testExposeFilteringSpecificClaim() new ContextPath("/user/userId", false)), null); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -542,7 +540,7 @@ public void testPropertiesEncryptedWhenExposePathMarkedEncrypted() .certificateContent("test-cert-pem").build()); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, encryption)); @@ -581,7 +579,7 @@ public void testClaimEncryptedWhenExposePathMarkedEncrypted() .certificateContent("test-cert-pem").build()); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, encryption)); @@ -636,7 +634,7 @@ public void testCredentialEncryptedWhenExposePathMarkedEncrypted() .certificateContent("test-cert-pem").build()); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, encryption)); @@ -674,7 +672,7 @@ public void testEncryptionFailureThrowsException() .certificateContent("test-cert-pem").build()); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); requestBuilder.buildActionExecutionRequest(flowContext, mockReqCtx(accessConfig, encryption)); } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessorTest.java index 2b3876f32269..2801ca5511df 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessorTest.java @@ -37,6 +37,7 @@ import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; import org.wso2.carbon.identity.action.execution.api.model.Success; +import org.wso2.carbon.identity.flow.execution.engine.Constants; import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; @@ -122,7 +123,7 @@ public void testPropertyReplaceFlatExists() throws ActionExecutionResponseProces assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); Map pendingProps = - capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); assertEquals(pendingProps.get("riskScore"), "80"); } @@ -138,7 +139,7 @@ public void testPropertyReplaceCreatesIfMissing() throws ActionExecutionResponse assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); Map pendingProps = - capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); assertEquals(pendingProps.get("riskScore"), "80"); } @@ -156,7 +157,7 @@ public void testPropertyReplaceCoercesToStringByDefault() assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); Map pendingProps = - capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); assertEquals(pendingProps.get("riskScore"), "75"); } @@ -175,7 +176,7 @@ public void testPropertyReplaceWithMultivaluedAnnotation() assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); Map pendingProps = - capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); Object stored = pendingProps.get("riskFactors"); assertTrue(stored instanceof List); @@ -195,7 +196,7 @@ public void testPropertyReplaceMultivaluedSingleValueWrapped() executeSuccessResponse(execCtx, replaceOp, annotations); Map pendingProps = - capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); Object stored = pendingProps.get("tags"); assertTrue(stored instanceof List); @@ -220,7 +221,7 @@ public void testPropertyReplaceWithComplexAnnotation() assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); Map pendingProps = - capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); // Complex annotation → value passed through as-is. Object stored = pendingProps.get("item"); @@ -240,7 +241,7 @@ public void testPropertyReplaceWithPrimaryTypeAnnotation() executeSuccessResponse(execCtx, replaceOp, annotations); Map pendingProps = - capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); assertEquals(pendingProps.get("score"), "95"); } @@ -352,7 +353,7 @@ public void testUserClaimReplace() throws ActionExecutionResponseProcessorExcept executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); Map pendingClaims = - capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_CLAIMS_KEY, Map.class); + capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class); assertNotNull(pendingClaims); assertEquals(pendingClaims.get("http://wso2.org/claims/email"), "new@example.com"); } @@ -367,7 +368,7 @@ public void testUserClaimReplaceCreatesNewClaim() throws ActionExecutionResponse executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); Map pendingClaims = - capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_CLAIMS_KEY, Map.class); + capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class); assertNotNull(pendingClaims); assertEquals(pendingClaims.get("http://wso2.org/claims/country"), "US"); } @@ -383,7 +384,7 @@ public void testUserClaimReplaceStringifiesValue() throws ActionExecutionRespons executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); Map pendingClaims = - capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_CLAIMS_KEY, Map.class); + capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class); assertNotNull(pendingClaims); assertEquals(pendingClaims.get("http://wso2.org/claims/country"), "42"); } @@ -586,7 +587,7 @@ public void testMultipleOperationsMixedResults() throws ActionExecutionResponseP assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); Map pendingProps = - capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); assertEquals(pendingProps.get("newProp"), "newValue"); assertEquals(pendingProps.get("existingProp"), "updated"); @@ -718,7 +719,7 @@ public void testProcessIncompleteResponseWithRedirectStashesUrlAndReturnsIncompl assertEquals(status.getStatus(), ActionExecutionStatus.Status.INCOMPLETE); // URL must be stashed under the key the executor reads. - assertEquals(flowContext.getValue(InFlowExtensionExecutor.PENDING_REDIRECT_URL_KEY, String.class), + assertEquals(flowContext.getValue(Constants.InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, String.class), "https://example.com/step-up"); } @@ -738,11 +739,11 @@ public void testProcessIncompleteResponseIgnoresNonRedirectOperations() assertEquals(status.getStatus(), ActionExecutionStatus.Status.INCOMPLETE); // Redirect URL is captured, but the REPLACE must NOT have produced any pending props. - assertEquals(flowContext.getValue(InFlowExtensionExecutor.PENDING_REDIRECT_URL_KEY, String.class), + assertEquals(flowContext.getValue(Constants.InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, String.class), "https://example.com/step-up"); - assertNull(flowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class)); - assertNull(flowContext.getValue(InFlowExtensionExecutor.PENDING_CLAIMS_KEY, Map.class)); - assertNull(flowContext.getValue(InFlowExtensionExecutor.PENDING_CREDENTIALS_KEY, Map.class)); + assertNull(flowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class)); + assertNull(flowContext.getValue(Constants.InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class)); + assertNull(flowContext.getValue(Constants.InFlowExtensionConstants.PENDING_CREDENTIALS_KEY, Map.class)); } @Test(expectedExceptions = ActionExecutionResponseProcessorException.class) @@ -847,10 +848,10 @@ private ActionExecutionStatus executeSuccessResponse( throws ActionExecutionResponseProcessorException { FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); if (pathTypeAnnotations != null && !pathTypeAnnotations.isEmpty()) { - flowContext.add(InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); + flowContext.add(Constants.InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); } capturedFlowContext = flowContext; @@ -872,13 +873,13 @@ private ActionExecutionStatus executeSuccessResponseWithModifyPaths( throws ActionExecutionResponseProcessorException { FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionExecutor.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); if (pathTypeAnnotations != null && !pathTypeAnnotations.isEmpty()) { - flowContext.add(InFlowExtensionExecutor.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); + flowContext.add(Constants.InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); } if (modifyPaths != null) { - flowContext.add(InFlowExtensionRequestBuilder.MODIFY_PATHS_KEY, modifyPaths); + flowContext.add(Constants.InFlowExtensionConstants.MODIFY_PATHS_KEY, modifyPaths); } capturedFlowContext = flowContext; @@ -932,7 +933,7 @@ public void testNonStringValueForEncryptedPathHandledGracefully() assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); // Value should be coerced to String (default behavior for properties). Map pendingProps = - capturedFlowContext.getValue(InFlowExtensionExecutor.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); assertEquals(pendingProps.get("data"), "42"); } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEventTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEventTest.java index 6ade5c4354e5..b75d09a38400 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEventTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEventTest.java @@ -19,7 +19,6 @@ package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; import org.testng.annotations.Test; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionEvent; import java.util.HashMap; import java.util.Map; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResultTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResultTest.java index ba2ff451f798..698bc66092a4 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResultTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResultTest.java @@ -21,7 +21,6 @@ import org.testng.annotations.Test; import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.OperationExecutionResult; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; From 2aac9a7e1e1d41b00cac8e6560983b52943e57cb Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Wed, 6 May 2026 15:30:45 +0530 Subject: [PATCH 23/41] Fix CodeRabbit review findings (1-7) for In-Flow Extension * Diagnostic Logging: Sanitized external error messages in `ActionExecutionDiagnosticLogger` to prevent potential PII or sensitive data leaks. * Action Management: - Enforced consistent action name uniqueness validation across both create and update operations in `ActionManagementServiceImpl`. - Added null guards to `isActionNameAvailable` methods to prevent `NullPointerException`s. * Response Processing: claim validation has removed - fix irrelevant * DTO Model Resolver: - no need to fix path overlaps since all paths will be terminal. - Refactored certificate deletion to utilize `extractCertificateId()` rather than `.toString()`, ensuring the correct UUID is passed and preventing orphaned certificates. * Access Configuration: no need to match area prefix since all paths will be terminal. --- .../util/ActionExecutionDiagnosticLogger.java | 13 ------------- .../impl/ActionManagementServiceImpl.java | 11 ++++++++++- .../flow/execution/engine/Constants.java | 18 +++++++++++------- .../executor/InFlowExtensionExecutor.java | 18 ++++++++++++------ .../InFlowExtensionActionDTOModelResolver.java | 15 ++++++++------- .../inflow/extension/model/AccessConfig.java | 11 +++++------ .../executor/InFlowExtensionExecutorTest.java | 10 +++++----- .../extension/model/AccessConfigTest.java | 7 ++++--- 8 files changed, 55 insertions(+), 48 deletions(-) diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutionDiagnosticLogger.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutionDiagnosticLogger.java index 5ba214da6887..4e8e142609f3 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutionDiagnosticLogger.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutionDiagnosticLogger.java @@ -327,19 +327,6 @@ private DiagnosticLog.DiagnosticLogBuilder initializeDiagnosticLogBuilder(String return diagLogBuilder; } - public void logActionExecutionError(Action action, String errorMessage) { - - if (!LoggerUtils.isDiagnosticLogsEnabled()) { - return; - } - - DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = initializeDiagnosticLogBuilder( - ActionExecutionLogConstants.ActionIDs.EXECUTE_ACTION, - "Failed to execute " + action.getType().getDisplayName() + " action. " + errorMessage, - DiagnosticLog.ResultStatus.FAILED); - triggerLogEvent(addActionConfigParams(diagnosticLogBuilder, action)); - } - private void triggerLogEvent(DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder) { LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java index 711366bd2969..ed1fe3f64051 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java @@ -166,7 +166,8 @@ public Action updateAction(String actionType, String actionId, Action action, St Action.ActionTypes castedActionType = Action.ActionTypes.valueOf(resolvedActionType); ActionValidatorFactory.getActionValidator(castedActionType).doPreUpdateActionValidations( castedActionType, resolveActionVersionAtUpdating(action, existingActionDTO), action); - if (action.getName() != null) { + if (action.getName() != null && + Action.ActionTypes.IN_FLOW_EXTENSION.equals(castedActionType)) { validateActionNameUniqueness(resolvedActionType, action.getName(), actionId, tenantId); } ActionDTO updatingActionDTO = buildActionDTOForUpdate(resolvedActionType, actionId, action); @@ -183,6 +184,10 @@ public Action updateAction(String actionType, String actionId, Action action, St public boolean isActionNameAvailable(String actionType, String name, String tenantDomain) throws ActionMgtException { + if (name == null) { + throw ActionManagementExceptionHandler.handleClientException( + ErrorMessage.ERROR_INVALID_ACTION_REQUEST_FIELD, "Action name"); + } String resolvedActionType = getActionTypeFromPath(actionType); int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); List actionDTOS = DAO_FACADE.getActionsByActionType(resolvedActionType, tenantId); @@ -194,6 +199,10 @@ public boolean isActionNameAvailable(String actionType, String name, String tena public boolean isActionNameAvailable(String actionType, String name, String excludeActionId, String tenantDomain) throws ActionMgtException { + if (name == null) { + throw ActionManagementExceptionHandler.handleClientException( + ErrorMessage.ERROR_INVALID_ACTION_REQUEST_FIELD, "Action name"); + } String resolvedActionType = getActionTypeFromPath(actionType); int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); List actionDTOS = DAO_FACADE.getActionsByActionType(resolvedActionType, tenantId); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java index 897a305db764..b7df56e13fa7 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java @@ -294,11 +294,15 @@ private InFlowExtensionConstants() { public static final String PENDING_PROPERTIES_KEY = "pendingProperties"; public static final String PENDING_REDIRECT_URL_KEY = "pendingRedirectUrl"; - // ---- Response info keys ---- - public static final String ERROR_TYPE_KEY = "errorType"; - public static final String EXTENSION_ERROR_TYPE = "EXTENSION_ERROR"; - public static final String ERROR_MESSAGE_KEY = "errorMessage"; - public static final String ERROR_DESCRIPTION_KEY = "errorDescription"; + // ---- Response info keys (FAILED path) ---- + public static final String FAILURE_TYPE_KEY = "failureType"; + public static final String IN_FLOW_EXTENSION_FAILURE_TYPE = "IN_FLOW_EXTENSION_FAILURE"; + public static final String FAILURE_MESSAGE_KEY = "failureMessage"; + public static final String FAILURE_DESCRIPTION_KEY = "failureDescription"; + + // ---- Response info keys (ERROR path) ---- + public static final String ERROR_MESSAGE_KEY = "errorMessage"; + public static final String ERROR_DESCRIPTION_KEY = "errorDescription"; // ---- Context path prefixes ---- public static final String PROPERTIES_PATH_PREFIX = "/properties/"; @@ -306,8 +310,8 @@ private InFlowExtensionConstants() { public static final String USER_CREDENTIALS_PATH_PREFIX = "/user/credentials/"; // ---- Miscellaneous ---- - public static final String ACTION_ID_METADATA_KEY = "actionId"; - public static final String EXTENSION_ERROR_CODE = "65033"; + public static final String ACTION_ID_METADATA_KEY = "actionId"; + public static final String IN_FLOW_EXTENSION_ERROR_CODE = "65033"; } public static class SQLConstants { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java index 38faa8d648c3..735be90dfafe 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java @@ -102,6 +102,7 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE .resultStatus(DiagnosticLog.ResultStatus.FAILED)); } response.setResult(ExecutorStatus.STATUS_ERROR); + response.setErrorCode(InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); return response; } @@ -130,6 +131,7 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE .resultStatus(DiagnosticLog.ResultStatus.FAILED)); } response.setResult(ExecutorStatus.STATUS_ERROR); + response.setErrorCode(InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); return response; } @@ -181,12 +183,12 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE if (additionalInfo == null) { additionalInfo = new HashMap<>(); } - additionalInfo.put(InFlowExtensionConstants.ERROR_TYPE_KEY, - InFlowExtensionConstants.EXTENSION_ERROR_TYPE); + additionalInfo.put(InFlowExtensionConstants.FAILURE_TYPE_KEY, + InFlowExtensionConstants.IN_FLOW_EXTENSION_FAILURE_TYPE); executionResponse.setAdditionalInfo(additionalInfo); if (LOG.isDebugEnabled()) { LOG.debug("In-Flow Extension action returned FAILED. actionId: " + actionId - + ", reason: " + additionalInfo.get(InFlowExtensionConstants.ERROR_MESSAGE_KEY)); + + ", reason: " + additionalInfo.get(InFlowExtensionConstants.FAILURE_MESSAGE_KEY)); } } @@ -205,6 +207,7 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE .resultStatus(DiagnosticLog.ResultStatus.FAILED)); } response.setResult(ExecutorStatus.STATUS_ERROR); + response.setErrorCode(InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); response.setErrorMessage("An error occurred while processing the extension. Please try again."); return response; } @@ -239,6 +242,7 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt if (executionStatus == null) { response.setResult(ExecutorStatus.STATUS_ERROR); + response.setErrorCode(InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); return response; } @@ -253,11 +257,11 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt if (failure != null) { Map failureInfo = new HashMap<>(); if (failure.getFailureReason() != null) { - failureInfo.put(InFlowExtensionConstants.ERROR_MESSAGE_KEY, + failureInfo.put(InFlowExtensionConstants.FAILURE_MESSAGE_KEY, failure.getFailureReason()); } if (failure.getFailureDescription() != null) { - failureInfo.put(InFlowExtensionConstants.ERROR_DESCRIPTION_KEY, + failureInfo.put(InFlowExtensionConstants.FAILURE_DESCRIPTION_KEY, failure.getFailureDescription()); } response.setAdditionalInfo(failureInfo); @@ -268,7 +272,7 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt case ERROR: response.setResult(ExecutorStatus.STATUS_ERROR); Error error = (Error) executionStatus.getResponse(); - response.setErrorCode(InFlowExtensionConstants.EXTENSION_ERROR_CODE); + response.setErrorCode(InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); if (error != null) { Map errorInfo = new HashMap<>(); if (error.getErrorMessage() != null) { @@ -300,6 +304,7 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt .resultStatus(DiagnosticLog.ResultStatus.FAILED)); } response.setResult(ExecutorStatus.STATUS_ERROR); + response.setErrorCode(InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); response.setErrorMessage("Extension returned INCOMPLETE without a redirect URL."); break; } @@ -336,6 +341,7 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt default: LOG.warn("Unknown execution status: " + executionStatus.getStatus()); response.setResult(ExecutorStatus.STATUS_ERROR); + response.setErrorCode(InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); } return response; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java index 670fcc550004..9175fa8972db 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java @@ -329,14 +329,11 @@ private List validateExpose(Object exposeValue) throws ActionDTOMod List result = new ArrayList<>(); for (Object item : exposeList) { - if (item instanceof String) { - // Simple string path — no encryption. - result.add(new ContextPath((String) item, false)); - } else if (item instanceof Map) { + if (item instanceof Map) { Map map = (Map) item; if (!map.containsKey("path") || !(map.get("path") instanceof String)) { throw new ActionDTOModelResolverClientException("Invalid expose format.", - "Each expose entry must be a string or an object with a 'path' field."); + "Each expose entry must be an object with a 'path' field."); } String path = (String) map.get("path"); boolean encrypted = map.containsKey("encrypted") && toBooleanSafe(map.get("encrypted")); @@ -345,7 +342,7 @@ private List validateExpose(Object exposeValue) throws ActionDTOMod result.add((ContextPath) item); } else { throw new ActionDTOModelResolverClientException("Invalid expose format.", - "Each expose entry must be a string or an object with 'path' and optional 'encrypted' fields."); + "Each expose entry must be an object with 'path' and optional 'encrypted' fields."); } } @@ -376,6 +373,10 @@ private void validateContextPathFormat(List expose) throws ActionDT throw new ActionDTOModelResolverClientException("Invalid expose path.", String.format("Expose path '%s' must start with '/'.", path)); } + if (path.endsWith("/")) { + throw new ActionDTOModelResolverClientException("Invalid expose path.", + String.format("Expose path '%s' must not end with a trailing '/'.", path)); + } if (!seen.add(path)) { throw new ActionDTOModelResolverClientException("Duplicate expose path.", String.format("The expose path '%s' is duplicated.", path)); @@ -500,7 +501,7 @@ private void handleCertificateDelete(ActionDTO deletingActionDTO, String tenantD } try { - String certId = certIdValue.toString(); + String certId = extractCertificateId(certIdValue); certificateManagementService.deleteCertificate(certId, tenantDomain); } catch (CertificateMgtException e) { throw new ActionDTOModelResolverException("Error deleting certificate for action: " diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java index 887d0d379075..6acc16408129 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java @@ -133,20 +133,19 @@ public boolean isExposePathEncrypted(String path) { /** * Check if a given modify path has inbound encryption enabled. - * Matches the most specific (longest) modify path that is a prefix of the given path. + * Matches the modify entry whose path (after stripping type annotations) exactly equals the given path. * - * @param pathPrefix The path to check (clean, without annotations). + * @param path The terminal path to check (clean, without annotations). * @return {@code true} if the matching modify entry has {@code encrypted = true}. */ - public boolean isModifyPathEncrypted(String pathPrefix) { + public boolean isModifyPathEncrypted(String path) { if (modify == null) { return false; } return modify.stream() - .filter(mp -> pathPrefix.startsWith(PathTypeAnnotationUtil.stripAnnotation(mp.getPath())[0])) - .reduce((a, b) -> PathTypeAnnotationUtil.stripAnnotation(a.getPath())[0].length() - >= PathTypeAnnotationUtil.stripAnnotation(b.getPath())[0].length() ? a : b) + .filter(mp -> path.equals(PathTypeAnnotationUtil.stripAnnotation(mp.getPath())[0])) + .findFirst() .map(ContextPath::isEncrypted) .orElse(false); } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java index 739bab750f87..fee305e78731 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java @@ -223,9 +223,9 @@ public void testExecuteFailed() throws Exception { assertEquals(response.getResult(), ExecutorStatus.STATUS_RETRY); assertEquals(response.getErrorMessage(), "Risk score exceeds threshold"); - // Verify errorType metadata is set for RETRY. + // Verify failureType metadata is set for RETRY. assertNotNull(response.getAdditionalInfo()); - assertEquals(response.getAdditionalInfo().get("errorType"), "EXTENSION_ERROR"); + assertEquals(response.getAdditionalInfo().get("failureType"), "IN_FLOW_EXTENSION_FAILURE"); } @Test @@ -304,7 +304,7 @@ public void testExecuteError() throws Exception { ExecutorResponse response = executor.execute(context); assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); - assertEquals(response.getErrorCode(), Constants.InFlowExtensionConstants.EXTENSION_ERROR_CODE); + assertEquals(response.getErrorCode(), Constants.InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); // errorMessage carries the Error reason/code field; errorDescription carries the human-readable text. assertEquals(response.getErrorMessage(), "internal_error"); assertEquals(response.getErrorDescription(), "DB connection failed"); @@ -332,7 +332,7 @@ public void testExecuteErrorNoDescription() throws Exception { ExecutorResponse response = executor.execute(context); assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); - assertEquals(response.getErrorCode(), Constants.InFlowExtensionConstants.EXTENSION_ERROR_CODE); + assertEquals(response.getErrorCode(), Constants.InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); assertEquals(response.getErrorMessage(), "internal_error"); assertNull(response.getErrorDescription()); } @@ -359,7 +359,7 @@ public void testExecuteErrorBothNull() throws Exception { ExecutorResponse response = executor.execute(context); assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); - assertEquals(response.getErrorCode(), Constants.InFlowExtensionConstants.EXTENSION_ERROR_CODE); + assertEquals(response.getErrorCode(), Constants.InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); // Both fields null → errorMessage and errorDescription remain null; errorCode alone triggers FE-65033 routing. assertNull(response.getErrorMessage()); assertNull(response.getErrorDescription()); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfigTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfigTest.java index e60fbc2b3180..896fe46a124b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfigTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfigTest.java @@ -121,16 +121,17 @@ public void testIsExposePathEncryptedWithNullExpose() { } @Test - public void testIsModifyPathEncryptedMatchesLongestPrefix() { + public void testIsModifyPathEncryptedMatchesExactPath() { List modifyPaths = Arrays.asList( - new ContextPath("/user/", false), - new ContextPath("/user/credentials/", true) + new ContextPath("/user/username", false), + new ContextPath("/user/credentials/password", true) ); AccessConfig config = new AccessConfig(null, modifyPaths); assertTrue(config.isModifyPathEncrypted("/user/credentials/password")); assertFalse(config.isModifyPathEncrypted("/user/username")); + assertFalse(config.isModifyPathEncrypted("/user/credentials/other")); } @Test From 261eff1a75eadde89275ed40d44d75d31d9b753c Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Fri, 8 May 2026 10:51:05 +0530 Subject: [PATCH 24/41] Moved inflow extensions to a new module --- .../pom.xml | 27 - .../flow/execution/engine/Constants.java | 45 +- .../FlowExecutionEngineDataHolder.java | 92 +-- .../FlowExecutionEngineServiceComponent.java | 104 +-- .../src/test/resources/testng.xml | 22 - .../pom.xml | 291 ++++++++ .../extensions/InFlowExtensionConstants.java | 57 ++ .../config/FlowContextHandoverConfig.java | 2 +- .../config/FlowContextHandoverPolicy.java | 2 +- .../config/FlowExecutionContextFilter.java | 2 +- .../executor/HierarchicalPrefixMatcher.java | 2 +- .../executor/InFlowExtensionExecutor.java | 53 +- .../executor/InFlowExtensionLogConstants.java | 2 +- .../InFlowExtensionRequestBuilder.java | 61 +- .../InFlowExtensionResponseProcessor.java | 10 +- .../executor/JWEEncryptionUtil.java | 2 +- .../executor/PathTypeAnnotationUtil.java | 2 +- .../internal/InFlowExtensionDataHolder.java | 99 +++ .../InFlowExtensionServiceComponent.java | 144 ++++ .../InFlowExtensionActionConstants.java | 2 +- .../InFlowExtensionActionConverter.java | 22 +- ...InFlowExtensionActionDTOModelResolver.java | 20 +- .../InFlowExtensionContextTreeBuilder.java | 6 +- .../InFlowExtensionContextTreeMetadata.java | 2 +- .../InFlowExtensionContextTreeNode.java | 2 +- .../InFlowExtensionContextTreeService.java | 4 +- .../extensions}/model/AccessConfig.java | 4 +- .../inflow/extensions}/model/ContextPath.java | 2 +- .../inflow/extensions}/model/Encryption.java | 2 +- .../model/InFlowExtensionAction.java | 2 +- .../model/InFlowExtensionEvent.java | 2 +- .../model/InFlowExtensionRequest.java | 34 + .../model/OperationExecutionResult.java | 2 +- .../config/FlowContextHandoverPolicyTest.java | 2 +- .../FlowExecutionContextFilterTest.java | 2 +- .../HierarchicalPrefixMatcherTest.java | 2 +- .../executor/InFlowExtensionExecutorTest.java | 42 +- .../InFlowExtensionRequestBuilderTest.java | 57 +- .../InFlowExtensionResponseProcessorTest.java | 71 +- .../executor/PathTypeAnnotationUtilTest.java | 2 +- .../extensions}/model/AccessConfigTest.java | 2 +- .../model/InFlowExtensionEventTest.java | 2 +- .../model/OperationExecutionResultTest.java | 2 +- .../test/resources/repository/conf/carbon.xml | 687 ++++++++++++++++++ .../src/test/resources/testng.xml | 44 ++ .../flow-orchestration-framework/pom.xml | 1 + .../pom.xml | 73 ++ pom.xml | 11 + 48 files changed, 1666 insertions(+), 459 deletions(-) create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/config/FlowContextHandoverConfig.java (98%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/config/FlowContextHandoverPolicy.java (99%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/config/FlowExecutionContextFilter.java (98%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/executor/HierarchicalPrefixMatcher.java (98%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/executor/InFlowExtensionExecutor.java (88%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/executor/InFlowExtensionLogConstants.java (94%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/executor/InFlowExtensionRequestBuilder.java (88%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/executor/InFlowExtensionResponseProcessor.java (98%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/executor/JWEEncryptionUtil.java (99%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/executor/PathTypeAnnotationUtil.java (99%) create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionDataHolder.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionServiceComponent.java rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/management/InFlowExtensionActionConstants.java (97%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/management/InFlowExtensionActionConverter.java (86%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/management/InFlowExtensionActionDTOModelResolver.java (95%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/metadata/InFlowExtensionContextTreeBuilder.java (97%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/metadata/InFlowExtensionContextTreeMetadata.java (96%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/metadata/InFlowExtensionContextTreeNode.java (98%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/metadata/InFlowExtensionContextTreeService.java (93%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/model/AccessConfig.java (96%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/model/ContextPath.java (96%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/model/Encryption.java (96%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/model/InFlowExtensionAction.java (99%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/model/InFlowExtensionEvent.java (98%) create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionRequest.java rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions}/model/OperationExecutionResult.java (95%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions}/config/FlowContextHandoverPolicyTest.java (97%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions}/config/FlowExecutionContextFilterTest.java (99%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions}/executor/HierarchicalPrefixMatcherTest.java (98%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions}/executor/InFlowExtensionExecutorTest.java (94%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions}/executor/InFlowExtensionRequestBuilderTest.java (92%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions}/executor/InFlowExtensionResponseProcessorTest.java (91%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions}/executor/PathTypeAnnotationUtilTest.java (99%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions}/model/AccessConfigTest.java (98%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions}/model/InFlowExtensionEventTest.java (97%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension => org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions}/model/OperationExecutionResultTest.java (96%) create mode 100755 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/repository/conf/carbon.xml create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/testng.xml create mode 100644 features/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions.server.feature/pom.xml diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml index 9a8cf09a777a..eb43f6da7fc5 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml @@ -66,14 +66,6 @@ org.wso2.carbon.identity.framework org.wso2.carbon.identity.flow.mgt
      - - org.wso2.carbon.identity.framework - org.wso2.carbon.identity.action.execution - - - org.wso2.carbon.identity.framework - org.wso2.carbon.identity.action.management - org.wso2.carbon.identity.framework org.wso2.carbon.identity.claim.metadata.mgt @@ -111,10 +103,6 @@ org.wso2.carbon.identity.framework org.wso2.carbon.identity.user.action - - org.wso2.carbon.identity.framework - org.wso2.carbon.identity.certificate.management - @@ -178,18 +166,8 @@ version="${carbon.identity.package.import.version.range}", org.wso2.carbon.core.util; version="${carbon.kernel.package.import.version.range}", org.wso2.carbon.identity.event.*; version="${carbon.identity.package.import.version.range}", - org.wso2.carbon.identity.action.execution.api.*; - version="${carbon.identity.package.import.version.range}", - org.wso2.carbon.identity.action.management.api.*; - version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.central.log.mgt.utils; version="${carbon.identity.package.import.version.range}", - org.wso2.carbon.identity.certificate.management.exception; - version="${carbon.identity.package.import.version.range}", - org.wso2.carbon.identity.certificate.management.model; - version="${carbon.identity.package.import.version.range}", - org.wso2.carbon.identity.certificate.management.service; - version="${carbon.identity.package.import.version.range}", com.fasterxml.jackson.core.*; version="${com.fasterxml.jackson.annotation.version.range}", com.fasterxml.jackson.databind.*; version="${com.fasterxml.jackson.annotation.version.range}", @@ -210,11 +188,6 @@ org.wso2.carbon.identity.flow.execution.engine.listener, org.wso2.carbon.identity.flow.execution.engine.validation, org.wso2.carbon.identity.flow.execution.engine.exception, - org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor, - org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model, - org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management, - org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config, - org.wso2.carbon.identity.flow.execution.engine.inflow.extension.metadata, org.wso2.carbon.identity.flow.execution.engine.graph; version="${carbon.identity.package.export.version}" diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java index b7df56e13fa7..9dbf462c7df9 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java @@ -163,7 +163,7 @@ public enum ErrorMessages { "Error while loading claim metadata.", "Error occurred loading the claim metadata for tenant: %s."), ERROR_CODE_INFLOW_EXTENSION_ERROR("65033", - "InFlow extension reported an error.", + "%s", "%s"), @@ -271,49 +271,6 @@ private ExecutorStatus() { } } - /** - * Constants for the In-Flow Extension executor pipeline. - * - *

      Keys are shared across the executor, request builder, and response processor - * via the {@link org.wso2.carbon.identity.action.execution.api.model.FlowContext} - * handoff mechanism. Path prefixes drive operation routing in the response processor.

      - */ - public static class InFlowExtensionConstants { - - private InFlowExtensionConstants() { - - } - - // ---- FlowContext pipeline keys ---- - public static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; - public static final String HANDOVER_POLICY_KEY = "handoverPolicy"; - public static final String PATH_TYPE_ANNOTATIONS_KEY = "pathTypeAnnotations"; - public static final String MODIFY_PATHS_KEY = "modifyPaths"; - public static final String PENDING_CLAIMS_KEY = "pendingClaims"; - public static final String PENDING_CREDENTIALS_KEY = "pendingCredentials"; - public static final String PENDING_PROPERTIES_KEY = "pendingProperties"; - public static final String PENDING_REDIRECT_URL_KEY = "pendingRedirectUrl"; - - // ---- Response info keys (FAILED path) ---- - public static final String FAILURE_TYPE_KEY = "failureType"; - public static final String IN_FLOW_EXTENSION_FAILURE_TYPE = "IN_FLOW_EXTENSION_FAILURE"; - public static final String FAILURE_MESSAGE_KEY = "failureMessage"; - public static final String FAILURE_DESCRIPTION_KEY = "failureDescription"; - - // ---- Response info keys (ERROR path) ---- - public static final String ERROR_MESSAGE_KEY = "errorMessage"; - public static final String ERROR_DESCRIPTION_KEY = "errorDescription"; - - // ---- Context path prefixes ---- - public static final String PROPERTIES_PATH_PREFIX = "/properties/"; - public static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; - public static final String USER_CREDENTIALS_PATH_PREFIX = "/user/credentials/"; - - // ---- Miscellaneous ---- - public static final String ACTION_ID_METADATA_KEY = "actionId"; - public static final String IN_FLOW_EXTENSION_ERROR_CODE = "65033"; - } - public static class SQLConstants { private SQLConstants() { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java index 28be71d9acf8..7a2a5fc6cf4e 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java @@ -18,14 +18,10 @@ package org.wso2.carbon.identity.flow.execution.engine.internal; -import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; -import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; import org.wso2.carbon.identity.application.mgt.ApplicationManagementService; -import org.wso2.carbon.identity.certificate.management.service.CertificateManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverConfig; import org.wso2.carbon.identity.flow.execution.engine.listener.FlowExecutionListener; import org.wso2.carbon.identity.flow.mgt.FlowMgtService; import org.wso2.carbon.identity.input.validation.mgt.services.InputValidationManagementService; @@ -51,10 +47,6 @@ public class FlowExecutionEngineDataHolder { private ApplicationManagementService applicationManagementService; private FederatedAssociationManager federatedAssociationManager; private IdentityEventService identityEventService; - private ActionExecutorService actionExecutorService; - private ActionManagementService actionManagementService; - private CertificateManagementService certificateManagementService; - private FlowContextHandoverConfig flowContextHandoverConfig; private List flowExecutionListeners = new ArrayList<>(); private FlowExecutionEngineDataHolder() { @@ -217,6 +209,7 @@ public void setFederatedAssociationManager(FederatedAssociationManager federated this.federatedAssociationManager = federatedAssociationManager; } + public IdentityEventService getIdentityEventService() { return identityEventService; @@ -226,87 +219,4 @@ public void setIdentityEventService(IdentityEventService identityEventService) { this.identityEventService = identityEventService; } - - /** - * Get the ActionExecutorService instance. - * - * @return ActionExecutorService instance. - */ - public ActionExecutorService getActionExecutorService() { - - return actionExecutorService; - } - - /** - * Set the ActionExecutorService instance. - * - * @param actionExecutorService ActionExecutorService instance. - */ - public void setActionExecutorService(ActionExecutorService actionExecutorService) { - - this.actionExecutorService = actionExecutorService; - } - - /** - * Get the ActionManagementService instance. - * - * @return ActionManagementService instance. - */ - public ActionManagementService getActionManagementService() { - - return actionManagementService; - } - - /** - * Set the ActionManagementService instance. - * - * @param actionManagementService ActionManagementService instance. - */ - public void setActionManagementService(ActionManagementService actionManagementService) { - - this.actionManagementService = actionManagementService; - } - - /** - * Get the CertificateManagementService instance. - * - * @return CertificateManagementService instance. - */ - public CertificateManagementService getCertificateManagementService() { - - return certificateManagementService; - } - - /** - * Set the CertificateManagementService instance. - * - * @param certificateManagementService CertificateManagementService instance. - */ - public void setCertificateManagementService(CertificateManagementService certificateManagementService) { - - this.certificateManagementService = certificateManagementService; - } - - /** - * Get the In-Flow Extension context handover config. Lazily initialised on first access - * (the constructor reads from {@code IdentityUtil} which requires the carbon configuration - * to be loaded — keeping this lazy avoids ordering issues with OSGi component activation). - * - * @return The handover config, never null. - */ - public synchronized FlowContextHandoverConfig getFlowContextHandoverConfig() { - - if (flowContextHandoverConfig == null) { - flowContextHandoverConfig = new FlowContextHandoverConfig(); - } - return flowContextHandoverConfig; - } - - /** - * Override the handover config. Intended for tests only. - */ - public synchronized void setFlowContextHandoverConfig(FlowContextHandoverConfig flowContextHandoverConfig) { - - this.flowContextHandoverConfig = flowContextHandoverConfig; - } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java index 315519e53fc2..ff73956f2e09 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java @@ -28,23 +28,11 @@ import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; -import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionRequestBuilder; -import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionResponseProcessor; -import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; -import org.wso2.carbon.identity.action.management.api.service.ActionConverter; -import org.wso2.carbon.identity.action.management.api.service.ActionDTOModelResolver; -import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; import org.wso2.carbon.identity.application.mgt.ApplicationManagementService; -import org.wso2.carbon.identity.certificate.management.service.CertificateManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.flow.execution.engine.FlowExecutionService; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionExecutor; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionRequestBuilder; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.InFlowExtensionResponseProcessor; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConverter; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionDTOModelResolver; import org.wso2.carbon.identity.flow.execution.engine.listener.FlowExecutionListener; import org.wso2.carbon.identity.flow.execution.engine.validation.InputProcessingListener; import org.wso2.carbon.identity.flow.mgt.FlowMgtService; @@ -92,19 +80,6 @@ protected void activate(ComponentContext context) { bundleContext.registerService(FlowExecutionListener.class.getName(), new InputProcessingListener(), null); - bundleContext.registerService(Executor.class.getName(), new InFlowExtensionExecutor(), null); - bundleContext.registerService(ActionExecutionRequestBuilder.class.getName(), - new InFlowExtensionRequestBuilder(), null); - bundleContext.registerService(ActionExecutionResponseProcessor.class.getName(), - new InFlowExtensionResponseProcessor(), null); - - bundleContext.registerService(ActionConverter.class.getName(), - new InFlowExtensionActionConverter(), null); - bundleContext.registerService(ActionDTOModelResolver.class.getName(), - new InFlowExtensionActionDTOModelResolver( - FlowExecutionEngineDataHolder.getInstance().getCertificateManagementService()), - null); - LOG.debug("Flow Engine service successfully activated."); } catch (Throwable e) { LOG.error("Error while initiating Flow Engine service", e); @@ -268,6 +243,7 @@ protected void unsetClaimMetadataManagementService(ClaimMetadataManagementServic FlowExecutionEngineDataHolder.getInstance().setClaimMetadataManagementService(null); } + @Reference( name = "identity.event.service", service = IdentityEventService.class, @@ -284,82 +260,4 @@ protected void unsetIdentityEventService(IdentityEventService identityEventServi FlowExecutionEngineDataHolder.getInstance().setIdentityEventService(null); } - - @Reference( - name = "ActionExecutorService", - service = ActionExecutorService.class, - cardinality = ReferenceCardinality.MANDATORY, - policy = ReferencePolicy.DYNAMIC, - unbind = "unsetActionExecutorService" - ) - protected void setActionExecutorService(ActionExecutorService actionExecutorService) { - - LOG.debug("Setting the ActionExecutorService in the Flow Engine component."); - FlowExecutionEngineDataHolder.getInstance().setActionExecutorService(actionExecutorService); - } - - protected void unsetActionExecutorService(ActionExecutorService actionExecutorService) { - - LOG.debug("Unsetting the ActionExecutorService in the Flow Engine component."); - FlowExecutionEngineDataHolder.getInstance().setActionExecutorService(null); - } - - @Reference( - name = "ActionManagementService", - service = ActionManagementService.class, - cardinality = ReferenceCardinality.MANDATORY, - policy = ReferencePolicy.DYNAMIC, - unbind = "unsetActionManagementService" - ) - protected void setActionManagementService(ActionManagementService actionManagementService) { - - LOG.debug("Setting the ActionManagementService in the Flow Engine component."); - FlowExecutionEngineDataHolder.getInstance().setActionManagementService(actionManagementService); - } - - protected void unsetActionManagementService(ActionManagementService actionManagementService) { - - LOG.debug("Unsetting the ActionManagementService in the Flow Engine component."); - FlowExecutionEngineDataHolder.getInstance().setActionManagementService(null); - } - - @Reference( - name = "ActionExecutorService", - service = ActionExecutorService.class, - cardinality = ReferenceCardinality.MANDATORY, - policy = ReferencePolicy.DYNAMIC, - unbind = "unsetActionExecutorService" - ) - protected void setActionExecutorService(ActionExecutorService actionExecutorService) { - - LOG.debug("Setting the ActionExecutorService in the Flow Engine component."); - FlowExecutionEngineDataHolder.getInstance().setActionExecutorService(actionExecutorService); - } - - protected void unsetActionExecutorService(ActionExecutorService actionExecutorService) { - - LOG.debug("Unsetting the ActionExecutorService in the Flow Engine component."); - FlowExecutionEngineDataHolder.getInstance().setActionExecutorService(null); - } - - @Reference( - name = "CertificateManagementService", - service = CertificateManagementService.class, - cardinality = ReferenceCardinality.MANDATORY, - policy = ReferencePolicy.DYNAMIC, - unbind = "unsetCertificateManagementService" - ) - protected void setCertificateManagementService(CertificateManagementService certificateManagementService) { - - LOG.debug("Setting the CertificateManagementService in the Flow Engine component."); - FlowExecutionEngineDataHolder.getInstance() - .setCertificateManagementService(certificateManagementService); - } - - protected void unsetCertificateManagementService( - CertificateManagementService certificateManagementService) { - - LOG.debug("Unsetting the CertificateManagementService in the Flow Engine component."); - FlowExecutionEngineDataHolder.getInstance().setCertificateManagementService(null); - } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml index f583dba3beaf..e76f17cabd37 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml @@ -30,26 +30,4 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml new file mode 100644 index 000000000000..5937b4269f80 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml @@ -0,0 +1,291 @@ + + + + + org.wso2.carbon.identity.framework + identity-framework + 7.11.70-SNAPSHOT + ../../../pom.xml + + + 4.0.0 + org.wso2.carbon.identity.flow.inflow.extensions + bundle + WSO2 Carbon - Identity Flow In-Flow Extensions + WSO2 flow engine in-flow extensions + http://www.wso2.com + + + + org.wso2.carbon + org.wso2.carbon.core + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.base + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.core + + + org.ops4j.pax.logging + pax-logging-api + + + com.fasterxml.jackson.core + jackson-databind + provided + + + com.fasterxml.jackson.core + jackson-core + provided + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.flow.mgt + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.flow.execution.engine + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.action.execution + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.action.management + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.claim.metadata.mgt + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.central.log.mgt + + + org.testng + testng + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-testng + test + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.testutil + test + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.user.action + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.certificate.management + + + org.wso2.orbit.com.nimbusds + nimbus-jose-jwt + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + ${project.artifactId} + ${project.artifactId} + + org.wso2.carbon.identity.flow.inflow.extensions.internal + + + javax.xml.parsers; version="${javax.xml.parsers.import.pkg.version}", + org.xml.sax, + org.w3c.dom, + org.osgi.framework; version="${osgi.framework.imp.pkg.version.range}", + org.apache.axiom.om; version="${axiom.osgi.version.range}", + org.apache.xerces.util; resolution:=optional, + org.apache.commons.logging; version="${import.package.version.commons.logging}", + org.apache.commons.collections; version="${commons-collections.wso2.osgi.version.range}", + org.osgi.service.component; version="${osgi.service.component.imp.pkg.version.range}", + org.wso2.carbon.context; version="${carbon.kernel.package.import.version.range}", + org.wso2.carbon.user.core.*;version="${carbon.kernel.package.import.version.range}", + org.wso2.carbon.utils.multitenancy;version="${carbon.kernel.package.import.version.range}", + org.wso2.carbon.user.api; version="${carbon.user.api.imp.pkg.version.range}", + org.wso2.carbon.identity.base; version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.core.*; version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.application.*; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.claim.metadata.mgt.*; + version="${carbon.identity.package.import.version.range}", + javax.servlet.http; version="${imp.pkg.version.javax.servlet}", + org.wso2.carbon.identity.flow.execution.engine; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.flow.execution.engine.exception; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.flow.execution.engine.graph; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.flow.execution.engine.model; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.flow.mgt; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.flow.mgt.model; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.flow.mgt.exception; + version="${carbon.identity.package.import.version.range}", + com.nimbusds.jose.*; version="${nimbusds.osgi.version.range}", + com.nimbusds.jwt; version="${nimbusds.osgi.version.range}", + org.wso2.carbon.identity.user.action.api.exception; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.core.util; version="${carbon.kernel.package.import.version.range}", + org.wso2.carbon.identity.action.execution.api.*; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.action.management.api.*; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.central.log.mgt.utils; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.certificate.management.exception; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.certificate.management.model; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.certificate.management.service; + version="${carbon.identity.package.import.version.range}", + com.fasterxml.jackson.core.*; version="${com.fasterxml.jackson.annotation.version.range}", + com.fasterxml.jackson.databind.*; + version="${com.fasterxml.jackson.annotation.version.range}", + 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.wso2.carbon.identity.flow.inflow.extensions.internal, + org.wso2.carbon.identity.flow.inflow.extensions, + org.wso2.carbon.identity.flow.inflow.extensions.executor, + org.wso2.carbon.identity.flow.inflow.extensions.model, + org.wso2.carbon.identity.flow.inflow.extensions.management, + org.wso2.carbon.identity.flow.inflow.extensions.config, + org.wso2.carbon.identity.flow.inflow.extensions.metadata; + version="${carbon.identity.package.export.version}" + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.plugin.version} + + + src/test/resources/testng.xml + + + ${project.build.testOutputDirectory} + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + true + + + + org.codehaus.mojo + findbugs-maven-plugin + + true + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + org/wso2/carbon/identity/flow/inflow/extensions/internal/** + + + + + default-prepare-agent + + prepare-agent + + + + default-prepare-agent-integration + + prepare-agent-integration + + + + default-report + + report + + + + default-report-integration + + report-integration + + + + default-check + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + 0.45 + + + + + + + + + + + + diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java new file mode 100644 index 000000000000..273a9f9dd122 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * 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.inflow.extensions; + +/** + * Constants for the In-Flow Extension executor pipeline. + * + *

      Keys are shared across the executor, request builder, and response processor + * via the {@link org.wso2.carbon.identity.action.execution.api.model.FlowContext} + * handoff mechanism. Path prefixes drive operation routing in the response processor.

      + */ +public class InFlowExtensionConstants { + + private InFlowExtensionConstants() { + + } + + // ---- FlowContext pipeline keys ---- + public static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; + public static final String HANDOVER_POLICY_KEY = "handoverPolicy"; + public static final String PATH_TYPE_ANNOTATIONS_KEY = "pathTypeAnnotations"; + public static final String MODIFY_PATHS_KEY = "modifyPaths"; + public static final String PENDING_CLAIMS_KEY = "pendingClaims"; + public static final String PENDING_CREDENTIALS_KEY = "pendingCredentials"; + public static final String PENDING_PROPERTIES_KEY = "pendingProperties"; + public static final String PENDING_REDIRECT_URL_KEY = "pendingRedirectUrl"; + + // ---- Response info keys (FAILED path) ---- + public static final String FAILURE_TYPE_KEY = "failureType"; + public static final String IN_FLOW_EXTENSION_FAILURE_TYPE = "IN_FLOW_EXTENSION_FAILURE"; + public static final String FAILURE_MESSAGE_KEY = "failureMessage"; + public static final String FAILURE_DESCRIPTION_KEY = "failureDescription"; + + // ---- Context path prefixes ---- + public static final String PROPERTIES_PATH_PREFIX = "/properties/"; + public static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; + public static final String USER_CREDENTIALS_PATH_PREFIX = "/user/credentials/"; + + // ---- Miscellaneous ---- + public static final String ACTION_ID_METADATA_KEY = "actionId"; +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverConfig.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverConfig.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverConfig.java index 9af590a470ca..309ee1afc86d 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverConfig.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverConfig.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config; +package org.wso2.carbon.identity.flow.inflow.extensions.config; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverPolicy.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverPolicy.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverPolicy.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverPolicy.java index e97cfe448942..4c5b285532d3 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverPolicy.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverPolicy.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config; +package org.wso2.carbon.identity.flow.inflow.extensions.config; /** * Immutable per-flow-type policy controlling which {@code FlowExecutionContext} fields the diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowExecutionContextFilter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowExecutionContextFilter.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowExecutionContextFilter.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowExecutionContextFilter.java index 80978d0ec625..9acc4c6c5890 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowExecutionContextFilter.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowExecutionContextFilter.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config; +package org.wso2.carbon.identity.flow.inflow.extensions.config; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcher.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcher.java index 5496312061eb..dd7221c54f27 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcher.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcher.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; +package org.wso2.carbon.identity.flow.inflow.extensions.executor; import java.util.List; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java similarity index 88% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java index 735be90dfafe..8583067b5ce6 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; +package org.wso2.carbon.identity.flow.inflow.extensions.executor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -33,12 +33,12 @@ import org.wso2.carbon.identity.flow.execution.engine.Constants; import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; import org.wso2.carbon.utils.DiagnosticLog; -import org.wso2.carbon.identity.flow.execution.engine.Constants.InFlowExtensionConstants; +import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; import org.wso2.carbon.identity.flow.execution.engine.exception.FlowEngineException; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverConfig; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverPolicy; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowExecutionContextFilter; -import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; +import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverPolicy; +import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowExecutionContextFilter; +import org.wso2.carbon.identity.flow.inflow.extensions.internal.InFlowExtensionDataHolder; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; @@ -102,7 +102,10 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE .resultStatus(DiagnosticLog.ResultStatus.FAILED)); } response.setResult(ExecutorStatus.STATUS_ERROR); - response.setErrorCode(InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); + response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); + response.setErrorMessage("Extension is not configured."); + response.setErrorDescription("The In-Flow Extension action is missing required configuration. " + + "Contact your administrator."); return response; } @@ -131,14 +134,16 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE .resultStatus(DiagnosticLog.ResultStatus.FAILED)); } response.setResult(ExecutorStatus.STATUS_ERROR); - response.setErrorCode(InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); + response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); + response.setErrorMessage("Extension execution is disabled."); + response.setErrorDescription("The In-Flow Extension action type is currently disabled on this server."); return response; } try { // Resolve the per-flow-type handover policy and hand the action framework only a // FILTERED copy of the FlowExecutionContext (non-whitelisted fields nulled out). - FlowContextHandoverConfig handoverConfig = FlowExecutionEngineDataHolder.getInstance() + FlowContextHandoverConfig handoverConfig = InFlowExtensionDataHolder.getInstance() .getFlowContextHandoverConfig(); FlowContextHandoverPolicy policy = handoverConfig.resolve(context.getFlowType()); FlowExecutionContext filteredContext = FlowExecutionContextFilter.filter(context, policy); @@ -207,8 +212,10 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE .resultStatus(DiagnosticLog.ResultStatus.FAILED)); } response.setResult(ExecutorStatus.STATUS_ERROR); - response.setErrorCode(InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); + response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); response.setErrorMessage("An error occurred while processing the extension. Please try again."); + response.setErrorDescription("The external extension service could not complete the request. " + + "If the problem persists, contact your administrator."); return response; } } @@ -242,7 +249,9 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt if (executionStatus == null) { response.setResult(ExecutorStatus.STATUS_ERROR); - response.setErrorCode(InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); + response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); + response.setErrorMessage("Extension did not return a response."); + response.setErrorDescription("The In-Flow Extension action did not return a status. Please try again."); return response; } @@ -271,19 +280,9 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt case ERROR: response.setResult(ExecutorStatus.STATUS_ERROR); + response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); Error error = (Error) executionStatus.getResponse(); - response.setErrorCode(InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); if (error != null) { - Map errorInfo = new HashMap<>(); - if (error.getErrorMessage() != null) { - errorInfo.put(InFlowExtensionConstants.ERROR_MESSAGE_KEY, - error.getErrorMessage()); - } - if (error.getErrorDescription() != null) { - errorInfo.put(InFlowExtensionConstants.ERROR_DESCRIPTION_KEY, - error.getErrorDescription()); - } - response.setAdditionalInfo(errorInfo); response.setErrorMessage(stripI18nBraces(error.getErrorMessage())); response.setErrorDescription(stripI18nBraces(error.getErrorDescription())); } @@ -304,8 +303,10 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt .resultStatus(DiagnosticLog.ResultStatus.FAILED)); } response.setResult(ExecutorStatus.STATUS_ERROR); - response.setErrorCode(InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); + response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); response.setErrorMessage("Extension returned INCOMPLETE without a redirect URL."); + response.setErrorDescription("The external extension returned an incomplete response. " + + "Please try again."); break; } @@ -341,7 +342,9 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt default: LOG.warn("Unknown execution status: " + executionStatus.getStatus()); response.setResult(ExecutorStatus.STATUS_ERROR); - response.setErrorCode(InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); + response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); + response.setErrorMessage("Extension returned an unexpected response."); + response.setErrorDescription("The In-Flow Extension returned an unrecognised status. Please try again."); } return response; @@ -370,7 +373,7 @@ private String buildUserFacingErrorMessage(Failure failure) { private ActionExecutorService getActionExecutorService() { - return FlowExecutionEngineDataHolder.getInstance().getActionExecutorService(); + return InFlowExtensionDataHolder.getInstance().getActionExecutorService(); } /** diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionLogConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionLogConstants.java similarity index 94% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionLogConstants.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionLogConstants.java index 22592fc4f21e..403e59c94d3a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionLogConstants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionLogConstants.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; +package org.wso2.carbon.identity.flow.inflow.extensions.executor; /** * Diagnostic log constants for the In-Flow Extension layer. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilder.java similarity index 88% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilder.java index 20a891eca3ae..ed9db5bde7fc 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilder.java @@ -16,14 +16,14 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; +package org.wso2.carbon.identity.flow.inflow.extensions.executor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.identity.action.execution.api.constant.ActionExecutionLogConstants; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionRequestBuilderException; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.*; +import org.wso2.carbon.identity.flow.inflow.extensions.model.*; import org.wso2.carbon.utils.DiagnosticLog; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequest; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequestContext; @@ -31,16 +31,19 @@ import org.wso2.carbon.identity.action.execution.api.model.AllowedOperation; import org.wso2.carbon.identity.action.execution.api.model.Application; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.action.execution.api.model.Header; import org.wso2.carbon.identity.action.execution.api.model.Operation; +import org.wso2.carbon.identity.action.execution.api.model.Organization; import org.wso2.carbon.identity.action.execution.api.model.Tenant; import org.wso2.carbon.identity.action.execution.api.model.User; import org.wso2.carbon.identity.action.execution.api.model.UserClaim; import org.wso2.carbon.identity.action.execution.api.model.UserStore; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionRequestBuilder; import org.wso2.carbon.identity.action.management.api.model.Action; +import org.wso2.carbon.identity.core.context.IdentityContext; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; -import org.wso2.carbon.identity.flow.execution.engine.Constants.InFlowExtensionConstants; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverPolicy; +import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; +import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverPolicy; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; @@ -248,6 +251,10 @@ private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List headers = new ArrayList<>(); + for (org.wso2.carbon.identity.core.context.model.Header coreHeader : inboundRequest.getHeaders()) { + if (coreHeader.getName() == null) { + continue; + } + List values = coreHeader.getValue(); + String[] valueArray = values != null + ? values.toArray(new String[0]) : new String[0]; + headers.add(new Header(coreHeader.getName(), valueArray)); + } + request.setAdditionalHeaders(headers); + + return request; + } + /** * Build the {@link User} model from {@link FlowUser}, filtering by expose config. * Encrypts credential and claim values for expose paths marked as encrypted. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessor.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessor.java index f43e7b403032..ea839c01c820 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessor.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; +package org.wso2.carbon.identity.flow.inflow.extensions.executor; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; @@ -45,10 +45,10 @@ import org.wso2.carbon.identity.action.execution.api.model.SuccessStatus; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionResponseProcessor; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; -import org.wso2.carbon.identity.flow.execution.engine.Constants.InFlowExtensionConstants; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.OperationExecutionResult; +import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; +import org.wso2.carbon.identity.flow.inflow.extensions.model.AccessConfig; +import org.wso2.carbon.identity.flow.inflow.extensions.model.ContextPath; +import org.wso2.carbon.identity.flow.inflow.extensions.model.OperationExecutionResult; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.utils.DiagnosticLog; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/JWEEncryptionUtil.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/JWEEncryptionUtil.java index d795fd7f94eb..e38d93464124 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/JWEEncryptionUtil.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/JWEEncryptionUtil.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; +package org.wso2.carbon.identity.flow.inflow.extensions.executor; import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JOSEException; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtil.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtil.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtil.java index 3eb1d0cb49e3..06b51247c229 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtil.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtil.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; +package org.wso2.carbon.identity.flow.inflow.extensions.executor; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionDataHolder.java new file mode 100644 index 000000000000..efcebf8ee215 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionDataHolder.java @@ -0,0 +1,99 @@ +/* + * 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.inflow.extensions.internal; + +import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; +import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; +import org.wso2.carbon.identity.certificate.management.service.CertificateManagementService; +import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverConfig; + +/** + * Data holder for the In-Flow Extension bundle. + */ +public class InFlowExtensionDataHolder { + + private static final InFlowExtensionDataHolder instance = new InFlowExtensionDataHolder(); + + private ActionExecutorService actionExecutorService; + private ActionManagementService actionManagementService; + private CertificateManagementService certificateManagementService; + private FlowContextHandoverConfig flowContextHandoverConfig; + + private InFlowExtensionDataHolder() { + + } + + public static InFlowExtensionDataHolder getInstance() { + + return instance; + } + + public ActionExecutorService getActionExecutorService() { + + return actionExecutorService; + } + + public void setActionExecutorService(ActionExecutorService actionExecutorService) { + + this.actionExecutorService = actionExecutorService; + } + + public ActionManagementService getActionManagementService() { + + return actionManagementService; + } + + public void setActionManagementService(ActionManagementService actionManagementService) { + + this.actionManagementService = actionManagementService; + } + + public CertificateManagementService getCertificateManagementService() { + + return certificateManagementService; + } + + public void setCertificateManagementService(CertificateManagementService certificateManagementService) { + + this.certificateManagementService = certificateManagementService; + } + + /** + * Get the In-Flow Extension context handover config. Lazily initialised on first access + * (the constructor reads from {@code IdentityUtil} which requires the carbon configuration + * to be loaded — keeping this lazy avoids ordering issues with OSGi component activation). + * + * @return The handover config, never null. + */ + public synchronized FlowContextHandoverConfig getFlowContextHandoverConfig() { + + if (flowContextHandoverConfig == null) { + flowContextHandoverConfig = new FlowContextHandoverConfig(); + } + return flowContextHandoverConfig; + } + + /** + * Override the handover config. Intended for tests only. + */ + public synchronized void setFlowContextHandoverConfig(FlowContextHandoverConfig flowContextHandoverConfig) { + + this.flowContextHandoverConfig = flowContextHandoverConfig; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionServiceComponent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionServiceComponent.java new file mode 100644 index 000000000000..e1e8258ca769 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionServiceComponent.java @@ -0,0 +1,144 @@ +/* + * 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.inflow.extensions.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionRequestBuilder; +import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionResponseProcessor; +import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; +import org.wso2.carbon.identity.action.management.api.service.ActionConverter; +import org.wso2.carbon.identity.action.management.api.service.ActionDTOModelResolver; +import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; +import org.wso2.carbon.identity.certificate.management.service.CertificateManagementService; +import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; +import org.wso2.carbon.identity.flow.inflow.extensions.executor.InFlowExtensionExecutor; +import org.wso2.carbon.identity.flow.inflow.extensions.executor.InFlowExtensionRequestBuilder; +import org.wso2.carbon.identity.flow.inflow.extensions.executor.InFlowExtensionResponseProcessor; +import org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConverter; +import org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionDTOModelResolver; + +/** + * OSGi declarative services component which registers the In-Flow Extension services. + */ +@Component( + name = "flow.inflow.extensions.component", + immediate = true) +public class InFlowExtensionServiceComponent { + + private static final Log LOG = LogFactory.getLog(InFlowExtensionServiceComponent.class); + + @Activate + protected void activate(ComponentContext context) { + + try { + BundleContext bundleContext = context.getBundleContext(); + + bundleContext.registerService(Executor.class.getName(), new InFlowExtensionExecutor(), null); + bundleContext.registerService(ActionExecutionRequestBuilder.class.getName(), + new InFlowExtensionRequestBuilder(), null); + bundleContext.registerService(ActionExecutionResponseProcessor.class.getName(), + new InFlowExtensionResponseProcessor(), null); + + bundleContext.registerService(ActionConverter.class.getName(), + new InFlowExtensionActionConverter(), null); + bundleContext.registerService(ActionDTOModelResolver.class.getName(), + new InFlowExtensionActionDTOModelResolver( + InFlowExtensionDataHolder.getInstance().getCertificateManagementService()), + null); + + LOG.debug("In-Flow Extension service successfully activated."); + } catch (Throwable e) { + LOG.error("Error while initiating In-Flow Extension service", e); + } + } + + @Deactivate + protected void deactivate(ComponentContext context) { + + LOG.debug("In-Flow Extension service successfully deactivated."); + } + + @Reference( + name = "ActionManagementService", + service = ActionManagementService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetActionManagementService" + ) + protected void setActionManagementService(ActionManagementService actionManagementService) { + + LOG.debug("Setting the ActionManagementService in the In-Flow Extension component."); + InFlowExtensionDataHolder.getInstance().setActionManagementService(actionManagementService); + } + + protected void unsetActionManagementService(ActionManagementService actionManagementService) { + + LOG.debug("Unsetting the ActionManagementService in the In-Flow Extension component."); + InFlowExtensionDataHolder.getInstance().setActionManagementService(null); + } + + @Reference( + name = "ActionExecutorService", + service = ActionExecutorService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetActionExecutorService" + ) + protected void setActionExecutorService(ActionExecutorService actionExecutorService) { + + LOG.debug("Setting the ActionExecutorService in the In-Flow Extension component."); + InFlowExtensionDataHolder.getInstance().setActionExecutorService(actionExecutorService); + } + + protected void unsetActionExecutorService(ActionExecutorService actionExecutorService) { + + LOG.debug("Unsetting the ActionExecutorService in the In-Flow Extension component."); + InFlowExtensionDataHolder.getInstance().setActionExecutorService(null); + } + + @Reference( + name = "CertificateManagementService", + service = CertificateManagementService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetCertificateManagementService" + ) + protected void setCertificateManagementService(CertificateManagementService certificateManagementService) { + + LOG.debug("Setting the CertificateManagementService in the In-Flow Extension component."); + InFlowExtensionDataHolder.getInstance() + .setCertificateManagementService(certificateManagementService); + } + + protected void unsetCertificateManagementService( + CertificateManagementService certificateManagementService) { + + LOG.debug("Unsetting the CertificateManagementService in the In-Flow Extension component."); + InFlowExtensionDataHolder.getInstance().setCertificateManagementService(null); + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConstants.java similarity index 97% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConstants.java index 009fa1053c1b..feebefa0e01b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConstants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConstants.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management; +package org.wso2.carbon.identity.flow.inflow.extensions.management; /** * Constants for In-Flow Extension action management. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConverter.java similarity index 86% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConverter.java index fe1f62d1f74a..7e1f0f33e589 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionConverter.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConverter.java @@ -16,28 +16,28 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management; +package org.wso2.carbon.identity.flow.inflow.extensions.management; import org.wso2.carbon.identity.action.management.api.model.Action; import org.wso2.carbon.identity.action.management.api.model.ActionDTO; import org.wso2.carbon.identity.action.management.api.model.ActionProperty; import org.wso2.carbon.identity.action.management.api.service.ActionConverter; import org.wso2.carbon.identity.certificate.management.model.Certificate; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.AccessConfig; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.Encryption; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.InFlowExtensionAction; +import org.wso2.carbon.identity.flow.inflow.extensions.model.AccessConfig; +import org.wso2.carbon.identity.flow.inflow.extensions.model.Encryption; +import org.wso2.carbon.identity.flow.inflow.extensions.model.ContextPath; +import org.wso2.carbon.identity.flow.inflow.extensions.model.InFlowExtensionAction; import java.util.HashMap; import java.util.List; import java.util.Map; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY_PREFIX; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.CERTIFICATE; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ICON_URL; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE_PREFIX; +import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY; +import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY_PREFIX; +import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.CERTIFICATE; +import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ICON_URL; +import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE; +import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE_PREFIX; /** * ActionConverter implementation for In-Flow Extension actions. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionDTOModelResolver.java similarity index 95% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionDTOModelResolver.java index 9175fa8972db..3f26fe5ee804 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/management/InFlowExtensionActionDTOModelResolver.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionDTOModelResolver.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management; +package org.wso2.carbon.identity.flow.inflow.extensions.management; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -33,7 +33,7 @@ import org.wso2.carbon.identity.certificate.management.exception.CertificateMgtException; import org.wso2.carbon.identity.certificate.management.model.Certificate; import org.wso2.carbon.identity.certificate.management.service.CertificateManagementService; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; +import org.wso2.carbon.identity.flow.inflow.extensions.model.ContextPath; import java.io.IOException; import java.util.ArrayList; @@ -44,14 +44,14 @@ import java.util.Set; import static java.util.Collections.emptyList; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY_PREFIX; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.CERTIFICATE; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ICON_URL; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE_PREFIX; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.CERTIFICATE_NAME_PREFIX; -import static org.wso2.carbon.identity.flow.execution.engine.inflow.extension.management.InFlowExtensionActionConstants.MAX_EXPOSE_PATHS; +import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY; +import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY_PREFIX; +import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.CERTIFICATE; +import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ICON_URL; +import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE; +import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE_PREFIX; +import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.CERTIFICATE_NAME_PREFIX; +import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.MAX_EXPOSE_PATHS; /** * ActionDTOModelResolver implementation for In-Flow Extension actions. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java similarity index 97% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeBuilder.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java index f535edc836e1..46199bc1e2cc 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java @@ -16,10 +16,10 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.metadata; +package org.wso2.carbon.identity.flow.inflow.extensions.metadata; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverConfig; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverPolicy; +import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverPolicy; import java.util.ArrayList; import java.util.Arrays; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeMetadata.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeMetadata.java similarity index 96% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeMetadata.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeMetadata.java index 2b97377972ea..d26d2ca2780b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeMetadata.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeMetadata.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.metadata; +package org.wso2.carbon.identity.flow.inflow.extensions.metadata; import java.util.Collections; import java.util.List; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeNode.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeNode.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeNode.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeNode.java index ee20519ae5d9..e00a7fef97e7 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeNode.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeNode.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.metadata; +package org.wso2.carbon.identity.flow.inflow.extensions.metadata; import java.util.Collections; import java.util.List; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeService.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeService.java similarity index 93% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeService.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeService.java index ea847312806f..b48431409b56 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/metadata/InFlowExtensionContextTreeService.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeService.java @@ -16,9 +16,9 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.metadata; +package org.wso2.carbon.identity.flow.inflow.extensions.metadata; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverConfig; /** * Public-API entry point for retrieving the controlled In-Flow Extension context tree. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfig.java similarity index 96% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfig.java index 6acc16408129..047a57a22bda 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfig.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfig.java @@ -16,9 +16,9 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; +package org.wso2.carbon.identity.flow.inflow.extensions.model; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor.PathTypeAnnotationUtil; +import org.wso2.carbon.identity.flow.inflow.extensions.executor.PathTypeAnnotationUtil; import java.util.Collections; import java.util.List; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/ContextPath.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/ContextPath.java similarity index 96% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/ContextPath.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/ContextPath.java index 06b8445a0c4d..964c0767c63a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/ContextPath.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/ContextPath.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; +package org.wso2.carbon.identity.flow.inflow.extensions.model; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/Encryption.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/Encryption.java similarity index 96% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/Encryption.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/Encryption.java index a3b72ecafeaa..21247cdb12d9 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/Encryption.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/Encryption.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; +package org.wso2.carbon.identity.flow.inflow.extensions.model; import org.wso2.carbon.identity.certificate.management.model.Certificate; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionAction.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionAction.java index 7cb125c34201..60e67feafede 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionAction.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionAction.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; +package org.wso2.carbon.identity.flow.inflow.extensions.model; import org.wso2.carbon.identity.action.management.api.model.Action; import org.wso2.carbon.identity.action.management.api.model.EndpointConfig; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEvent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEvent.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEvent.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEvent.java index 466a15247ea0..1a26573be74d 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEvent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEvent.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; +package org.wso2.carbon.identity.flow.inflow.extensions.model; import org.wso2.carbon.identity.action.execution.api.model.Application; import org.wso2.carbon.identity.action.execution.api.model.Event; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionRequest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionRequest.java new file mode 100644 index 000000000000..4aab7195933e --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionRequest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025, 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.inflow.extensions.model; + +import org.wso2.carbon.identity.action.execution.api.model.Request; + +/** + * Request payload carried inside an {@link InFlowExtensionEvent}. Holds the inbound HTTP + * request's additional headers and parameters (filtered downstream by the action framework + * against the action's allowed-headers / allowed-parameters configuration). + */ +public class InFlowExtensionRequest extends Request { + + public InFlowExtensionRequest() { + + super(); + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResult.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResult.java similarity index 95% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResult.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResult.java index 79517fe81ad1..a80c6c2ece30 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResult.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResult.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; +package org.wso2.carbon.identity.flow.inflow.extensions.model; import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverPolicyTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverPolicyTest.java similarity index 97% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverPolicyTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverPolicyTest.java index 24049b88b55a..3136bfadabca 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowContextHandoverPolicyTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverPolicyTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config; +package org.wso2.carbon.identity.flow.inflow.extensions.config; import org.testng.annotations.Test; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowExecutionContextFilterTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowExecutionContextFilterTest.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowExecutionContextFilterTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowExecutionContextFilterTest.java index 4fc589e52840..76515fe41f53 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/config/FlowExecutionContextFilterTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowExecutionContextFilterTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config; +package org.wso2.carbon.identity.flow.inflow.extensions.config; import org.testng.annotations.Test; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcherTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcherTest.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcherTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcherTest.java index 7af9f74e1488..4126048778f5 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/HierarchicalPrefixMatcherTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcherTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; +package org.wso2.carbon.identity.flow.inflow.extensions.executor; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java similarity index 94% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java index fee305e78731..29341fb0c414 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; +package org.wso2.carbon.identity.flow.inflow.extensions.executor; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -34,10 +34,11 @@ import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.flow.execution.engine.Constants; +import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverConfig; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.config.FlowContextHandoverPolicy; -import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; +import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverPolicy; +import org.wso2.carbon.identity.flow.inflow.extensions.internal.InFlowExtensionDataHolder; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; @@ -71,7 +72,7 @@ public class InFlowExtensionExecutorTest { private ActionExecutorService actionExecutorService; private AutoCloseable mocks; - private MockedStatic holderMock; + private MockedStatic holderMock; private MockedStatic loggerUtilsMock; @BeforeMethod @@ -80,8 +81,8 @@ public void setUp() { mocks = MockitoAnnotations.openMocks(this); executor = new InFlowExtensionExecutor(); - FlowExecutionEngineDataHolder holderInstance = - mock(FlowExecutionEngineDataHolder.class); + InFlowExtensionDataHolder holderInstance = + mock(InFlowExtensionDataHolder.class); when(holderInstance.getActionExecutorService()).thenReturn(actionExecutorService); // The executor consults the handover config on every run. Stub it to a permissive @@ -90,8 +91,8 @@ public void setUp() { when(handoverConfig.resolve(any())).thenReturn(FlowContextHandoverPolicy.permissive()); when(holderInstance.getFlowContextHandoverConfig()).thenReturn(handoverConfig); - holderMock = mockStatic(FlowExecutionEngineDataHolder.class); - holderMock.when(FlowExecutionEngineDataHolder::getInstance).thenReturn(holderInstance); + holderMock = mockStatic(InFlowExtensionDataHolder.class); + holderMock.when(InFlowExtensionDataHolder::getInstance).thenReturn(holderInstance); loggerUtilsMock = mockStatic(LoggerUtils.class); loggerUtilsMock.when(LoggerUtils::isDiagnosticLogsEnabled).thenReturn(false); @@ -304,7 +305,8 @@ public void testExecuteError() throws Exception { ExecutorResponse response = executor.execute(context); assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); - assertEquals(response.getErrorCode(), Constants.InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); + assertEquals(response.getErrorCode(), + Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); // errorMessage carries the Error reason/code field; errorDescription carries the human-readable text. assertEquals(response.getErrorMessage(), "internal_error"); assertEquals(response.getErrorDescription(), "DB connection failed"); @@ -332,7 +334,8 @@ public void testExecuteErrorNoDescription() throws Exception { ExecutorResponse response = executor.execute(context); assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); - assertEquals(response.getErrorCode(), Constants.InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); + assertEquals(response.getErrorCode(), + Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); assertEquals(response.getErrorMessage(), "internal_error"); assertNull(response.getErrorDescription()); } @@ -359,7 +362,8 @@ public void testExecuteErrorBothNull() throws Exception { ExecutorResponse response = executor.execute(context); assertEquals(response.getResult(), ExecutorStatus.STATUS_ERROR); - assertEquals(response.getErrorCode(), Constants.InFlowExtensionConstants.IN_FLOW_EXTENSION_ERROR_CODE); + assertEquals(response.getErrorCode(), + Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); // Both fields null → errorMessage and errorDescription remain null; errorCode alone triggers FE-65033 routing. assertNull(response.getErrorMessage()); assertNull(response.getErrorDescription()); @@ -416,7 +420,7 @@ public void testExecuteIncompleteWithRedirectUrlReturnsExternalRedirection() thr any(FlowContext.class), eq("carbon.super"))) .thenAnswer(invocation -> { FlowContext fc = invocation.getArgument(2); - fc.add(Constants.InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, + fc.add(InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, "https://example.com/step-up"); return incompleteStatus; }); @@ -465,7 +469,7 @@ public void testExecuteIncompleteRedirectAppendsFlowIdWithAmpersandWhenUrlHasQue .thenAnswer(invocation -> { FlowContext fc = invocation.getArgument(2); // URL already has a query string — the executor must use & not ?. - fc.add(Constants.InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, + fc.add(InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, "https://example.com/step-up?ref=abc"); return incompleteStatus; }); @@ -498,7 +502,7 @@ public void testExecuteIncompleteRedirectEmptyUrlReturnsError() throws Exception any(FlowContext.class), eq("carbon.super"))) .thenAnswer(invocation -> { FlowContext fc = invocation.getArgument(2); - fc.add(Constants.InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, ""); + fc.add(InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, ""); return incompleteStatus; }); @@ -560,12 +564,12 @@ public void testExecuteServiceUnavailable() throws Exception { // Override holder mock to return null service. holderMock.close(); - FlowExecutionEngineDataHolder holderInstance = - mock(FlowExecutionEngineDataHolder.class); + InFlowExtensionDataHolder holderInstance = + mock(InFlowExtensionDataHolder.class); when(holderInstance.getActionExecutorService()).thenReturn(null); - holderMock = mockStatic(FlowExecutionEngineDataHolder.class); - holderMock.when(FlowExecutionEngineDataHolder::getInstance).thenReturn(holderInstance); + holderMock = mockStatic(InFlowExtensionDataHolder.class); + holderMock.when(InFlowExtensionDataHolder::getInstance).thenReturn(holderInstance); Map metadata = new HashMap<>(); metadata.put("actionId", "test-action-001"); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilderTest.java similarity index 92% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilderTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilderTest.java index 535b3486d846..a6a083383a12 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilderTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; +package org.wso2.carbon.identity.flow.inflow.extensions.executor; import org.mockito.MockedStatic; import org.testng.annotations.AfterMethod; @@ -31,9 +31,10 @@ import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.*; +import org.wso2.carbon.identity.flow.inflow.extensions.model.*; import org.wso2.carbon.identity.certificate.management.model.Certificate; import org.wso2.carbon.identity.flow.execution.engine.Constants; +import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; 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.mgt.model.NodeConfig; @@ -104,7 +105,7 @@ public void testBuildRequestWithMinimalContext() throws ActionExecutionRequestBu FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -120,7 +121,7 @@ public void testBuildRequestUsesEmptyExposeWhenExposeIsNull() FlowExecutionContext execCtx = createFullFlowExecutionContext(); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -144,7 +145,7 @@ public void testBuildRequestWithValidModifyPaths() Arrays.asList(new ContextPath("/properties/riskScore", false))); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -166,7 +167,7 @@ public void testBuildRequestWithNoAccessConfig() FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); // No action → no access config → no modify paths → only REDIRECT (always present). ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); @@ -186,7 +187,7 @@ public void testBuildRequestWithEmptyModifyPaths() AccessConfig accessConfig = new AccessConfig(null, Arrays.asList()); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -209,7 +210,7 @@ public void testRedirectIsAdvertisedAlongsideReplace() Arrays.asList(new ContextPath("/properties/riskScore", false))); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -227,7 +228,7 @@ public void testRedirectIsAdvertisedWhenNoAccessConfig() FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); @@ -250,7 +251,7 @@ public void testPathAnnotationStrippingSimpleArray() Arrays.asList(new ContextPath("/properties/riskFactors{[String]}", false))); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -263,7 +264,7 @@ public void testPathAnnotationStrippingSimpleArray() // Annotations should be stored in FlowContext. Map annotations = flowContext.getValue( - Constants.InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, Map.class); + InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, Map.class); assertNotNull(annotations); assertEquals(annotations.get("/properties/riskFactors"), "[String]"); } @@ -278,7 +279,7 @@ public void testPathAnnotationStrippingSchemaAnnotation() Arrays.asList(new ContextPath("/properties/items{name: String, count: Integer}", false))); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -289,7 +290,7 @@ public void testPathAnnotationStrippingSchemaAnnotation() assertFalse(op.getPaths().contains("/properties/items{name: String, count: Integer}")); Map annotations = flowContext.getValue( - Constants.InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, Map.class); + InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, Map.class); assertEquals(annotations.get("/properties/items"), "name: String, count: Integer"); } @@ -303,13 +304,13 @@ public void testPathWithoutAnnotationNotStoredInAnnotationsMap() Arrays.asList(new ContextPath("/properties/riskScore", false))); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); requestBuilder.buildActionExecutionRequest(flowContext, mockReqCtx(accessConfig, null)); // No annotations should be stored when paths have no annotations. Map annotations = flowContext.getValue( - Constants.InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, Map.class); + InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, Map.class); assertNull(annotations); } @@ -325,7 +326,7 @@ public void testMultipleAnnotatedPaths() new ContextPath("/properties/items{name: String, count: Integer}", false))); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -338,7 +339,7 @@ public void testMultipleAnnotatedPaths() assertTrue(op.getPaths().contains("/properties/items")); Map annotations = flowContext.getValue( - Constants.InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, Map.class); + InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, Map.class); assertEquals(annotations.size(), 2); assertEquals(annotations.get("/properties/riskFactors"), "[String]"); assertEquals(annotations.get("/properties/items"), "name: String, count: Integer"); @@ -362,7 +363,7 @@ public void testModifyPathsDoNotAffectExpose() execCtx.setProperty("riskScore", "50"); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -389,7 +390,7 @@ public void testMultipleModifyPathsProduceSingleReplaceOperation() new ContextPath("/user/claims/http://wso2.org/claims/email", false))); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -416,7 +417,7 @@ public void testExposeFilteringOnlyExposedAreaIncluded() new ContextPath("/flow/flowType", false)), null); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -441,7 +442,7 @@ public void testFlowPortalUrlExposed() new ContextPath("/flow/portalUrl", false)), null); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -463,7 +464,7 @@ public void testFlowPortalUrlNotExposedYieldsNull() new ContextPath("/flow/flowType", false)), null); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -483,7 +484,7 @@ public void testFlowCallbackUrlExposed() new ContextPath("/flow/callbackUrl", false)), null); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -503,7 +504,7 @@ public void testExposeFilteringSpecificClaim() new ContextPath("/user/userId", false)), null); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, null)); @@ -540,7 +541,7 @@ public void testPropertiesEncryptedWhenExposePathMarkedEncrypted() .certificateContent("test-cert-pem").build()); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, encryption)); @@ -579,7 +580,7 @@ public void testClaimEncryptedWhenExposePathMarkedEncrypted() .certificateContent("test-cert-pem").build()); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, encryption)); @@ -634,7 +635,7 @@ public void testCredentialEncryptedWhenExposePathMarkedEncrypted() .certificateContent("test-cert-pem").build()); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( flowContext, mockReqCtx(accessConfig, encryption)); @@ -672,7 +673,7 @@ public void testEncryptionFailureThrowsException() .certificateContent("test-cert-pem").build()); FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); requestBuilder.buildActionExecutionRequest(flowContext, mockReqCtx(accessConfig, encryption)); } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessorTest.java similarity index 91% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessorTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessorTest.java index 2801ca5511df..88ede1b73332 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessorTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; +package org.wso2.carbon.identity.flow.inflow.extensions.executor; import org.mockito.MockedStatic; import org.testng.annotations.AfterMethod; @@ -38,11 +38,10 @@ import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.flow.execution.engine.Constants; -import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; -import org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model.ContextPath; +import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; +import org.wso2.carbon.identity.flow.inflow.extensions.internal.InFlowExtensionDataHolder; +import org.wso2.carbon.identity.flow.inflow.extensions.model.ContextPath; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; -import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; -import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; @@ -69,8 +68,7 @@ public class InFlowExtensionResponseProcessorTest { private InFlowExtensionResponseProcessor responseProcessor; private MockedStatic loggerUtilsMock; - private MockedStatic holderMock; - private ClaimMetadataManagementService claimService; + private MockedStatic holderMock; private FlowContext capturedFlowContext; @BeforeMethod @@ -80,18 +78,9 @@ public void setUp() throws Exception { loggerUtilsMock = mockStatic(LoggerUtils.class); loggerUtilsMock.when(LoggerUtils::isDiagnosticLogsEnabled).thenReturn(false); - // Mock claim service for claim validation - claimService = mock(ClaimMetadataManagementService.class); - LocalClaim emailClaim = mock(LocalClaim.class); - when(emailClaim.getClaimURI()).thenReturn("http://wso2.org/claims/email"); - LocalClaim countryClaim = mock(LocalClaim.class); - when(countryClaim.getClaimURI()).thenReturn("http://wso2.org/claims/country"); - when(claimService.getLocalClaims(anyString())).thenReturn(Arrays.asList(emailClaim, countryClaim)); - - FlowExecutionEngineDataHolder holderInstance = mock(FlowExecutionEngineDataHolder.class); - when(holderInstance.getClaimMetadataManagementService()).thenReturn(claimService); - holderMock = mockStatic(FlowExecutionEngineDataHolder.class); - holderMock.when(FlowExecutionEngineDataHolder::getInstance).thenReturn(holderInstance); + InFlowExtensionDataHolder holderInstance = mock(InFlowExtensionDataHolder.class); + holderMock = mockStatic(InFlowExtensionDataHolder.class); + holderMock.when(InFlowExtensionDataHolder::getInstance).thenReturn(holderInstance); } @AfterMethod @@ -123,7 +112,7 @@ public void testPropertyReplaceFlatExists() throws ActionExecutionResponseProces assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); Map pendingProps = - capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); assertEquals(pendingProps.get("riskScore"), "80"); } @@ -139,7 +128,7 @@ public void testPropertyReplaceCreatesIfMissing() throws ActionExecutionResponse assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); Map pendingProps = - capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); assertEquals(pendingProps.get("riskScore"), "80"); } @@ -157,7 +146,7 @@ public void testPropertyReplaceCoercesToStringByDefault() assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); Map pendingProps = - capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); assertEquals(pendingProps.get("riskScore"), "75"); } @@ -176,7 +165,7 @@ public void testPropertyReplaceWithMultivaluedAnnotation() assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); Map pendingProps = - capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); Object stored = pendingProps.get("riskFactors"); assertTrue(stored instanceof List); @@ -196,7 +185,7 @@ public void testPropertyReplaceMultivaluedSingleValueWrapped() executeSuccessResponse(execCtx, replaceOp, annotations); Map pendingProps = - capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); Object stored = pendingProps.get("tags"); assertTrue(stored instanceof List); @@ -221,7 +210,7 @@ public void testPropertyReplaceWithComplexAnnotation() assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); Map pendingProps = - capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); // Complex annotation → value passed through as-is. Object stored = pendingProps.get("item"); @@ -241,7 +230,7 @@ public void testPropertyReplaceWithPrimaryTypeAnnotation() executeSuccessResponse(execCtx, replaceOp, annotations); Map pendingProps = - capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); assertEquals(pendingProps.get("score"), "95"); } @@ -353,7 +342,7 @@ public void testUserClaimReplace() throws ActionExecutionResponseProcessorExcept executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); Map pendingClaims = - capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class); + capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class); assertNotNull(pendingClaims); assertEquals(pendingClaims.get("http://wso2.org/claims/email"), "new@example.com"); } @@ -368,7 +357,7 @@ public void testUserClaimReplaceCreatesNewClaim() throws ActionExecutionResponse executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); Map pendingClaims = - capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class); + capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class); assertNotNull(pendingClaims); assertEquals(pendingClaims.get("http://wso2.org/claims/country"), "US"); } @@ -384,7 +373,7 @@ public void testUserClaimReplaceStringifiesValue() throws ActionExecutionRespons executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); Map pendingClaims = - capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class); + capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class); assertNotNull(pendingClaims); assertEquals(pendingClaims.get("http://wso2.org/claims/country"), "42"); } @@ -587,7 +576,7 @@ public void testMultipleOperationsMixedResults() throws ActionExecutionResponseP assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); Map pendingProps = - capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); assertEquals(pendingProps.get("newProp"), "newValue"); assertEquals(pendingProps.get("existingProp"), "updated"); @@ -719,7 +708,7 @@ public void testProcessIncompleteResponseWithRedirectStashesUrlAndReturnsIncompl assertEquals(status.getStatus(), ActionExecutionStatus.Status.INCOMPLETE); // URL must be stashed under the key the executor reads. - assertEquals(flowContext.getValue(Constants.InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, String.class), + assertEquals(flowContext.getValue(InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, String.class), "https://example.com/step-up"); } @@ -739,11 +728,11 @@ public void testProcessIncompleteResponseIgnoresNonRedirectOperations() assertEquals(status.getStatus(), ActionExecutionStatus.Status.INCOMPLETE); // Redirect URL is captured, but the REPLACE must NOT have produced any pending props. - assertEquals(flowContext.getValue(Constants.InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, String.class), + assertEquals(flowContext.getValue(InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, String.class), "https://example.com/step-up"); - assertNull(flowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class)); - assertNull(flowContext.getValue(Constants.InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class)); - assertNull(flowContext.getValue(Constants.InFlowExtensionConstants.PENDING_CREDENTIALS_KEY, Map.class)); + assertNull(flowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class)); + assertNull(flowContext.getValue(InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class)); + assertNull(flowContext.getValue(InFlowExtensionConstants.PENDING_CREDENTIALS_KEY, Map.class)); } @Test(expectedExceptions = ActionExecutionResponseProcessorException.class) @@ -848,10 +837,10 @@ private ActionExecutionStatus executeSuccessResponse( throws ActionExecutionResponseProcessorException { FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); if (pathTypeAnnotations != null && !pathTypeAnnotations.isEmpty()) { - flowContext.add(Constants.InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); + flowContext.add(InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); } capturedFlowContext = flowContext; @@ -873,13 +862,13 @@ private ActionExecutionStatus executeSuccessResponseWithModifyPaths( throws ActionExecutionResponseProcessorException { FlowContext flowContext = FlowContext.create() - .add(Constants.InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); if (pathTypeAnnotations != null && !pathTypeAnnotations.isEmpty()) { - flowContext.add(Constants.InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); + flowContext.add(InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); } if (modifyPaths != null) { - flowContext.add(Constants.InFlowExtensionConstants.MODIFY_PATHS_KEY, modifyPaths); + flowContext.add(InFlowExtensionConstants.MODIFY_PATHS_KEY, modifyPaths); } capturedFlowContext = flowContext; @@ -933,7 +922,7 @@ public void testNonStringValueForEncryptedPathHandledGracefully() assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); // Value should be coerced to String (default behavior for properties). Map pendingProps = - capturedFlowContext.getValue(Constants.InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); + capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); assertNotNull(pendingProps); assertEquals(pendingProps.get("data"), "42"); } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtilTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtilTest.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtilTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtilTest.java index 91a41108a2ae..7727e8b085fe 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/executor/PathTypeAnnotationUtilTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtilTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.executor; +package org.wso2.carbon.identity.flow.inflow.extensions.executor; import org.testng.annotations.Test; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfigTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfigTest.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfigTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfigTest.java index 896fe46a124b..f68a99b98cda 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/AccessConfigTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfigTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; +package org.wso2.carbon.identity.flow.inflow.extensions.model; import org.testng.annotations.Test; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEventTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEventTest.java similarity index 97% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEventTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEventTest.java index b75d09a38400..e7c5d3d00f46 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/InFlowExtensionEventTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEventTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; +package org.wso2.carbon.identity.flow.inflow.extensions.model; import org.testng.annotations.Test; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResultTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResultTest.java similarity index 96% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResultTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResultTest.java index 698bc66092a4..0c0c67b0b3ca 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/inflow/extension/model/OperationExecutionResultTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResultTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.execution.engine.inflow.extension.model; +package org.wso2.carbon.identity.flow.inflow.extensions.model; import org.testng.annotations.Test; import org.wso2.carbon.identity.action.execution.api.model.Operation; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/repository/conf/carbon.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/repository/conf/carbon.xml new file mode 100755 index 000000000000..17a93f2ff2ad --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/repository/conf/carbon.xml @@ -0,0 +1,687 @@ + + + + + + + + WSO2 Identity Server + + + IS + + + 5.3.0 + + + localhost + + + localhost + + + local:/${carbon.context}/services/ + + + + + + + IdentityServer + + + + + + + org.wso2.carbon + + + / + + + + + + + + + 15 + + + + + + + + + 0 + + + + + 9999 + + 11111 + + + + + + 10389 + + 8000 + + + + + + 10500 + + + + + + + + + org.wso2.carbon.tomcat.jndi.CarbonJavaURLContextFactory + + + + + + + + + + java + + + + + + + + + + false + + + false + + + 600 + + + + false + + + + + + + + 30 + + + + + + + + + 15 + + + + + + ${carbon.home}/repository/deployment/server/ + + + 15 + + + ${carbon.home}/repository/conf/axis2/axis2.xml + + + 30000 + + + ${carbon.home}/repository/deployment/client/ + + ${carbon.home}/repository/conf/axis2/axis2_client.xml + + true + + + + + + + + + + admin + Default Administrator Role + + + user + Default User Role + + + + + + + + + + + + ${carbon.home}/repository/resources/security/wso2carbon.jks + + JKS + + wso2carbon + + wso2carbon + + wso2carbon + + + + + + ${carbon.home}/repository/resources/security/client-truststore.jks + + JKS + + wso2carbon + + + + + + + + + + + + + + + + + + + UserManager + + + false + + org.wso2.carbon.identity.provider.AttributeCallbackHandler + + + org.wso2.carbon.identity.sts.store.DBTokenStore + + + true + allow + + + + + + + claim_mgt_menu + identity_mgt_emailtemplate_menu + identity_security_questions_menu + + + + ${carbon.home}/tmp/work + + + + + + true + + + 10 + + + 30 + + + + + + 100 + + + + keystore + certificate + * + + org.wso2.carbon.ui.transports.fileupload.AnyFileUploadExecutor + + + + + jarZip + + org.wso2.carbon.ui.transports.fileupload.JarZipUploadExecutor + + + + dbs + + org.wso2.carbon.ui.transports.fileupload.DBSFileUploadExecutor + + + + tools + + org.wso2.carbon.ui.transports.fileupload.ToolsFileUploadExecutor + + + + toolsAny + + org.wso2.carbon.ui.transports.fileupload.ToolsAnyFileUploadExecutor + + + + + + + + + + info + org.wso2.carbon.core.transports.util.InfoProcessor + + + wsdl + org.wso2.carbon.core.transports.util.Wsdl11Processor + + + wsdl2 + org.wso2.carbon.core.transports.util.Wsdl20Processor + + + xsd + org.wso2.carbon.core.transports.util.XsdProcessor + + + + + + false + false + true + svn + http://svnrepo.example.com/repos/ + username + password + true + + + + + + + + + + + + + + + ${require.carbon.servlet} + + + + + true + + + + + + + default repository + http://product-dist.wso2.com/p2/carbon/releases/wilkes/ + + + + + + + + true + + + + + + true + + diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/testng.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/testng.xml new file mode 100644 index 000000000000..8c47c787ee13 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/testng.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/flow-orchestration-framework/pom.xml b/components/flow-orchestration-framework/pom.xml index ad99cb8b1eb1..c72d439a9a56 100644 --- a/components/flow-orchestration-framework/pom.xml +++ b/components/flow-orchestration-framework/pom.xml @@ -37,6 +37,7 @@ org.wso2.carbon.identity.flow.mgt org.wso2.carbon.identity.flow.execution.engine + org.wso2.carbon.identity.flow.inflow.extensions diff --git a/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions.server.feature/pom.xml b/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions.server.feature/pom.xml new file mode 100644 index 000000000000..08505499f447 --- /dev/null +++ b/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions.server.feature/pom.xml @@ -0,0 +1,73 @@ + + + + + org.wso2.carbon.identity.framework + flow-orchestration-framework-feature + 7.11.70-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.identity.flow.inflow.extensions.server.feature + pom + Flow InFlow Extensions Feature + https://wso2.com + This feature contains the bundles required for in-flow extension support in the flow engine. + + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.flow.inflow.extensions + + + + + + + org.wso2.maven + carbon-p2-plugin + ${carbon.p2.plugin.version} + + + 4-p2-feature-generation + package + + p2-feature-gen + + + org.wso2.carbon.identity.flow.inflow.extensions.server + ../../etc/feature.properties + + + org.wso2.carbon.p2.category.type:server + + + + + org.wso2.carbon.identity.framework:org.wso2.carbon.identity.flow.inflow.extensions + + + + + + + + + diff --git a/pom.xml b/pom.xml index 50efeeb80aac..e5a675a01b57 100644 --- a/pom.xml +++ b/pom.xml @@ -854,6 +854,12 @@ zip ${project.version} + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.flow.inflow.extensions.server.feature + zip + ${project.version} + com.google.api-client google-api-client @@ -1941,6 +1947,11 @@ org.wso2.carbon.identity.flow.execution.engine ${project.version} + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.flow.inflow.extensions + ${project.version} + org.wso2.carbon.identity.framework org.wso2.carbon.identity.servlet.mgt From a63b57ed13ae8356631dd3ee491032428e882e9f Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Sat, 9 May 2026 01:48:16 +0530 Subject: [PATCH 25/41] Flow execution context handover filtering Generalized and moved to flow execution engine - fallback kept permissive until discussion --- .../pom.xml | 2 + .../config/FlowContextHandoverConfig.java | 177 +++++++++++ .../config/FlowContextHandoverConstants.java | 47 +++ .../config/FlowExecutionContextFilter.java | 195 ++++++++++++ .../FlowExecutionEngineDataHolder.java | 24 ++ .../flow/execution/engine/model/FlowUser.java | 16 + .../config/FlowContextHandoverConfigTest.java | 153 ++++++++++ .../FlowContextHandoverConfigTestHelper.java | 54 ++++ .../FlowExecutionContextFilterTest.java | 193 ++++++++---- .../src/test/resources/testng.xml | 6 + .../pom.xml | 10 +- .../extensions/InFlowExtensionConstants.java | 1 - .../config/FlowContextHandoverConfig.java | 128 -------- .../config/FlowContextHandoverPolicy.java | 287 ------------------ .../config/FlowExecutionContextFilter.java | 125 -------- .../executor/InFlowExtensionExecutor.java | 16 +- .../InFlowExtensionRequestBuilder.java | 19 +- .../internal/InFlowExtensionDataHolder.java | 24 -- .../InFlowExtensionContextTreeBuilder.java | 132 +++++--- .../InFlowExtensionContextTreeService.java | 33 +- .../extensions/InFlowExtensionTestUtils.java | 81 +++++ .../config/FlowContextHandoverPolicyTest.java | 106 ------- .../executor/InFlowExtensionExecutorTest.java | 26 +- .../FlowContextHandoverConfigTestHelper.java | 40 +++ ...InFlowExtensionContextTreeBuilderTest.java | 282 +++++++++++++++++ .../src/test/resources/testng.xml | 11 +- .../resources/identity.xml.j2 | 123 ++------ 27 files changed, 1361 insertions(+), 950 deletions(-) create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfig.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConstants.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilter.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTest.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTestHelper.java rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions => org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine}/config/FlowExecutionContextFilterTest.java (51%) delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverConfig.java delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverPolicy.java delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowExecutionContextFilter.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionTestUtils.java delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverPolicyTest.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/FlowContextHandoverConfigTestHelper.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml index eb43f6da7fc5..12bceaa28730 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml @@ -180,6 +180,8 @@ !org.wso2.carbon.identity.flow.execution.internal, org.wso2.carbon.identity.flow.execution.engine.util, org.wso2.carbon.identity.flow.execution.engine, + org.wso2.carbon.identity.flow.execution.engine.config, + org.wso2.carbon.identity.flow.execution.engine.internal, org.wso2.carbon.identity.flow.execution.engine.core, org.wso2.carbon.identity.flow.execution.engine.cache, org.wso2.carbon.identity.flow.execution.engine.store, diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfig.java new file mode 100644 index 000000000000..9e8c3d37379e --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfig.java @@ -0,0 +1,177 @@ +/* + * 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.config; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.core.util.IdentityConfigParser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Loads and exposes the global flow execution context handover filtering configuration from + * {@code identity.xml} (templated from {@code deployment.toml}). + * + *

      Two flat allow-lists are read: + *

        + *
      • {@code FlowExecutionContextHandover.IncludedAttributes.IncludedAttribute} — + * top-level property names of {@link + * org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext} + * to expose.
      • + *
      • {@code FlowExecutionContextHandover.IncludedUserAttributes.IncludedUserAttribute} — + * property names of {@link + * org.wso2.carbon.identity.flow.execution.engine.model.FlowUser} to expose.
      • + *
      + * + *

      If {@code "flowUser"} is present in {@code includedAttributes} the entire + * {@code FlowUser} is passed through and {@code includedUserAttributes} is ignored + * ({@link #isFullUserPassthrough()} returns {@code true}).

      + * + *

      When the configuration is absent or empty a permissive fallback (all known attributes) + * is used so that upgrading a server without updating deployment.toml preserves current + * behaviour.

      + */ +public final class FlowContextHandoverConfig { + + private static final Log LOG = LogFactory.getLog(FlowContextHandoverConfig.class); + + private final Set includedAttributes; + private final Set includedUserAttributes; + private final boolean fullUserPassthrough; + + /** + * Construct by reading from the carbon configuration ({@code IdentityConfigParser}). + * Call once during component activation; the result is immutable. + */ + public FlowContextHandoverConfig() { + + Set attrs = readList(FlowContextHandoverConstants.INCLUDED_ATTRIBUTES_KEY); + Set userAttrs = readList(FlowContextHandoverConstants.INCLUDED_USER_ATTRIBUTES_KEY); + + if (attrs.isEmpty()) { + // Permissive fallback — no config present, expose everything. + LOG.warn("No 'FlowExecutionContextHandover.IncludedAttributes' configured in identity.xml. " + + "Falling back to permissive mode (all attributes exposed)."); + FlowContextHandoverConfig permissive = permissive(); + this.includedAttributes = permissive.includedAttributes; + this.includedUserAttributes = permissive.includedUserAttributes; + this.fullUserPassthrough = permissive.fullUserPassthrough; + return; + } + + this.includedAttributes = Collections.unmodifiableSet(attrs); + this.includedUserAttributes = Collections.unmodifiableSet(userAttrs); + this.fullUserPassthrough = attrs.contains(FlowContextHandoverConstants.ATTR_FLOW_USER); + + if (LOG.isDebugEnabled()) { + LOG.debug("FlowContextHandoverConfig loaded. includedAttributes=" + includedAttributes + + ", includedUserAttributes=" + includedUserAttributes + + ", fullUserPassthrough=" + fullUserPassthrough); + } + } + + /** + * Private constructor used by {@link #permissive()}. + */ + private FlowContextHandoverConfig(Set includedAttributes, + Set includedUserAttributes, + boolean fullUserPassthrough) { + + this.includedAttributes = Collections.unmodifiableSet(includedAttributes); + this.includedUserAttributes = Collections.unmodifiableSet(includedUserAttributes); + this.fullUserPassthrough = fullUserPassthrough; + } + + /** + * Factory that creates a permissive config allowing all known attributes on both + * {@code FlowExecutionContext} and {@code FlowUser}. Used as a safe fallback when no + * configuration is present, preserving behaviour for servers upgrading without updating + * deployment.toml. + * + * @return a permissive {@link FlowContextHandoverConfig}. + */ + public static FlowContextHandoverConfig permissive() { + + Set allContext = new HashSet<>(FlowExecutionContextFilter.getKnownContextAttributes()); + Set allUser = new HashSet<>(FlowExecutionContextFilter.getKnownUserAttributes()); + return new FlowContextHandoverConfig(allContext, allUser, false); + } + + /** + * Top-level property names of {@code FlowExecutionContext} that are included in the + * filtered copy handed to downstream consumers. + * + * @return unmodifiable set of attribute names. + */ + public Set getIncludedAttributes() { + + return includedAttributes; + } + + /** + * Property names of {@code FlowUser} that are included in the filtered copy. + * Ignored when {@link #isFullUserPassthrough()} is {@code true}. + * + * @return unmodifiable set of user attribute names. + */ + public Set getIncludedUserAttributes() { + + return includedUserAttributes; + } + + /** + * Returns {@code true} when {@code "flowUser"} is present in {@code includedAttributes}, + * meaning the entire {@code FlowUser} object is passed through and + * {@code includedUserAttributes} is not consulted. + * + * @return whether the full FlowUser passes through unchanged. + */ + public boolean isFullUserPassthrough() { + + return fullUserPassthrough; + } + + // ---- private helpers ---- + + @SuppressWarnings("unchecked") + private static Set readList(String key) { + + Object raw = IdentityConfigParser.getInstance().getConfiguration().get(key); + List items = new ArrayList<>(); + if (raw instanceof List) { + items = (List) raw; + } else if (raw instanceof String) { + String s = ((String) raw).trim(); + if (!s.isEmpty()) { + items.add(s); + } + } + Set result = new HashSet<>(); + for (String item : items) { + if (item != null && !item.trim().isEmpty()) { + result.add(item.trim()); + } + } + return result; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConstants.java new file mode 100644 index 000000000000..8a8dfe50ede1 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConstants.java @@ -0,0 +1,47 @@ +/* + * 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.config; + +/** + * Constants used by the flow execution context handover filtering. + */ +public final class FlowContextHandoverConstants { + + private FlowContextHandoverConstants() { + + } + + // ---- identity.xml config keys (used by IdentityUtil.getPropertyAsList) ---- + public static final String HANDOVER_ROOT = "FlowExecutionContextHandover"; + public static final String INCLUDED_ATTRIBUTES_KEY = + HANDOVER_ROOT + ".IncludedAttributes.IncludedAttribute"; + public static final String INCLUDED_USER_ATTRIBUTES_KEY = + HANDOVER_ROOT + ".IncludedUserAttributes.IncludedUserAttribute"; + + // ---- Special attribute names ---- + /** When present in {@code includedAttributes}, the entire {@code FlowUser} passes through + * and {@code includedUserAttributes} is ignored. */ + public static final String ATTR_FLOW_USER = "flowUser"; + + /** Engine-internal flow id; always copied regardless of configuration. */ + public static final String ATTR_CONTEXT_IDENTIFIER = "contextIdentifier"; + + /** User-credentials property name; the only Map field requiring per-entry char[] cloning. */ + public static final String ATTR_USER_CREDENTIALS = "userCredentials"; +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilter.java new file mode 100644 index 000000000000..09b1f02c2748 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilter.java @@ -0,0 +1,195 @@ +/* + * 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.config; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; +import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; + +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * Builds a filtered defensive copy of a {@link FlowExecutionContext} containing only the + * attributes permitted by the supplied {@link FlowContextHandoverConfig}. Used at the + * engine boundary by downstream consumers (e.g. the In-Flow Extension executor) to bound + * the data surface visible to external code. + * + *

      Implementation reflects over the JavaBean property descriptors of + * {@link FlowExecutionContext} and {@link FlowUser}, so any new getter/setter pair added + * to those models is automatically eligible for whitelisting via deployment.toml without + * a filter change. The original context is never mutated; non-permitted attributes are + * left null/empty on the copy.

      + * + *

      Map fields receive a defensive shallow {@link HashMap} copy. The {@code userCredentials} + * field receives a per-entry {@code char[]} clone so that the request builder's + * post-extraction {@code Arrays.fill(...,'\0')} wipe zeroes the copy, not the source.

      + */ +public final class FlowExecutionContextFilter { + + private static final Log LOG = LogFactory.getLog(FlowExecutionContextFilter.class); + + private static final Map CONTEXT_PROPERTIES; + private static final Map USER_PROPERTIES; + + static { + CONTEXT_PROPERTIES = Collections.unmodifiableMap(introspect(FlowExecutionContext.class)); + USER_PROPERTIES = Collections.unmodifiableMap(introspect(FlowUser.class)); + } + + private FlowExecutionContextFilter() { + + } + + /** + * Build a filtered copy of {@code original} according to {@code config}. + * + * @param original the source context (untouched). + * @param config the handover config; if null, a permissive fallback is used. + * @return a new {@link FlowExecutionContext} carrying only whitelisted attributes, + * or {@code null} if {@code original} is {@code null}. + */ + public static FlowExecutionContext filter(FlowExecutionContext original, FlowContextHandoverConfig config) { + + if (original == null) { + return null; + } + if (config == null) { + config = FlowContextHandoverConfig.permissive(); + } + + FlowExecutionContext copy = new FlowExecutionContext(); + + // contextIdentifier is engine-internal and always propagated. + copy.setContextIdentifier(original.getContextIdentifier()); + + // Top-level attributes (skip flowUser — handled separately below; + // skip contextIdentifier — already copied). + for (String name : config.getIncludedAttributes()) { + if (FlowContextHandoverConstants.ATTR_FLOW_USER.equals(name) + || FlowContextHandoverConstants.ATTR_CONTEXT_IDENTIFIER.equals(name)) { + continue; + } + copyProperty(CONTEXT_PROPERTIES, name, original, copy); + } + + // User attributes — a fresh non-null FlowUser is always set on the copy so that + // request builders / response processors don't have to null-guard the user. + FlowUser dstUser = new FlowUser(); + copy.setFlowUser(dstUser); + FlowUser srcUser = original.getFlowUser(); + if (srcUser != null) { + Set userAttrs = config.isFullUserPassthrough() + ? USER_PROPERTIES.keySet() + : config.getIncludedUserAttributes(); + for (String name : userAttrs) { + copyProperty(USER_PROPERTIES, name, srcUser, dstUser); + } + } + + return copy; + } + + /** + * Known JavaBean property names on {@link FlowExecutionContext} (read+writable). + * Used by {@link FlowContextHandoverConfig#permissive()} to seed an "everything allowed" + * baseline. + */ + public static Set getKnownContextAttributes() { + + return CONTEXT_PROPERTIES.keySet(); + } + + /** + * Known JavaBean property names on {@link FlowUser} (read+writable). + */ + public static Set getKnownUserAttributes() { + + return USER_PROPERTIES.keySet(); + } + + private static void copyProperty(Map descriptors, String name, + T source, T destination) { + + PropertyDescriptor pd = descriptors.get(name); + if (pd == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Skipping unknown handover attribute: " + name); + } + return; + } + Method reader = pd.getReadMethod(); + Method writer = pd.getWriteMethod(); + if (reader == null || writer == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Skipping handover attribute without readable+writable accessors: " + name); + } + return; + } + try { + Object value = reader.invoke(source); + value = defensivelyCopy(name, value); + writer.invoke(destination, value); + } catch (IllegalAccessException | InvocationTargetException e) { + LOG.warn("Failed to copy handover attribute '" + name + "': " + e.getMessage()); + } + } + + @SuppressWarnings("unchecked") + private static Object defensivelyCopy(String name, Object value) { + + if (!(value instanceof Map)) { + return value; + } + if (FlowContextHandoverConstants.ATTR_USER_CREDENTIALS.equals(name)) { + Map src = (Map) value; + Map out = new LinkedHashMap<>(); + for (Map.Entry entry : src.entrySet()) { + char[] v = entry.getValue(); + out.put(entry.getKey(), v == null ? null : v.clone()); + } + return out; + } + return new HashMap<>((Map) value); + } + + private static Map introspect(Class beanClass) { + + Map result = new HashMap<>(); + try { + for (PropertyDescriptor pd : Introspector.getBeanInfo(beanClass, Object.class).getPropertyDescriptors()) { + if (pd.getReadMethod() != null && pd.getWriteMethod() != null) { + result.put(pd.getName(), pd); + } + } + } catch (IntrospectionException e) { + LOG.error("Failed to introspect " + beanClass.getName() + " for handover filtering.", e); + } + return result; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java index 7a2a5fc6cf4e..31ef18547817 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java @@ -21,6 +21,7 @@ import org.wso2.carbon.identity.application.mgt.ApplicationManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.event.services.IdentityEventService; +import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; import org.wso2.carbon.identity.flow.execution.engine.listener.FlowExecutionListener; import org.wso2.carbon.identity.flow.mgt.FlowMgtService; @@ -48,6 +49,7 @@ public class FlowExecutionEngineDataHolder { private FederatedAssociationManager federatedAssociationManager; private IdentityEventService identityEventService; private List flowExecutionListeners = new ArrayList<>(); + private FlowContextHandoverConfig flowContextHandoverConfig; private FlowExecutionEngineDataHolder() { @@ -219,4 +221,26 @@ public void setIdentityEventService(IdentityEventService identityEventService) { this.identityEventService = identityEventService; } + + /** + * Get the flow execution context handover config. Lazily initialised on first access + * so that {@code IdentityConfigParser} is guaranteed to be loaded before we read it. + * + * @return the handover config, never null. + */ + public synchronized FlowContextHandoverConfig getFlowContextHandoverConfig() { + + if (flowContextHandoverConfig == null) { + flowContextHandoverConfig = new FlowContextHandoverConfig(); + } + return flowContextHandoverConfig; + } + + /** + * Override the handover config. Intended for tests only. + */ + public synchronized void setFlowContextHandoverConfig(FlowContextHandoverConfig flowContextHandoverConfig) { + + this.flowContextHandoverConfig = flowContextHandoverConfig; + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java index 1583423b015d..f8c7ad362b63 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java @@ -109,6 +109,14 @@ public void addClaims(Map claims) { this.claims.putAll(claims); } + public void setClaims(Map claims) { + + this.claims.clear(); + if (claims != null) { + this.claims.putAll(claims); + } + } + public Object getClaim(String claimUri) { return this.claims.get(claimUri); @@ -160,6 +168,14 @@ public void addFederatedAssociation(String idpName, String idpSubject) { this.federatedAssociations.put(idpName, idpSubject); } + public void setFederatedAssociations(Map federatedAssociations) { + + this.federatedAssociations.clear(); + if (federatedAssociations != null) { + this.federatedAssociations.putAll(federatedAssociations); + } + } + /** * Check whether the user credentials are managed locally. * diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTest.java new file mode 100644 index 000000000000..b5c3377c62b6 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTest.java @@ -0,0 +1,153 @@ +/* + * 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.config; + +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +/** + * Unit tests for {@link FlowContextHandoverConfig}. + * + *

      Tests that do not read {@code identity.xml} use + * {@link FlowContextHandoverConfigTestHelper} to build instances with explicit allow-lists, + * or use the {@link FlowContextHandoverConfig#permissive()} factory which seeds allow-lists + * from the static maps on {@link FlowExecutionContextFilter}.

      + */ +public class FlowContextHandoverConfigTest { + + // ========================= permissive() factory ========================= + + @Test + public void testPermissiveIncludesAllKnownContextAttributes() { + + FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); + + // At minimum all well-known FlowExecutionContext properties must be present. + Set attrs = cfg.getIncludedAttributes(); + assertNotNull(attrs); + assertFalse(attrs.isEmpty()); + + // These are the canonical FlowExecutionContext property names exposed by the filter. + assertTrue(attrs.contains("tenantDomain"), "permissive missing tenantDomain"); + assertTrue(attrs.contains("applicationId"), "permissive missing applicationId"); + assertTrue(attrs.contains("flowType"), "permissive missing flowType"); + assertTrue(attrs.contains("callbackUrl"), "permissive missing callbackUrl"); + assertTrue(attrs.contains("portalUrl"), "permissive missing portalUrl"); + assertTrue(attrs.contains("properties"), "permissive missing properties"); + } + + @Test + public void testPermissiveIncludesAllKnownUserAttributes() { + + FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); + + Set userAttrs = cfg.getIncludedUserAttributes(); + assertNotNull(userAttrs); + assertFalse(userAttrs.isEmpty()); + + // Canonical FlowUser property names. + assertTrue(userAttrs.contains("username"), "permissive missing username"); + assertTrue(userAttrs.contains("userId"), "permissive missing userId"); + assertTrue(userAttrs.contains("userStoreDomain"), "permissive missing userStoreDomain"); + assertTrue(userAttrs.contains("claims"), "permissive missing claims"); + assertTrue(userAttrs.contains("userCredentials"), "permissive missing userCredentials"); + } + + @Test + public void testPermissiveDoesNotSetFullUserPassthrough() { + + // permissive() does NOT put "flowUser" in includedAttributes — it seeds individual + // user attributes instead. So isFullUserPassthrough() must be false. + FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); + assertFalse(cfg.isFullUserPassthrough()); + } + + // ========================= fullUserPassthrough flag ========================= + + @Test + public void testFullUserPassthroughTrueWhenFlowUserInAttrs() { + + Set attrs = new HashSet<>(Arrays.asList("flowUser", "tenantDomain")); + FlowContextHandoverConfig cfg = FlowContextHandoverConfigTestHelper.of(attrs, new HashSet<>()); + + assertTrue(cfg.isFullUserPassthrough()); + } + + @Test + public void testFullUserPassthroughFalseWhenFlowUserAbsent() { + + Set attrs = new HashSet<>(Arrays.asList("tenantDomain", "applicationId")); + FlowContextHandoverConfig cfg = FlowContextHandoverConfigTestHelper.of(attrs, new HashSet<>()); + + assertFalse(cfg.isFullUserPassthrough()); + } + + // ========================= allow-list contents ========================= + + @Test + public void testIncludedAttributesReflectInput() { + + Set attrs = new HashSet<>(Arrays.asList("tenantDomain", "properties")); + Set userAttrs = new HashSet<>(Arrays.asList("username", "claims")); + + FlowContextHandoverConfig cfg = FlowContextHandoverConfigTestHelper.of(attrs, userAttrs); + + assertTrue(cfg.getIncludedAttributes().contains("tenantDomain")); + assertTrue(cfg.getIncludedAttributes().contains("properties")); + assertFalse(cfg.getIncludedAttributes().contains("applicationId")); + + assertTrue(cfg.getIncludedUserAttributes().contains("username")); + assertTrue(cfg.getIncludedUserAttributes().contains("claims")); + assertFalse(cfg.getIncludedUserAttributes().contains("userId")); + } + + @Test + public void testIncludedAttributesSetIsUnmodifiable() { + + FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); + + try { + cfg.getIncludedAttributes().add("injected"); + // If no exception, check the value wasn't actually added (some impls are backed + // by unmodifiable wrappers that silently discard — but the API contract is that + // clients must not rely on mutation, so either throwing or silent discard is fine). + } catch (UnsupportedOperationException e) { + // Expected — the set is unmodifiable. + } + } + + @Test + public void testIncludedUserAttributesSetIsUnmodifiable() { + + FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); + + try { + cfg.getIncludedUserAttributes().add("injected"); + } catch (UnsupportedOperationException e) { + // Expected. + } + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTestHelper.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTestHelper.java new file mode 100644 index 000000000000..ca82b4ae59a7 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTestHelper.java @@ -0,0 +1,54 @@ +/* + * 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.config; + +import java.lang.reflect.Constructor; +import java.util.Set; + +/** + * Test-only helper that instantiates {@link FlowContextHandoverConfig} with explicit + * allow-lists without touching {@code IdentityConfigParser}. Lives in the same package + * as the production class so it can access the package-private constructor via reflection + * from a predictable location. + */ +public final class FlowContextHandoverConfigTestHelper { + + private FlowContextHandoverConfigTestHelper() { + + } + + /** + * Construct a {@link FlowContextHandoverConfig} with the given allow-lists. + * {@code fullUserPassthrough} is derived from whether {@code "flowUser"} is in + * {@code attrs}, mirroring the production constructor logic. + */ + public static FlowContextHandoverConfig of(Set attrs, Set userAttrs) { + + try { + Constructor ctor = + FlowContextHandoverConfig.class.getDeclaredConstructor( + Set.class, Set.class, boolean.class); + ctor.setAccessible(true); + boolean fullPassthrough = attrs.contains(FlowContextHandoverConstants.ATTR_FLOW_USER); + return ctor.newInstance(attrs, userAttrs, fullPassthrough); + } catch (Exception e) { + throw new RuntimeException("Cannot construct FlowContextHandoverConfig for test", e); + } + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowExecutionContextFilterTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilterTest.java similarity index 51% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowExecutionContextFilterTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilterTest.java index 76515fe41f53..b41ca4a920cc 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowExecutionContextFilterTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilterTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.config; +package org.wso2.carbon.identity.flow.execution.engine.config; import org.testng.annotations.Test; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; @@ -24,7 +24,9 @@ import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -32,21 +34,30 @@ import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; +/** + * Unit tests for {@link FlowExecutionContextFilter}. + * + *

      Uses the {@link FlowContextHandoverConfig#permissive()} factory and the private + * constructor variant (accessed via a local helper) to avoid touching + * {@code IdentityConfigParser} — no OSGi or Mockito static setup required.

      + */ public class FlowExecutionContextFilterTest { + // ========================= null guards ========================= + @Test public void testNullOriginalReturnsNull() { - assertNull(FlowExecutionContextFilter.filter(null, FlowContextHandoverPolicy.permissive())); + assertNull(FlowExecutionContextFilter.filter(null, FlowContextHandoverConfig.permissive())); } @Test - public void testNullPolicyFallsBackToPermissive() { + public void testNullConfigFallsBackToPermissive() { FlowExecutionContext orig = createFullyPopulatedContext(); FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, null); - // Permissive fallback — every field should round-trip. + // Permissive fallback — every whitelisted field should round-trip. assertEquals(copy.getTenantDomain(), "carbon.super"); assertEquals(copy.getApplicationId(), "app-1"); assertEquals(copy.getFlowType(), "REGISTRATION"); @@ -57,29 +68,32 @@ public void testNullPolicyFallsBackToPermissive() { assertEquals(copy.getProperties().get("p1"), "v1"); } + // ========================= contextIdentifier ========================= + @Test public void testContextIdentifierAlwaysCopied() { FlowExecutionContext orig = createFullyPopulatedContext(); - // Even with the most restrictive policy, contextIdentifier must round-trip — it's the - // engine-internal flow id, not user data. - FlowContextHandoverPolicy strict = FlowContextHandoverPolicy.builder().build(); + // Even with an empty allow-list contextIdentifier must round-trip. + FlowContextHandoverConfig strict = configFrom(new HashSet<>(), new HashSet<>()); FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, strict); assertEquals(copy.getContextIdentifier(), "ctx-1"); } + // ========================= flow-branch toggles ========================= + @Test - public void testFlowDisabledOmitsAllFlowFields() { + public void testAllFlowAttrsExcludedWhenNoneInAllowList() { FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.builder() - .startingFrom(FlowContextHandoverPolicy.permissive()) - .flowEnabled(false) - .build(); + // Allow-list has only user attrs — no flow attrs. + FlowContextHandoverConfig cfg = configFrom( + new HashSet<>(Arrays.asList("username", "claims")), + new HashSet<>(Arrays.asList("username", "claims"))); - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); assertNull(copy.getTenantDomain()); assertNull(copy.getApplicationId()); @@ -89,19 +103,14 @@ public void testFlowDisabledOmitsAllFlowFields() { } @Test - public void testPerLeafFlowToggles() { + public void testFlowAttrSelective() { FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.builder() - .flowEnabled(true) - .flowTenantDomain(true) - .flowApplicationId(false) // off - .flowFlowType(true) - .flowCallbackUrl(false) // off - .flowPortalUrl(true) - .build(); + FlowContextHandoverConfig cfg = configFrom( + new HashSet<>(Arrays.asList("tenantDomain", "flowType", "portalUrl")), + new HashSet<>()); - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); assertEquals(copy.getTenantDomain(), "carbon.super"); assertNull(copy.getApplicationId()); @@ -110,53 +119,72 @@ public void testPerLeafFlowToggles() { assertEquals(copy.getPortalUrl(), "https://portal"); } + // ========================= user-branch toggles ========================= + @Test - public void testUserDisabledYieldsEmptyFlowUser() { + public void testUserAbsentFromAllowListYieldsEmptyFlowUser() { FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.builder() - .startingFrom(FlowContextHandoverPolicy.permissive()) - .userEnabled(false) - .build(); + // No "flowUser" and no user attrs — user branch omitted. + FlowContextHandoverConfig cfg = configFrom( + new HashSet<>(Arrays.asList("tenantDomain")), + new HashSet<>()); - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - // Must be a fresh non-null FlowUser — the response processor relies on this when - // applying pendingClaims/pendingCredentials on second-call SUCCESS responses. + // FlowUser must be a non-null empty shell so downstream code doesn't NPE. assertNotNull(copy.getFlowUser()); assertNull(copy.getFlowUser().getUserId()); assertNull(copy.getFlowUser().getUserStoreDomain()); - assertTrue(copy.getFlowUser().getClaims().isEmpty()); - assertTrue(copy.getFlowUser().getUserCredentials().isEmpty()); + assertTrue(copy.getFlowUser().getClaims() == null || copy.getFlowUser().getClaims().isEmpty()); + assertTrue(copy.getFlowUser().getUserCredentials() == null + || copy.getFlowUser().getUserCredentials().isEmpty()); } @Test public void testPerLeafUserToggles() { FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.builder() - .userEnabled(true) - .userUserId(false) // off - .userUserStoreDomain(true) - .userClaims(true) - .userCredentials(false) // off - .build(); + FlowContextHandoverConfig cfg = configFrom( + new HashSet<>(Arrays.asList("flowUser")), // flowUser in context attrs + new HashSet<>(Arrays.asList("userStoreDomain", "claims"))); // userId excluded - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); + + // fullUserPassthrough == true because "flowUser" is in includedAttributes + // → entire user is passed through regardless of includedUserAttributes + assertEquals(copy.getFlowUser().getUserId(), "user-1"); + assertEquals(copy.getFlowUser().getUserStoreDomain(), "PRIMARY"); + assertEquals(copy.getFlowUser().getClaims().get("http://wso2.org/claims/email"), "u@e.com"); + } + + @Test + public void testIndividualUserAttrSelection() { + + FlowExecutionContext orig = createFullyPopulatedContext(); + // No "flowUser" — use individual user attr list. + FlowContextHandoverConfig cfg = configFrom( + new HashSet<>(Arrays.asList("tenantDomain")), // no "flowUser" + new HashSet<>(Arrays.asList("userStoreDomain", "claims"))); + + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); assertNull(copy.getFlowUser().getUserId()); assertEquals(copy.getFlowUser().getUserStoreDomain(), "PRIMARY"); assertEquals(copy.getFlowUser().getClaims().get("http://wso2.org/claims/email"), "u@e.com"); - assertTrue(copy.getFlowUser().getUserCredentials().isEmpty()); + assertTrue(copy.getFlowUser().getUserCredentials() == null + || copy.getFlowUser().getUserCredentials().isEmpty()); } + // ========================= defensive copies ========================= + @Test public void testCredentialsAreDeepCopied() { FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.permissive(); + FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); char[] origPwd = orig.getFlowUser().getUserCredentials().get("password"); char[] copyPwd = copy.getFlowUser().getUserCredentials().get("password"); @@ -164,9 +192,7 @@ public void testCredentialsAreDeepCopied() { assertNotSame(origPwd, copyPwd); assertEquals(copyPwd, "secret".toCharArray()); - // Wiping the copy must not affect the original — this is the desired post-extraction - // sanitisation behaviour: the request builder's Arrays.fill(...,'\0') zeroes the copy, - // not the original on the live FlowExecutionContext. + // Wiping the copy must not affect the original. Arrays.fill(copyPwd, '\0'); assertEquals(origPwd, "secret".toCharArray()); } @@ -175,51 +201,65 @@ public void testCredentialsAreDeepCopied() { public void testClaimsMapIsDefensivelyCopied() { FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.permissive(); + FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - // Mutating the copy's claims must not leak to the original. copy.getFlowUser().addClaim("http://wso2.org/claims/leak", "oops"); assertNull(orig.getFlowUser().getClaims().get("http://wso2.org/claims/leak")); } @Test - public void testPropertiesDisabledOmitsAll() { + public void testPropertiesAreDefensivelyCopied() { FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.builder() - .startingFrom(FlowContextHandoverPolicy.permissive()) - .propertiesEnabled(false) - .build(); + FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); + + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + copy.setProperty("p2", "leak"); + assertNull(orig.getProperty("p2")); + } + + // ========================= properties branch ========================= + + @Test + public void testPropertiesExcludedWhenNotInAllowList() { + + FlowExecutionContext orig = createFullyPopulatedContext(); + // "properties" not in allow-list. + FlowContextHandoverConfig cfg = configFrom( + new HashSet<>(Arrays.asList("tenantDomain")), + new HashSet<>()); + + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - // Properties default to an empty map on a fresh FlowExecutionContext. assertTrue(copy.getProperties() == null || copy.getProperties().isEmpty()); } @Test - public void testPropertiesAreDefensivelyCopied() { + public void testPropertiesIncludedWhenInAllowList() { FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverPolicy policy = FlowContextHandoverPolicy.permissive(); + FlowContextHandoverConfig cfg = configFrom( + new HashSet<>(Arrays.asList("properties")), + new HashSet<>()); - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, policy); + FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - copy.setProperty("p2", "leak"); - assertNull(orig.getProperty("p2")); + assertEquals(copy.getProperty("p1"), "v1"); } + // ========================= original untouched ========================= + @Test public void testOriginalUntouchedAcrossAllToggles() { FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverPolicy strict = FlowContextHandoverPolicy.builder().build(); + FlowContextHandoverConfig strict = configFrom(new HashSet<>(), new HashSet<>()); FlowExecutionContextFilter.filter(orig, strict); - // Original must be byte-for-byte equivalent after filtering. assertEquals(orig.getTenantDomain(), "carbon.super"); assertEquals(orig.getApplicationId(), "app-1"); assertEquals(orig.getFlowType(), "REGISTRATION"); @@ -229,6 +269,31 @@ public void testOriginalUntouchedAcrossAllToggles() { assertEquals(orig.getProperty("p1"), "v1"); } + // ========================= helpers ========================= + + /** + * Build a minimal {@link FlowContextHandoverConfig} from raw sets without touching + * {@code IdentityConfigParser}. Uses the permissive factory as a baseline and overrides + * via reflection would be heavy — instead we rely on the fact that + * {@code FlowContextHandoverConfig.permissive()} already covers the all-on case, and + * create targeted configs via a thin wrapper that directly exercises the filter with + * controlled allow-lists by passing them to the filter via the engine's {@code permissive()} + * overridden in a subclass if needed. + * + *

      Since {@link FlowContextHandoverConfig} has no public constructor taking sets, we + * call {@code permissive()} as the base and then exercise filter behaviour by passing + * allow-list information through a thin anonymous subclass. However, the class is + * {@code final}, so we instead test the filter contract indirectly by constructing real + * configs through the only available factory that doesn't read identity.xml.

      + * + *

      For selective allow-list tests we use a package-private test helper in the same + * package ({@code engine.config}) that exposes the private constructor.

      + */ + private static FlowContextHandoverConfig configFrom(Set attrs, Set userAttrs) { + + return FlowContextHandoverConfigTestHelper.of(attrs, userAttrs); + } + private FlowExecutionContext createFullyPopulatedContext() { FlowExecutionContext ctx = new FlowExecutionContext(); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml index e76f17cabd37..a7edbb0325fb 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml @@ -19,6 +19,12 @@ + + + + + + diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml index 5937b4269f80..543f50d74a60 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml @@ -97,6 +97,11 @@ mockito-testng test
      + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.application.authentication.framework + test + org.wso2.carbon.identity.framework org.wso2.carbon.identity.testutil @@ -152,6 +157,10 @@ javax.servlet.http; version="${imp.pkg.version.javax.servlet}", org.wso2.carbon.identity.flow.execution.engine; version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.flow.execution.engine.config; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.flow.execution.engine.internal; + version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.flow.execution.engine.exception; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.flow.execution.engine.graph; @@ -195,7 +204,6 @@ org.wso2.carbon.identity.flow.inflow.extensions.executor, org.wso2.carbon.identity.flow.inflow.extensions.model, org.wso2.carbon.identity.flow.inflow.extensions.management, - org.wso2.carbon.identity.flow.inflow.extensions.config, org.wso2.carbon.identity.flow.inflow.extensions.metadata; version="${carbon.identity.package.export.version}" diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java index 273a9f9dd122..2f276da61e93 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java @@ -33,7 +33,6 @@ private InFlowExtensionConstants() { // ---- FlowContext pipeline keys ---- public static final String FLOW_EXECUTION_CONTEXT_KEY = "flowExecutionContext"; - public static final String HANDOVER_POLICY_KEY = "handoverPolicy"; public static final String PATH_TYPE_ANNOTATIONS_KEY = "pathTypeAnnotations"; public static final String MODIFY_PATHS_KEY = "modifyPaths"; public static final String PENDING_CLAIMS_KEY = "pendingClaims"; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverConfig.java deleted file mode 100644 index 309ee1afc86d..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverConfig.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.inflow.extensions.config; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.wso2.carbon.identity.core.util.IdentityUtil; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - * Loads and exposes the {@link FlowContextHandoverPolicy} for each known flow type from - * {@code identity.xml} (which is itself templated from {@code deployment.toml}). The j2 - * template performs deep-merge inheritance from the {@code default} section using chained - * {@code default(...)} Jinja filters, so by the time this loader runs every leaf is fully - * resolved and the loader just reads the literal value. - * - *

      Per-flow-type policies are keyed by {@code FlowTypes.getType()} (e.g., - * {@code "REGISTRATION"}). Unknown flow types fall through to {@link #getDefaultPolicy()}.

      - */ -public class FlowContextHandoverConfig { - - private static final Log LOG = LogFactory.getLog(FlowContextHandoverConfig.class); - - // Section names under in identity.xml. - private static final String ROOT = "InFlowExtensionContextHandover"; - private static final String SECTION_DEFAULT = "Default"; - private static final String SECTION_REGISTRATION = "Registration"; - private static final String SECTION_PASSWORD_RECOVERY = "PasswordRecovery"; - private static final String SECTION_INVITED_USER_REGISTRATION = "InvitedUserRegistration"; - - // Mapping XML section name → FlowTypes enum value (the keys we'll look up at runtime). - private static final Map XML_SECTION_TO_FLOW_TYPE; - - static { - Map map = new HashMap<>(); - map.put(SECTION_REGISTRATION, "REGISTRATION"); - map.put(SECTION_PASSWORD_RECOVERY, "PASSWORD_RECOVERY"); - map.put(SECTION_INVITED_USER_REGISTRATION, "INVITED_USER_REGISTRATION"); - XML_SECTION_TO_FLOW_TYPE = Collections.unmodifiableMap(map); - } - - private final FlowContextHandoverPolicy defaultPolicy; - private final Map perFlowTypePolicies; - - /** - * Eager-load all policies from {@code IdentityUtil}. Call once at component startup. - */ - public FlowContextHandoverConfig() { - - this.defaultPolicy = readPolicy(SECTION_DEFAULT); - Map perType = new HashMap<>(); - for (Map.Entry entry : XML_SECTION_TO_FLOW_TYPE.entrySet()) { - perType.put(entry.getValue(), readPolicy(entry.getKey())); - } - this.perFlowTypePolicies = Collections.unmodifiableMap(perType); - - if (LOG.isDebugEnabled()) { - LOG.debug("Loaded In-Flow Extension context handover policies: default + " - + perFlowTypePolicies.keySet()); - } - } - - /** - * Resolve the policy for the given flow type. Falls back to the default policy if the - * flow type is unknown or null. - */ - public FlowContextHandoverPolicy resolve(String flowType) { - - if (flowType == null) { - return defaultPolicy; - } - FlowContextHandoverPolicy policy = perFlowTypePolicies.get(flowType); - return policy != null ? policy : defaultPolicy; - } - - public FlowContextHandoverPolicy getDefaultPolicy() { - - return defaultPolicy; - } - - private FlowContextHandoverPolicy readPolicy(String section) { - - String prefix = ROOT + "." + section + "."; - return FlowContextHandoverPolicy.builder() - .redirectionEnabled(readBool(prefix + "Redirection.Enable", true)) - .userEnabled(readBool(prefix + "User.Enable", true)) - .userUserId(readBool(prefix + "User.UserId", false)) - .userUserStoreDomain(readBool(prefix + "User.UserStoreDomain", true)) - .userClaims(readBool(prefix + "User.Claims", true)) - .userCredentials(readBool(prefix + "User.Credentials", false)) - .flowEnabled(readBool(prefix + "Flow.Enable", true)) - .flowTenantDomain(readBool(prefix + "Flow.TenantDomain", true)) - .flowApplicationId(readBool(prefix + "Flow.ApplicationId", true)) - .flowFlowType(readBool(prefix + "Flow.FlowType", true)) - .flowCallbackUrl(readBool(prefix + "Flow.CallbackUrl", true)) - .flowPortalUrl(readBool(prefix + "Flow.PortalUrl", true)) - .propertiesEnabled(readBool(prefix + "Properties.Enable", true)) - .build(); - } - - private static boolean readBool(String key, boolean fallback) { - - String value = IdentityUtil.getProperty(key); - if (value == null || value.isEmpty()) { - return fallback; - } - return Boolean.parseBoolean(value.trim()); - } -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverPolicy.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverPolicy.java deleted file mode 100644 index 4c5b285532d3..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverPolicy.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * 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.inflow.extensions.config; - -/** - * Immutable per-flow-type policy controlling which {@code FlowExecutionContext} fields the - * In-Flow Extension executor hands over to the action framework, plus whether REDIRECT is - * advertised in {@code allowedOperations}. - * - *

      Sourced from {@code [identity.in_flow_extension.context.{flowType}]} in - * {@code deployment.toml}. A flow-type-specific policy {@link Builder#startingFrom inherits} - * from the {@code default} policy for unspecified keys.

      - */ -public final class FlowContextHandoverPolicy { - - private final boolean redirectionEnabled; - - private final boolean userEnabled; - private final boolean userUserId; - private final boolean userUserStoreDomain; - private final boolean userClaims; - private final boolean userCredentials; - - private final boolean flowEnabled; - private final boolean flowTenantDomain; - private final boolean flowApplicationId; - private final boolean flowFlowType; - private final boolean flowCallbackUrl; - private final boolean flowPortalUrl; - - private final boolean propertiesEnabled; - - private FlowContextHandoverPolicy(Builder b) { - - this.redirectionEnabled = b.redirectionEnabled; - this.userEnabled = b.userEnabled; - this.userUserId = b.userUserId; - this.userUserStoreDomain = b.userUserStoreDomain; - this.userClaims = b.userClaims; - this.userCredentials = b.userCredentials; - this.flowEnabled = b.flowEnabled; - this.flowTenantDomain = b.flowTenantDomain; - this.flowApplicationId = b.flowApplicationId; - this.flowFlowType = b.flowFlowType; - this.flowCallbackUrl = b.flowCallbackUrl; - this.flowPortalUrl = b.flowPortalUrl; - this.propertiesEnabled = b.propertiesEnabled; - } - - public boolean isRedirectionEnabled() { - - return redirectionEnabled; - } - - public boolean isUserEnabled() { - - return userEnabled; - } - - public boolean isUserUserId() { - - return userUserId; - } - - public boolean isUserUserStoreDomain() { - - return userUserStoreDomain; - } - - public boolean isUserClaims() { - - return userClaims; - } - - public boolean isUserCredentials() { - - return userCredentials; - } - - public boolean isFlowEnabled() { - - return flowEnabled; - } - - public boolean isFlowTenantDomain() { - - return flowTenantDomain; - } - - public boolean isFlowApplicationId() { - - return flowApplicationId; - } - - public boolean isFlowFlowType() { - - return flowFlowType; - } - - public boolean isFlowCallbackUrl() { - - return flowCallbackUrl; - } - - public boolean isFlowPortalUrl() { - - return flowPortalUrl; - } - - public boolean isPropertiesEnabled() { - - return propertiesEnabled; - } - - /** - * Permissive default — every field allowed. Used as the seed for building the - * deployment-configured "default" policy and as the safety net when no config is loaded. - */ - public static FlowContextHandoverPolicy permissive() { - - return new Builder().allowAll().build(); - } - - public static Builder builder() { - - return new Builder(); - } - - /** - * Builder for {@link FlowContextHandoverPolicy}. - * - *

      Defaults: every field disabled. Call {@link #allowAll()} to start from the permissive - * baseline, or {@link #startingFrom(FlowContextHandoverPolicy)} to inherit from another - * policy (used for per-flow-type overrides that fill unspecified keys from {@code default}).

      - */ - public static final class Builder { - - private boolean redirectionEnabled; - private boolean userEnabled; - private boolean userUserId; - private boolean userUserStoreDomain; - private boolean userClaims; - private boolean userCredentials; - private boolean flowEnabled; - private boolean flowTenantDomain; - private boolean flowApplicationId; - private boolean flowFlowType; - private boolean flowCallbackUrl; - private boolean flowPortalUrl; - private boolean propertiesEnabled; - - public Builder allowAll() { - - this.redirectionEnabled = true; - this.userEnabled = true; - this.userUserId = true; - this.userUserStoreDomain = true; - this.userClaims = true; - this.userCredentials = true; - this.flowEnabled = true; - this.flowTenantDomain = true; - this.flowApplicationId = true; - this.flowFlowType = true; - this.flowCallbackUrl = true; - this.flowPortalUrl = true; - this.propertiesEnabled = true; - return this; - } - - public Builder startingFrom(FlowContextHandoverPolicy base) { - - this.redirectionEnabled = base.redirectionEnabled; - this.userEnabled = base.userEnabled; - this.userUserId = base.userUserId; - this.userUserStoreDomain = base.userUserStoreDomain; - this.userClaims = base.userClaims; - this.userCredentials = base.userCredentials; - this.flowEnabled = base.flowEnabled; - this.flowTenantDomain = base.flowTenantDomain; - this.flowApplicationId = base.flowApplicationId; - this.flowFlowType = base.flowFlowType; - this.flowCallbackUrl = base.flowCallbackUrl; - this.flowPortalUrl = base.flowPortalUrl; - this.propertiesEnabled = base.propertiesEnabled; - return this; - } - - public Builder redirectionEnabled(boolean v) { - - this.redirectionEnabled = v; - return this; - } - - public Builder userEnabled(boolean v) { - - this.userEnabled = v; - return this; - } - - public Builder userUserId(boolean v) { - - this.userUserId = v; - return this; - } - - public Builder userUserStoreDomain(boolean v) { - - this.userUserStoreDomain = v; - return this; - } - - public Builder userClaims(boolean v) { - - this.userClaims = v; - return this; - } - - public Builder userCredentials(boolean v) { - - this.userCredentials = v; - return this; - } - - public Builder flowEnabled(boolean v) { - - this.flowEnabled = v; - return this; - } - - public Builder flowTenantDomain(boolean v) { - - this.flowTenantDomain = v; - return this; - } - - public Builder flowApplicationId(boolean v) { - - this.flowApplicationId = v; - return this; - } - - public Builder flowFlowType(boolean v) { - - this.flowFlowType = v; - return this; - } - - public Builder flowCallbackUrl(boolean v) { - - this.flowCallbackUrl = v; - return this; - } - - public Builder flowPortalUrl(boolean v) { - - this.flowPortalUrl = v; - return this; - } - - public Builder propertiesEnabled(boolean v) { - - this.propertiesEnabled = v; - return this; - } - - public FlowContextHandoverPolicy build() { - - return new FlowContextHandoverPolicy(this); - } - } -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowExecutionContextFilter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowExecutionContextFilter.java deleted file mode 100644 index 9acc4c6c5890..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowExecutionContextFilter.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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.inflow.extensions.config; - -import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; -import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; - -import java.util.HashMap; -import java.util.Map; - -/** - * Builds a filtered defensive copy of a {@link FlowExecutionContext} containing only the - * fields permitted by the supplied {@link FlowContextHandoverPolicy}. Used by - * {@code InFlowExtensionExecutor} to bound the surface area visible to the action framework. - * - *

      Non-permitted fields are left null/empty on the copy. The original context is never - * mutated. Only public getters/setters are used so the existing request-builder and - * response-processor code that reads via {@code execCtx.getFoo()} continues to work — it - * just gets {@code null} (or empty maps) for fields the deployment.toml whitelist omits.

      - */ -public final class FlowExecutionContextFilter { - - private FlowExecutionContextFilter() { - - } - - /** - * Build a filtered copy of {@code original} according to {@code policy}. - * - * @param original the original context (untouched). - * @param policy the per-flow handover policy. - * @return a new {@link FlowExecutionContext} carrying only whitelisted fields. - */ - public static FlowExecutionContext filter(FlowExecutionContext original, - FlowContextHandoverPolicy policy) { - - if (original == null) { - return null; - } - if (policy == null) { - // Defensive: no policy → permissive (preserve current behaviour). - policy = FlowContextHandoverPolicy.permissive(); - } - - FlowExecutionContext copy = new FlowExecutionContext(); - - // contextIdentifier is engine-internal and always copied — not a user-data field. - copy.setContextIdentifier(original.getContextIdentifier()); - - // ---- Flow block ---- - if (policy.isFlowEnabled()) { - if (policy.isFlowTenantDomain()) { - copy.setTenantDomain(original.getTenantDomain()); - } - if (policy.isFlowApplicationId()) { - copy.setApplicationId(original.getApplicationId()); - } - if (policy.isFlowFlowType()) { - copy.setFlowType(original.getFlowType()); - } - if (policy.isFlowCallbackUrl()) { - copy.setCallbackUrl(original.getCallbackUrl()); - } - if (policy.isFlowPortalUrl()) { - copy.setPortalUrl(original.getPortalUrl()); - } - } - - // ---- User block ---- - // Always set a non-null FlowUser on the copy. Pending claims/credentials/properties - // collected by the response processor are stashed in FlowContext keys (not on FlowUser) - // and applied to the *original* context by TaskExecutionNode after the executor returns, - // so this empty FlowUser never leaks back into engine state. The empty instance exists - // only as a defensive convenience: it lets the request builder call getClaims()/ - // getUserCredentials() without null-guarding the FlowUser itself. - FlowUser filteredUser = new FlowUser(); - FlowUser originalUser = original.getFlowUser(); - if (policy.isUserEnabled() && originalUser != null) { - if (policy.isUserUserId()) { - filteredUser.setUserId(originalUser.getUserId()); - } - if (policy.isUserUserStoreDomain()) { - filteredUser.setUserStoreDomain(originalUser.getUserStoreDomain()); - } - if (policy.isUserClaims() && originalUser.getClaims() != null) { - // Defensive copy of the claims map. - filteredUser.addClaims(new HashMap<>(originalUser.getClaims())); - } - if (policy.isUserCredentials() && originalUser.getUserCredentials() != null) { - // Defensive copy of credentials — clone each char[] so the request builder's - // post-extraction wipe (Arrays.fill(...,'\0')) zeroes the *copy*, not the original. - Map credentialsCopy = new HashMap<>(); - for (Map.Entry entry : originalUser.getUserCredentials().entrySet()) { - char[] value = entry.getValue(); - credentialsCopy.put(entry.getKey(), value == null ? null : value.clone()); - } - filteredUser.setUserCredentials(credentialsCopy); - } - } - copy.setFlowUser(filteredUser); - - // ---- Properties block ---- - if (policy.isPropertiesEnabled() && original.getProperties() != null) { - copy.setProperties(new HashMap<>(original.getProperties())); - } - - return copy; - } -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java index 8583067b5ce6..315dd7f76a67 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java @@ -34,10 +34,10 @@ import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; import org.wso2.carbon.utils.DiagnosticLog; import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; +import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.execution.engine.config.FlowExecutionContextFilter; import org.wso2.carbon.identity.flow.execution.engine.exception.FlowEngineException; -import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverConfig; -import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverPolicy; -import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowExecutionContextFilter; +import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; import org.wso2.carbon.identity.flow.inflow.extensions.internal.InFlowExtensionDataHolder; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; @@ -141,16 +141,14 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE } try { - // Resolve the per-flow-type handover policy and hand the action framework only a + // Resolve the handover config and hand the action framework only a // FILTERED copy of the FlowExecutionContext (non-whitelisted fields nulled out). - FlowContextHandoverConfig handoverConfig = InFlowExtensionDataHolder.getInstance() + FlowContextHandoverConfig handoverConfig = FlowExecutionEngineDataHolder.getInstance() .getFlowContextHandoverConfig(); - FlowContextHandoverPolicy policy = handoverConfig.resolve(context.getFlowType()); - FlowExecutionContext filteredContext = FlowExecutionContextFilter.filter(context, policy); + FlowExecutionContext filteredContext = FlowExecutionContextFilter.filter(context, handoverConfig); FlowContext flowContext = FlowContext.create() - .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, filteredContext) - .add(InFlowExtensionConstants.HANDOVER_POLICY_KEY, policy); + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, filteredContext); ActionExecutionStatus executionStatus = actionExecutorService.execute( ActionType.IN_FLOW_EXTENSION, actionId, flowContext, context.getTenantDomain()); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilder.java index ed9db5bde7fc..243d0e3e805a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilder.java @@ -43,7 +43,6 @@ import org.wso2.carbon.identity.core.context.IdentityContext; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; -import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverPolicy; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; @@ -220,15 +219,10 @@ private List buildAllowedOperations(AccessConfig accessConfig, } } - // REDIRECT is advertised when the per-flow-type handover policy allows it. A null - // policy always permitting REDIRECT. - FlowContextHandoverPolicy policy = flowContext.getValue( - InFlowExtensionConstants.HANDOVER_POLICY_KEY, FlowContextHandoverPolicy.class); - if (policy == null || policy.isRedirectionEnabled()) { - AllowedOperation redirectOp = new AllowedOperation(); - redirectOp.setOp(Operation.REDIRECT); - allowedOps.add(redirectOp); - } + // REDIRECT is always advertised — redirection is unconditionally enabled. + AllowedOperation redirectOp = new AllowedOperation(); + redirectOp.setOp(Operation.REDIRECT); + allowedOps.add(redirectOp); return allowedOps; } @@ -251,8 +245,6 @@ private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List FLOW_BRANCH_ATTRS = + Arrays.asList("tenantDomain", "applicationId", "flowType", "callbackUrl", "portalUrl"); + private final FlowContextHandoverConfig handoverConfig; public InFlowExtensionContextTreeBuilder(FlowContextHandoverConfig handoverConfig) { @@ -64,18 +69,22 @@ public InFlowExtensionContextTreeBuilder(FlowContextHandoverConfig handoverConfi */ public InFlowExtensionContextTreeMetadata build(String flowType) { - FlowContextHandoverPolicy policy = handoverConfig.resolve(flowType); + Set attrs = handoverConfig.getIncludedAttributes(); + Set userAttrs = handoverConfig.isFullUserPassthrough() + ? null // null → all user children shown + : handoverConfig.getIncludedUserAttributes(); List tree = new ArrayList<>(); - InFlowExtensionContextTreeNode userNode = buildUserNode(policy); + + InFlowExtensionContextTreeNode userNode = buildUserNode(attrs, userAttrs); if (userNode != null) { tree.add(userNode); } - InFlowExtensionContextTreeNode flowNode = buildFlowNode(policy); + InFlowExtensionContextTreeNode flowNode = buildFlowNode(attrs); if (flowNode != null) { tree.add(flowNode); } - InFlowExtensionContextTreeNode propsNode = buildPropertiesNode(policy); + InFlowExtensionContextTreeNode propsNode = buildPropertiesNode(attrs); if (propsNode != null) { tree.add(propsNode); } @@ -83,18 +92,18 @@ public InFlowExtensionContextTreeMetadata build(String flowType) { return new InFlowExtensionContextTreeMetadata( flowType, tree, - policy.isRedirectionEnabled(), + true, // redirection is unconditionally enabled resolveAllowReadOnlyClaimsModification(flowType)); } /** * Whether the Console UI may permit MODIFY on read-only claims for this flow type. - * Hardcoded enumerative mapping (rather than {@code != PASSWORD_RECOVERY}) so that any - * future flow type defaults to the safe value (false) until explicitly added here. + * Hardcoded enumerative mapping so that any future flow type defaults to false until + * explicitly added here. * - *

      The default tree (null flowType) inherits the registration-style permissive default - * — that matches today's behaviour for the connection-level access-config editor, which - * doesn't yet know which flow the action will be wired into.

      + *

      The default tree (null flowType) returns true — matches current behaviour for the + * connection-level access-config editor which doesn't yet know which flow the action + * will be wired into.

      */ static boolean resolveAllowReadOnlyClaimsModification(String flowType) { @@ -106,19 +115,27 @@ static boolean resolveAllowReadOnlyClaimsModification(String flowType) { case FLOW_INVITED_USER_REGISTRATION: return true; case FLOW_PASSWORD_RECOVERY: - return false; default: return false; } } - private InFlowExtensionContextTreeNode buildUserNode(FlowContextHandoverPolicy policy) { + /** + * Build the "user" subtree. {@code userAttrs == null} means full passthrough (show all). + */ + private InFlowExtensionContextTreeNode buildUserNode(Set attrs, Set userAttrs) { - if (!policy.isUserEnabled()) { + // User branch is shown when flowUser is in attrs OR any individual user attr is listed. + boolean fullPassthrough = attrs.contains(FlowContextHandoverConstants.ATTR_FLOW_USER); + boolean hasAnyUserAttr = fullPassthrough + || (userAttrs != null && !userAttrs.isEmpty()); + if (!hasAnyUserAttr) { return null; } + List children = new ArrayList<>(); - if (policy.isUserUserId()) { + + if (fullPassthrough || (userAttrs != null && userAttrs.contains("userId"))) { children.add(InFlowExtensionContextTreeNode.builder() .key("userId") .title("User ID") @@ -129,7 +146,18 @@ private InFlowExtensionContextTreeNode buildUserNode(FlowContextHandoverPolicy p .replaceable(false) .build()); } - if (policy.isUserUserStoreDomain()) { + if (fullPassthrough || (userAttrs != null && userAttrs.contains("username"))) { + children.add(InFlowExtensionContextTreeNode.builder() + .key("username") + .title("Username") + .path("/user/username") + .dataType("String") + .nodeType(NODE_LEAF) + .allowedOperations(Collections.singletonList(OP_EXPOSE)) + .replaceable(false) + .build()); + } + if (fullPassthrough || (userAttrs != null && userAttrs.contains("userStoreDomain"))) { children.add(InFlowExtensionContextTreeNode.builder() .key("userStoreDomain") .title("User Store Domain") @@ -140,7 +168,7 @@ private InFlowExtensionContextTreeNode buildUserNode(FlowContextHandoverPolicy p .replaceable(false) .build()); } - if (policy.isUserClaims()) { + if (fullPassthrough || (userAttrs != null && userAttrs.contains("claims"))) { children.add(InFlowExtensionContextTreeNode.builder() .key("claims") .title("Claims") @@ -153,7 +181,7 @@ private InFlowExtensionContextTreeNode buildUserNode(FlowContextHandoverPolicy p .children(Collections.emptyList()) .build()); } - if (policy.isUserCredentials()) { + if (fullPassthrough || (userAttrs != null && userAttrs.contains("userCredentials"))) { children.add(InFlowExtensionContextTreeNode.builder() .key("credentials") .title("Credentials") @@ -180,26 +208,17 @@ private InFlowExtensionContextTreeNode buildUserNode(FlowContextHandoverPolicy p .build(); } - private InFlowExtensionContextTreeNode buildFlowNode(FlowContextHandoverPolicy policy) { + /** + * Build the "flow" subtree from top-level context attributes that map to the flow branch. + */ + private InFlowExtensionContextTreeNode buildFlowNode(Set attrs) { - if (!policy.isFlowEnabled()) { - return null; - } List children = new ArrayList<>(); - if (policy.isFlowTenantDomain()) { - children.add(flowLeaf("tenantDomain", "Tenant Domain", "/flow/tenantDomain")); - } - if (policy.isFlowApplicationId()) { - children.add(flowLeaf("applicationId", "Application ID", "/flow/applicationId")); - } - if (policy.isFlowFlowType()) { - children.add(flowLeaf("flowType", "Flow Type", "/flow/flowType")); - } - if (policy.isFlowCallbackUrl()) { - children.add(flowLeaf("callbackUrl", "Callback URL", "/flow/callbackUrl")); - } - if (policy.isFlowPortalUrl()) { - children.add(flowLeaf("portalUrl", "Portal URL", "/flow/portalUrl")); + for (String attr : FLOW_BRANCH_ATTRS) { + if (attrs.contains(attr)) { + String title = attrTitle(attr); + children.add(flowLeaf(attr, title, "/flow/" + attr)); + } } if (children.isEmpty()) { return null; @@ -229,10 +248,23 @@ private InFlowExtensionContextTreeNode flowLeaf(String key, String title, String .build(); } - private InFlowExtensionContextTreeNode buildPropertiesNode(FlowContextHandoverPolicy policy) { + /** + * Build the "properties" node if {@code "properties"} is in the included attributes. + */ + private InFlowExtensionContextTreeNode buildPropertiesNode(Set attrs) { - if (!policy.isPropertiesEnabled()) { - return null; + if (!attrs.contains("properties")) { + return InFlowExtensionContextTreeNode.builder() + .key("properties") + .title("Properties") + .path("/properties/") + .dataType("Map") + .nodeType(NODE_COMPLEX_MAP) + .allowedOperations(Arrays.asList(OP_MODIFY)) + .dynamicEntryAllowed(true) + .dynamicEntryType("Object") + .children(Collections.emptyList()) + .build(); } return InFlowExtensionContextTreeNode.builder() .key("properties") @@ -246,4 +278,16 @@ private InFlowExtensionContextTreeNode buildPropertiesNode(FlowContextHandoverPo .children(Collections.emptyList()) .build(); } + + private static String attrTitle(String attr) { + + switch (attr) { + case "tenantDomain": return "Tenant Domain"; + case "applicationId": return "Application ID"; + case "flowType": return "Flow Type"; + case "callbackUrl": return "Callback URL"; + case "portalUrl": return "Portal URL"; + default: return attr; + } + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeService.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeService.java index b48431409b56..c1d291df971e 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeService.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeService.java @@ -18,7 +18,8 @@ package org.wso2.carbon.identity.flow.inflow.extensions.metadata; -import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; /** * Public-API entry point for retrieving the controlled In-Flow Extension context tree. @@ -26,16 +27,13 @@ * external bundles such as the flow-management API server can call it without depending on * the engine's {@code internal} package. * - *

      Singleton with lazy-init {@link FlowContextHandoverConfig} — the config reads from - * {@code IdentityUtil} which requires the carbon configuration to be loaded, so we defer - * construction until first use to avoid OSGi activation-order issues.

      + *

      Delegates config lookup to {@link FlowExecutionEngineDataHolder}, which owns the + * engine-level {@link FlowContextHandoverConfig} singleton.

      */ public final class InFlowExtensionContextTreeService { private static final InFlowExtensionContextTreeService INSTANCE = new InFlowExtensionContextTreeService(); - private volatile FlowContextHandoverConfig handoverConfig; - private InFlowExtensionContextTreeService() { } @@ -53,26 +51,7 @@ public static InFlowExtensionContextTreeService getInstance() { */ public InFlowExtensionContextTreeMetadata buildContextTree(String flowType) { - return new InFlowExtensionContextTreeBuilder(getHandoverConfig()).build(flowType); - } - - /** - * Override the handover config. Intended for tests only. - */ - public void setHandoverConfig(FlowContextHandoverConfig handoverConfig) { - - this.handoverConfig = handoverConfig; - } - - private FlowContextHandoverConfig getHandoverConfig() { - - if (handoverConfig == null) { - synchronized (this) { - if (handoverConfig == null) { - handoverConfig = new FlowContextHandoverConfig(); - } - } - } - return handoverConfig; + return new InFlowExtensionContextTreeBuilder( + FlowExecutionEngineDataHolder.getInstance().getFlowContextHandoverConfig()).build(flowType); } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionTestUtils.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionTestUtils.java new file mode 100644 index 000000000000..184c759d5731 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionTestUtils.java @@ -0,0 +1,81 @@ +/* + * 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.inflow.extensions; + +import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConstants; + +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Test-only helper that builds {@link FlowContextHandoverConfig} instances without + * touching {@code IdentityConfigParser} or triggering {@code FlowExecutionContextFilter}'s + * static initialiser (which fails in the inflow test classpath due to missing + * authentication-framework dependencies). + * + *

      Uses reflection to call the private 3-arg constructor on + * {@link FlowContextHandoverConfig} directly.

      + */ +public final class InFlowExtensionTestUtils { + + /** + * Commonly used attribute sets that cover all fields the filter exposes without needing + * {@code FlowExecutionContextFilter.getKnownContextAttributes()}. + */ + public static final Set ALL_CONTEXT_ATTRS = new HashSet<>(Arrays.asList( + "tenantDomain", "applicationId", "flowType", "callbackUrl", "portalUrl", + "properties", "contextIdentifier")); + + public static final Set ALL_USER_ATTRS = new HashSet<>(Arrays.asList( + "username", "userId", "userStoreDomain", "claims", "userCredentials")); + + private InFlowExtensionTestUtils() { + + } + + /** + * Construct a permissive {@link FlowContextHandoverConfig} without triggering + * {@code FlowContextHandoverConfig.permissive()} (which calls + * {@code FlowExecutionContextFilter}'s static initialiser). + */ + public static FlowContextHandoverConfig permissiveConfig() { + + return configOf(ALL_CONTEXT_ATTRS, ALL_USER_ATTRS); + } + + /** + * Construct a {@link FlowContextHandoverConfig} with explicit allow-lists. + */ + public static FlowContextHandoverConfig configOf(Set attrs, Set userAttrs) { + + try { + Constructor ctor = + FlowContextHandoverConfig.class.getDeclaredConstructor( + Set.class, Set.class, boolean.class); + ctor.setAccessible(true); + boolean fullPassthrough = attrs.contains(FlowContextHandoverConstants.ATTR_FLOW_USER); + return ctor.newInstance(attrs, userAttrs, fullPassthrough); + } catch (Exception e) { + throw new RuntimeException("Cannot construct FlowContextHandoverConfig for test", e); + } + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverPolicyTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverPolicyTest.java deleted file mode 100644 index 3136bfadabca..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/config/FlowContextHandoverPolicyTest.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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.inflow.extensions.config; - -import org.testng.annotations.Test; - -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertTrue; - -public class FlowContextHandoverPolicyTest { - - @Test - public void testDefaultBuilderHasEverythingDisabled() { - - FlowContextHandoverPolicy p = FlowContextHandoverPolicy.builder().build(); - - assertFalse(p.isRedirectionEnabled()); - assertFalse(p.isUserEnabled()); - assertFalse(p.isUserUserId()); - assertFalse(p.isUserUserStoreDomain()); - assertFalse(p.isUserClaims()); - assertFalse(p.isUserCredentials()); - assertFalse(p.isFlowEnabled()); - assertFalse(p.isFlowTenantDomain()); - assertFalse(p.isFlowApplicationId()); - assertFalse(p.isFlowFlowType()); - assertFalse(p.isFlowCallbackUrl()); - assertFalse(p.isFlowPortalUrl()); - assertFalse(p.isPropertiesEnabled()); - } - - @Test - public void testPermissiveAllowsEverything() { - - FlowContextHandoverPolicy p = FlowContextHandoverPolicy.permissive(); - - assertTrue(p.isRedirectionEnabled()); - assertTrue(p.isUserEnabled()); - assertTrue(p.isUserUserId()); - assertTrue(p.isUserUserStoreDomain()); - assertTrue(p.isUserClaims()); - assertTrue(p.isUserCredentials()); - assertTrue(p.isFlowEnabled()); - assertTrue(p.isFlowTenantDomain()); - assertTrue(p.isFlowApplicationId()); - assertTrue(p.isFlowFlowType()); - assertTrue(p.isFlowCallbackUrl()); - assertTrue(p.isFlowPortalUrl()); - assertTrue(p.isPropertiesEnabled()); - } - - @Test - public void testStartingFromInheritsThenOverrides() { - - // Mimic deployment.toml: per-flow-type override that says "redirection.enable = false" - // but inherits everything else from the permissive default. - FlowContextHandoverPolicy base = FlowContextHandoverPolicy.permissive(); - FlowContextHandoverPolicy override = FlowContextHandoverPolicy.builder() - .startingFrom(base) - .redirectionEnabled(false) - .userClaims(false) - .build(); - - // Overridden: - assertFalse(override.isRedirectionEnabled()); - assertFalse(override.isUserClaims()); - - // Inherited: - assertTrue(override.isUserEnabled()); - assertTrue(override.isUserUserStoreDomain()); - assertTrue(override.isFlowFlowType()); - assertTrue(override.isFlowPortalUrl()); - assertTrue(override.isPropertiesEnabled()); - } - - @Test - public void testBuilderIndependentOfBaseAfterStartingFrom() { - - // Calling startingFrom must copy values, not retain a reference. Mutating the builder - // afterwards must not affect the base policy. - FlowContextHandoverPolicy base = FlowContextHandoverPolicy.permissive(); - FlowContextHandoverPolicy.Builder b = FlowContextHandoverPolicy.builder().startingFrom(base); - - b.redirectionEnabled(false); - FlowContextHandoverPolicy mutated = b.build(); - - assertFalse(mutated.isRedirectionEnabled()); - assertTrue(base.isRedirectionEnabled()); // base unchanged - } -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java index 29341fb0c414..10376b65f710 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java @@ -36,8 +36,9 @@ import org.wso2.carbon.identity.flow.execution.engine.Constants; import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; -import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverConfig; -import org.wso2.carbon.identity.flow.inflow.extensions.config.FlowContextHandoverPolicy; +import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; +import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionTestUtils; import org.wso2.carbon.identity.flow.inflow.extensions.internal.InFlowExtensionDataHolder; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; @@ -81,19 +82,17 @@ public void setUp() { mocks = MockitoAnnotations.openMocks(this); executor = new InFlowExtensionExecutor(); - InFlowExtensionDataHolder holderInstance = - mock(InFlowExtensionDataHolder.class); + // Stub InFlowExtensionDataHolder for action executor service. + InFlowExtensionDataHolder holderInstance = mock(InFlowExtensionDataHolder.class); when(holderInstance.getActionExecutorService()).thenReturn(actionExecutorService); - - // The executor consults the handover config on every run. Stub it to a permissive - // policy so the existing tests remain focused on status mapping rather than filtering. - FlowContextHandoverConfig handoverConfig = mock(FlowContextHandoverConfig.class); - when(handoverConfig.resolve(any())).thenReturn(FlowContextHandoverPolicy.permissive()); - when(holderInstance.getFlowContextHandoverConfig()).thenReturn(handoverConfig); - holderMock = mockStatic(InFlowExtensionDataHolder.class); holderMock.when(InFlowExtensionDataHolder::getInstance).thenReturn(holderInstance); + // Set a permissive handover config directly on the engine DataHolder so tests remain + // focused on status mapping rather than filtering (avoids cross-module mockStatic). + FlowExecutionEngineDataHolder.getInstance() + .setFlowContextHandoverConfig(InFlowExtensionTestUtils.permissiveConfig()); + loggerUtilsMock = mockStatic(LoggerUtils.class); loggerUtilsMock.when(LoggerUtils::isDiagnosticLogsEnabled).thenReturn(false); } @@ -104,6 +103,8 @@ public void tearDown() throws Exception { loggerUtilsMock.close(); holderMock.close(); mocks.close(); + // Reset the handover config so it doesn't leak between test classes. + FlowExecutionEngineDataHolder.getInstance().setFlowContextHandoverConfig(null); } // ========================= getName ========================= @@ -564,8 +565,7 @@ public void testExecuteServiceUnavailable() throws Exception { // Override holder mock to return null service. holderMock.close(); - InFlowExtensionDataHolder holderInstance = - mock(InFlowExtensionDataHolder.class); + InFlowExtensionDataHolder holderInstance = mock(InFlowExtensionDataHolder.class); when(holderInstance.getActionExecutorService()).thenReturn(null); holderMock = mockStatic(InFlowExtensionDataHolder.class); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/FlowContextHandoverConfigTestHelper.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/FlowContextHandoverConfigTestHelper.java new file mode 100644 index 000000000000..c6a67abdaede --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/FlowContextHandoverConfigTestHelper.java @@ -0,0 +1,40 @@ +/* + * 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.inflow.extensions.metadata; + +import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionTestUtils; + +import java.util.Set; + +/** + * Test-only helper that delegates to {@link InFlowExtensionTestUtils#configOf} to + * instantiate {@link FlowContextHandoverConfig} with explicit allow-lists. + */ +final class FlowContextHandoverConfigTestHelper { + + private FlowContextHandoverConfigTestHelper() { + + } + + static FlowContextHandoverConfig of(Set attrs, Set userAttrs) { + + return InFlowExtensionTestUtils.configOf(attrs, userAttrs); + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java new file mode 100644 index 000000000000..1ef617c8f9ff --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java @@ -0,0 +1,282 @@ +/* + * 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.inflow.extensions.metadata; + +import org.testng.annotations.Test; +import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +/** + * Unit tests for {@link InFlowExtensionContextTreeBuilder}. + * + *

      Uses {@link FlowContextHandoverConfigTestHelper} to construct configs with explicit + * allow-lists without touching IdentityConfigParser.

      + */ +public class InFlowExtensionContextTreeBuilderTest { + + // ========================= redirection always enabled ========================= + + @Test + public void testRedirectionAlwaysEnabled() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(Arrays.asList("tenantDomain")), + new HashSet<>(), null); + + assertTrue(meta.isRedirectionEnabled(), + "Redirection must always be true regardless of config"); + } + + // ========================= allowReadOnlyClaimsModification ========================= + + @Test + public void testAllowReadOnlyClaimsModificationForRegistration() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(), new HashSet<>(), "REGISTRATION"); + assertTrue(meta.isAllowReadOnlyClaimsModification()); + } + + @Test + public void testAllowReadOnlyClaimsModificationForInvitedUserRegistration() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(), new HashSet<>(), "INVITED_USER_REGISTRATION"); + assertTrue(meta.isAllowReadOnlyClaimsModification()); + } + + @Test + public void testAllowReadOnlyClaimsModificationFalseForPasswordRecovery() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(), new HashSet<>(), "PASSWORD_RECOVERY"); + assertFalse(meta.isAllowReadOnlyClaimsModification()); + } + + @Test + public void testAllowReadOnlyClaimsModificationFalseForUnknownFlowType() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(), new HashSet<>(), "SOME_FUTURE_FLOW"); + assertFalse(meta.isAllowReadOnlyClaimsModification()); + } + + @Test + public void testAllowReadOnlyClaimsModificationTrueForNullFlowType() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(), new HashSet<>(), null); + assertTrue(meta.isAllowReadOnlyClaimsModification()); + } + + // ========================= flow branch ========================= + + @Test + public void testEmptyAllowListProducesEmptyTree() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(), new HashSet<>(), null); + + assertTrue(meta.getContextTree().isEmpty(), + "Empty allow-list must produce an empty tree"); + } + + @Test + public void testFlowNodeAppearsWhenFlowAttrsPresent() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(Arrays.asList("tenantDomain", "flowType")), + new HashSet<>(), null); + + InFlowExtensionContextTreeNode flowNode = findNode(meta, "flow"); + assertNotNull(flowNode, "flow node should be present"); + + // Only the allowed attrs should appear as children. + List children = flowNode.getChildren(); + assertTrue(hasChildKey(children, "tenantDomain"), "tenantDomain expected"); + assertTrue(hasChildKey(children, "flowType"), "flowType expected"); + assertFalse(hasChildKey(children, "applicationId"), "applicationId not in allow-list"); + assertFalse(hasChildKey(children, "callbackUrl"), "callbackUrl not in allow-list"); + assertFalse(hasChildKey(children, "portalUrl"), "portalUrl not in allow-list"); + } + + @Test + public void testFlowNodeAbsentWhenNoFlowAttrsIncluded() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(Arrays.asList("username")), // only user attr + new HashSet<>(Arrays.asList("username")), null); + + assertNull(findNode(meta, "flow"), "flow node must not appear if no flow attrs present"); + } + + // ========================= user branch ========================= + + @Test + public void testUserNodeAbsentWhenNoUserAttrsIncluded() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(Arrays.asList("tenantDomain")), + new HashSet<>(), null); + + assertNull(findNode(meta, "user"), "user node must not appear"); + } + + @Test + public void testUserNodeAppearsWithSelectedUserAttrs() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(Arrays.asList("tenantDomain")), // no flowUser + new HashSet<>(Arrays.asList("username", "claims")), null); + + InFlowExtensionContextTreeNode userNode = findNode(meta, "user"); + assertNotNull(userNode, "user node should be present"); + + List children = userNode.getChildren(); + assertTrue(hasChildKey(children, "username"), "username expected"); + assertTrue(hasChildKey(children, "claims"), "claims expected"); + assertFalse(hasChildKey(children, "userId"), "userId not in allow-list"); + assertFalse(hasChildKey(children, "userStoreDomain"), "userStoreDomain not in allow-list"); + assertFalse(hasChildKey(children, "credentials"), "credentials not in allow-list"); + } + + @Test + public void testFullUserPassthroughShowsAllUserChildren() { + + // "flowUser" in context attrs → fullUserPassthrough → all user children present. + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(Arrays.asList("flowUser")), + new HashSet<>(), null); // includedUserAttributes empty — ignored in passthrough + + InFlowExtensionContextTreeNode userNode = findNode(meta, "user"); + assertNotNull(userNode, "user node should be present with full passthrough"); + + List children = userNode.getChildren(); + assertTrue(hasChildKey(children, "userId"), "userId must appear in passthrough"); + assertTrue(hasChildKey(children, "username"), "username must appear in passthrough"); + assertTrue(hasChildKey(children, "userStoreDomain"),"userStoreDomain must appear in passthrough"); + assertTrue(hasChildKey(children, "claims"), "claims must appear in passthrough"); + assertTrue(hasChildKey(children, "credentials"), "credentials must appear in passthrough"); + } + + // ========================= properties branch ========================= + + @Test + public void testPropertiesNodeAbsentWhenNotInAllowList() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(Arrays.asList("tenantDomain")), + new HashSet<>(), null); + + assertNull(findNode(meta, "properties"), "properties node must not appear"); + } + + @Test + public void testPropertiesNodeAppearsWhenInAllowList() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(Arrays.asList("properties")), + new HashSet<>(), null); + + InFlowExtensionContextTreeNode propsNode = findNode(meta, "properties"); + assertNotNull(propsNode, "properties node should be present"); + } + + // ========================= flowType metadata field ========================= + + @Test + public void testFlowTypeFieldPreserved() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(), new HashSet<>(), "REGISTRATION"); + + assertEquals(meta.getFlowType(), "REGISTRATION"); + } + + @Test + public void testFlowTypeNullPreserved() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(), new HashSet<>(), null); + + assertNull(meta.getFlowType()); + } + + // ========================= resolveAllowReadOnlyClaimsModification (static) ========================= + + @Test + public void testResolveAllowReadOnlyClaimsModificationDirectly() { + + assertTrue(InFlowExtensionContextTreeBuilder.resolveAllowReadOnlyClaimsModification(null)); + assertTrue(InFlowExtensionContextTreeBuilder.resolveAllowReadOnlyClaimsModification("REGISTRATION")); + assertTrue(InFlowExtensionContextTreeBuilder + .resolveAllowReadOnlyClaimsModification("INVITED_USER_REGISTRATION")); + assertFalse(InFlowExtensionContextTreeBuilder + .resolveAllowReadOnlyClaimsModification("PASSWORD_RECOVERY")); + assertFalse(InFlowExtensionContextTreeBuilder + .resolveAllowReadOnlyClaimsModification("UNKNOWN_TYPE")); + } + + // ========================= helpers ========================= + + private InFlowExtensionContextTreeMetadata buildWith(Set attrs, + Set userAttrs, + String flowType) { + + FlowContextHandoverConfig cfg = FlowContextHandoverConfigTestHelper.of(attrs, userAttrs); + return new InFlowExtensionContextTreeBuilder(cfg).build(flowType); + } + + private InFlowExtensionContextTreeNode findNode(InFlowExtensionContextTreeMetadata meta, + String key) { + + if (meta.getContextTree() == null) { + return null; + } + for (InFlowExtensionContextTreeNode node : meta.getContextTree()) { + if (key.equals(node.getKey())) { + return node; + } + } + return null; + } + + private boolean hasChildKey(List children, String key) { + + if (children == null) { + return false; + } + for (InFlowExtensionContextTreeNode child : children) { + if (key.equals(child.getKey())) { + return true; + } + } + return false; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/testng.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/testng.xml index 8c47c787ee13..4c8191946b3e 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/testng.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/testng.xml @@ -19,12 +19,6 @@ - - - - - - @@ -34,6 +28,11 @@ + + + + + diff --git a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 index feb5fe63014b..cb9c548c4a57 100644 --- a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 +++ b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 @@ -2528,108 +2528,29 @@ - - - - {{identity.in_flow_extension.context.default.redirection.enable | default('true')}} - - - {{identity.in_flow_extension.context.default.user.enable | default('true')}} - {{identity.in_flow_extension.context.default.user.userId | default('false')}} - {{identity.in_flow_extension.context.default.user.userStoreDomain | default('true')}} - {{identity.in_flow_extension.context.default.user.claims | default('true')}} - {{identity.in_flow_extension.context.default.user.credentials | default('false')}} - - - {{identity.in_flow_extension.context.default.flow.enable | default('true')}} - {{identity.in_flow_extension.context.default.flow.tenantDomain | default('true')}} - {{identity.in_flow_extension.context.default.flow.applicationId | default('true')}} - {{identity.in_flow_extension.context.default.flow.flowType | default('true')}} - {{identity.in_flow_extension.context.default.flow.callbackUrl | default('true')}} - {{identity.in_flow_extension.context.default.flow.portalUrl | default('true')}} - - - {{identity.in_flow_extension.context.default.properties.enable | default('true')}} - - - - - {{identity.in_flow_extension.context.registration.redirection.enable | default(identity.in_flow_extension.context.default.redirection.enable | default('true'))}} - - - {{identity.in_flow_extension.context.registration.user.enable | default(identity.in_flow_extension.context.default.user.enable | default('true'))}} - {{identity.in_flow_extension.context.registration.user.userId | default(identity.in_flow_extension.context.default.user.userId | default('false'))}} - {{identity.in_flow_extension.context.registration.user.userStoreDomain | default(identity.in_flow_extension.context.default.user.userStoreDomain | default('true'))}} - {{identity.in_flow_extension.context.registration.user.claims | default(identity.in_flow_extension.context.default.user.claims | default('true'))}} - {{identity.in_flow_extension.context.registration.user.credentials | default(identity.in_flow_extension.context.default.user.credentials | default('false'))}} - - - {{identity.in_flow_extension.context.registration.flow.enable | default(identity.in_flow_extension.context.default.flow.enable | default('true'))}} - {{identity.in_flow_extension.context.registration.flow.tenantDomain | default(identity.in_flow_extension.context.default.flow.tenantDomain | default('true'))}} - {{identity.in_flow_extension.context.registration.flow.applicationId | default(identity.in_flow_extension.context.default.flow.applicationId | default('true'))}} - {{identity.in_flow_extension.context.registration.flow.flowType | default(identity.in_flow_extension.context.default.flow.flowType | default('true'))}} - {{identity.in_flow_extension.context.registration.flow.callbackUrl | default(identity.in_flow_extension.context.default.flow.callbackUrl | default('true'))}} - {{identity.in_flow_extension.context.registration.flow.portalUrl | default(identity.in_flow_extension.context.default.flow.portalUrl | default('true'))}} - - - {{identity.in_flow_extension.context.registration.properties.enable | default(identity.in_flow_extension.context.default.properties.enable | default('true'))}} - - - - - {{identity.in_flow_extension.context.password_recovery.redirection.enable | default(identity.in_flow_extension.context.default.redirection.enable | default('true'))}} - - - {{identity.in_flow_extension.context.password_recovery.user.enable | default(identity.in_flow_extension.context.default.user.enable | default('true'))}} - {{identity.in_flow_extension.context.password_recovery.user.userId | default(identity.in_flow_extension.context.default.user.userId | default('false'))}} - {{identity.in_flow_extension.context.password_recovery.user.userStoreDomain | default(identity.in_flow_extension.context.default.user.userStoreDomain | default('true'))}} - {{identity.in_flow_extension.context.password_recovery.user.claims | default(identity.in_flow_extension.context.default.user.claims | default('true'))}} - {{identity.in_flow_extension.context.password_recovery.user.credentials | default(identity.in_flow_extension.context.default.user.credentials | default('false'))}} - - - {{identity.in_flow_extension.context.password_recovery.flow.enable | default(identity.in_flow_extension.context.default.flow.enable | default('true'))}} - {{identity.in_flow_extension.context.password_recovery.flow.tenantDomain | default(identity.in_flow_extension.context.default.flow.tenantDomain | default('true'))}} - {{identity.in_flow_extension.context.password_recovery.flow.applicationId | default(identity.in_flow_extension.context.default.flow.applicationId | default('true'))}} - {{identity.in_flow_extension.context.password_recovery.flow.flowType | default(identity.in_flow_extension.context.default.flow.flowType | default('true'))}} - {{identity.in_flow_extension.context.password_recovery.flow.callbackUrl | default(identity.in_flow_extension.context.default.flow.callbackUrl | default('true'))}} - {{identity.in_flow_extension.context.password_recovery.flow.portalUrl | default(identity.in_flow_extension.context.default.flow.portalUrl | default('true'))}} - - - {{identity.in_flow_extension.context.password_recovery.properties.enable | default(identity.in_flow_extension.context.default.properties.enable | default('true'))}} - - - - - {{identity.in_flow_extension.context.invited_user_registration.redirection.enable | default(identity.in_flow_extension.context.default.redirection.enable | default('true'))}} - - - {{identity.in_flow_extension.context.invited_user_registration.user.enable | default(identity.in_flow_extension.context.default.user.enable | default('true'))}} - {{identity.in_flow_extension.context.invited_user_registration.user.userId | default(identity.in_flow_extension.context.default.user.userId | default('false'))}} - {{identity.in_flow_extension.context.invited_user_registration.user.userStoreDomain | default(identity.in_flow_extension.context.default.user.userStoreDomain | default('true'))}} - {{identity.in_flow_extension.context.invited_user_registration.user.claims | default(identity.in_flow_extension.context.default.user.claims | default('true'))}} - {{identity.in_flow_extension.context.invited_user_registration.user.credentials | default(identity.in_flow_extension.context.default.user.credentials | default('false'))}} - - - {{identity.in_flow_extension.context.invited_user_registration.flow.enable | default(identity.in_flow_extension.context.default.flow.enable | default('true'))}} - {{identity.in_flow_extension.context.invited_user_registration.flow.tenantDomain | default(identity.in_flow_extension.context.default.flow.tenantDomain | default('true'))}} - {{identity.in_flow_extension.context.invited_user_registration.flow.applicationId | default(identity.in_flow_extension.context.default.flow.applicationId | default('true'))}} - {{identity.in_flow_extension.context.invited_user_registration.flow.flowType | default(identity.in_flow_extension.context.default.flow.flowType | default('true'))}} - {{identity.in_flow_extension.context.invited_user_registration.flow.callbackUrl | default(identity.in_flow_extension.context.default.flow.callbackUrl | default('true'))}} - {{identity.in_flow_extension.context.invited_user_registration.flow.portalUrl | default(identity.in_flow_extension.context.default.flow.portalUrl | default('true'))}} - - - {{identity.in_flow_extension.context.invited_user_registration.properties.enable | default(identity.in_flow_extension.context.default.properties.enable | default('true'))}} - - - + + + {% if flow_execution_context.handover.filtering.included_attributes is defined %} + {% for attr in flow_execution_context.handover.filtering.included_attributes %} + {{attr}} + {% endfor %} + {% endif %} + + + {% if flow_execution_context.handover.filtering.included_user_attributes is defined %} + {% for attr in flow_execution_context.handover.filtering.included_user_attributes %} + {{attr}} + {% endfor %} + {% endif %} + + From f0545970df4f15953de4fdd978009c66e103ce23 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Sat, 9 May 2026 14:09:40 +0530 Subject: [PATCH 26/41] Reviewed action framework suggestions are fixed. --- .../action/management/api/model/Action.java | 4 +- .../api/service/ActionManagementService.java | 37 ++-- .../impl/ActionManagementServiceImpl.java | 26 +-- .../CacheBackedActionManagementService.java | 7 - .../management/model/ActionTypesTest.java | 4 +- .../FlowExecutionEngineServiceComponent.java | 2 - .../src/test/resources/testng.xml | 13 +- .../InFlowExtensionContextTreeBuilder.java | 117 ++++++------ ...InFlowExtensionContextTreeBuilderTest.java | 179 ++++++++++++++++-- 9 files changed, 268 insertions(+), 121 deletions(-) diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java index ed86789fd74b..b35aea2318ab 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java @@ -81,7 +81,7 @@ public enum ActionTypes { "IN_FLOW_EXTENSION", "In-Flow Extension", "Configure an extension point within any flow via a custom service.", - Category.EXTENSION); + Category.IN_FLOW_EXTENSION); private final String pathParam; private final String actionType; @@ -138,7 +138,7 @@ public static ActionTypes[] filterByCategory(Category category) { public enum Category { PRE_POST, IN_FLOW, - EXTENSION + IN_FLOW_EXTENSION } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java index de218c2902b4..fa370a5ffe27 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java @@ -118,7 +118,6 @@ Action updateAction(String actionType, String actionId, Action action, String te /** * Update the authentication of the action endpoint. - * * @param actionType Action Type. * @param actionId Action ID. @@ -132,26 +131,34 @@ Action updateActionEndpointAuthentication(String actionType, String actionId, Au /** * Check whether the given action name is available (unique) within the specified action type. - * - * @param actionType Action Type path parameter. - * @param name Action name to check. - * @param tenantDomain Tenant domain. - * @return {@code true} if the name is available, {@code false} otherwise. - * @throws ActionMgtException If an error occurs while checking name availability. - */ - boolean isActionNameAvailable(String actionType, String name, String tenantDomain) throws ActionMgtException; - - /** - * Check whether the given action name is available (unique) within the specified action type, - * excluding the action with the given ID (useful for update scenarios). + * When {@code excludeActionId} is {@code null} the check covers all existing actions of that + * type (creation scenario). When non-null the action with that ID is excluded from the + * uniqueness check (update scenario). * * @param actionType Action Type path parameter. * @param name Action name to check. - * @param excludeActionId Action ID to exclude from the uniqueness check. + * @param excludeActionId Action ID to exclude from the uniqueness check, or {@code null} for + * creation scenarios where no action should be excluded. * @param tenantDomain Tenant domain. - * @return {@code true} if the name is available, {@code false} otherwise. + * @return {@code true} if the name is not already used by another action of the same type + * (i.e., the caller may safely use this name); {@code false} if the name is already taken. * @throws ActionMgtException If an error occurs while checking name availability. */ boolean isActionNameAvailable(String actionType, String name, String excludeActionId, String tenantDomain) throws ActionMgtException; + + /** + * Convenience overload for creation scenarios — delegates to + * {@link #isActionNameAvailable(String, String, String, String)} with {@code excludeActionId = null}. + * + * @param actionType Action Type path parameter. + * @param name Action name to check. + * @param tenantDomain Tenant domain. + * @return {@code true} if the name is available; {@code false} if already taken. + * @throws ActionMgtException If an error occurs while checking name availability. + */ + default boolean isActionNameAvailable(String actionType, String name, String tenantDomain) + throws ActionMgtException { + return isActionNameAvailable(actionType, name, null, tenantDomain); + } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java index ed1fe3f64051..3d5bab88eefe 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java @@ -180,21 +180,6 @@ public Action updateAction(String actionType, String actionId, Action action, St return buildAction(resolvedActionType, updatedActionDTO); } - @Override - public boolean isActionNameAvailable(String actionType, String name, String tenantDomain) - throws ActionMgtException { - - if (name == null) { - throw ActionManagementExceptionHandler.handleClientException( - ErrorMessage.ERROR_INVALID_ACTION_REQUEST_FIELD, "Action name"); - } - String resolvedActionType = getActionTypeFromPath(actionType); - int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); - List actionDTOS = DAO_FACADE.getActionsByActionType(resolvedActionType, tenantId); - return actionDTOS.stream() - .noneMatch(dto -> name.equalsIgnoreCase(dto.getName())); - } - @Override public boolean isActionNameAvailable(String actionType, String name, String excludeActionId, String tenantDomain) throws ActionMgtException { @@ -203,11 +188,15 @@ public boolean isActionNameAvailable(String actionType, String name, String excl throw ActionManagementExceptionHandler.handleClientException( ErrorMessage.ERROR_INVALID_ACTION_REQUEST_FIELD, "Action name"); } + // actionType is the URL path param (e.g., "inFlowExtension"); resolve to the internal enum name. String resolvedActionType = getActionTypeFromPath(actionType); int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); List actionDTOS = DAO_FACADE.getActionsByActionType(resolvedActionType, tenantId); + // noneMatch returns true when no existing action has this name → name is available. + // When excludeActionId is null (creation), no action is excluded from the check. return actionDTOS.stream() - .noneMatch(dto -> name.equalsIgnoreCase(dto.getName()) && !dto.getId().equals(excludeActionId)); + .noneMatch(dto -> name.equalsIgnoreCase(dto.getName()) && + (excludeActionId == null || !excludeActionId.equals(dto.getId()))); } private String resolveActionVersionAtUpdating(Action updatingAction, ActionDTO existingActionDTO) { @@ -359,7 +348,7 @@ private void validateMaxActionsPerType(String actionType, String tenantDomain) t // In-flow and extension actions are not limited by the maximum actions per action type. Action.ActionTypes.Category category = Action.ActionTypes.valueOf(actionType).getCategory(); if (Action.ActionTypes.Category.IN_FLOW.equals(category) - || Action.ActionTypes.Category.EXTENSION.equals(category)) { + || Action.ActionTypes.Category.IN_FLOW_EXTENSION.equals(category)) { return; } Map actionsCountPerType = getActionsCountPerType(tenantDomain); @@ -429,6 +418,9 @@ private ActionDTO buildActionDTOForCreation(String actionType, String actionId, throws ActionMgtServerException { Action.ActionTypes resolvedActionType = Action.ActionTypes.valueOf(actionType); + // PRE_POST actions start INACTIVE (require explicit activation). + // IN_FLOW and IN_FLOW_EXTENSION category actions (e.g., AUTHENTICATION, IN_FLOW_EXTENSION) + // start ACTIVE and can be used immediately. Action.Status resolvedStatus = resolvedActionType.getCategory() == Action.ActionTypes.Category.PRE_POST ? Action.Status.INACTIVE : Action.Status.ACTIVE; diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java index ff3f399697d7..19180ca29400 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java @@ -182,13 +182,6 @@ public Action updateActionEndpointAuthentication(String actionType, String actio return updatedAction; } - @Override - public boolean isActionNameAvailable(String actionType, String name, String tenantDomain) - throws ActionMgtException { - - return ACTION_MGT_SERVICE.isActionNameAvailable(actionType, name, tenantDomain); - } - @Override public boolean isActionNameAvailable(String actionType, String name, String excludeActionId, String tenantDomain) throws ActionMgtException { diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java index 342682d37456..a8053186cba7 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java @@ -56,7 +56,7 @@ public Object[][] actionTypesProvider() { {Action.ActionTypes.IN_FLOW_EXTENSION, "inFlowExtension", "IN_FLOW_EXTENSION", "In-Flow Extension", "Configure an extension point within any flow via a custom service.", - Action.ActionTypes.Category.EXTENSION} + Action.ActionTypes.Category.IN_FLOW_EXTENSION} }; } @@ -81,7 +81,7 @@ public Object[][] filterByCategoryProvider() { Action.ActionTypes.PRE_UPDATE_PASSWORD, Action.ActionTypes.PRE_UPDATE_PROFILE, Action.ActionTypes.PRE_REGISTRATION, Action.ActionTypes.PRE_ISSUE_ID_TOKEN}}, {Action.ActionTypes.Category.IN_FLOW, new Action.ActionTypes[]{Action.ActionTypes.AUTHENTICATION}}, - {Action.ActionTypes.Category.EXTENSION, + {Action.ActionTypes.Category.IN_FLOW_EXTENSION, new Action.ActionTypes[]{Action.ActionTypes.IN_FLOW_EXTENSION}} }; } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java index ff73956f2e09..7aeaf911c88f 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineServiceComponent.java @@ -79,7 +79,6 @@ protected void activate(ComponentContext context) { FlowExecutionService.getInstance(), null); bundleContext.registerService(FlowExecutionListener.class.getName(), new InputProcessingListener(), null); - LOG.debug("Flow Engine service successfully activated."); } catch (Throwable e) { LOG.error("Error while initiating Flow Engine service", e); @@ -243,7 +242,6 @@ protected void unsetClaimMetadataManagementService(ClaimMetadataManagementServic FlowExecutionEngineDataHolder.getInstance().setClaimMetadataManagementService(null); } - @Reference( name = "identity.event.service", service = IdentityEventService.class, diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml index a7edbb0325fb..035a941187d0 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml @@ -19,12 +19,6 @@ - - - - - - @@ -33,7 +27,14 @@ + + + + + + + diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java index 74a07adc1db7..76a7b01a2a7a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java @@ -19,7 +19,6 @@ package org.wso2.carbon.identity.flow.inflow.extensions.metadata; import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; -import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConstants; import java.util.ArrayList; import java.util.Arrays; @@ -121,21 +120,29 @@ static boolean resolveAllowReadOnlyClaimsModification(String flowType) { } /** - * Build the "user" subtree. {@code userAttrs == null} means full passthrough (show all). + * Build the "user" subtree. + * + *

      Strategy: + *

        + *
      • Read-only fields (userId, username, userStoreDomain): only emitted when the + * attribute is in the allow-list (or full-passthrough is active). They support EXPOSE + * only, so excluding them when restricted is the correct behaviour.
      • + *
      • Modifiable fields (claims, credentials): always emitted because modifications + * bypass the context handover — they travel through the executor response and are applied + * by the task execution node. EXPOSE is added only when the attribute is configured.
      • + *
      + * + *

      {@code userAttrs == null} signals full-passthrough (show and expose everything). */ private InFlowExtensionContextTreeNode buildUserNode(Set attrs, Set userAttrs) { - // User branch is shown when flowUser is in attrs OR any individual user attr is listed. - boolean fullPassthrough = attrs.contains(FlowContextHandoverConstants.ATTR_FLOW_USER); - boolean hasAnyUserAttr = fullPassthrough - || (userAttrs != null && !userAttrs.isEmpty()); - if (!hasAnyUserAttr) { - return null; - } + // userAttrs == null → full passthrough set by the caller (build()). + boolean fullPassthrough = (userAttrs == null); List children = new ArrayList<>(); - if (fullPassthrough || (userAttrs != null && userAttrs.contains("userId"))) { + // ── Read-only fields: emit only when exposed ──────────────────────────────────────── + if (fullPassthrough || userAttrs.contains("userId")) { children.add(InFlowExtensionContextTreeNode.builder() .key("userId") .title("User ID") @@ -146,7 +153,7 @@ private InFlowExtensionContextTreeNode buildUserNode(Set attrs, Set attrs, Set attrs, Set") - .nodeType(NODE_MAP) - .allowedOperations(Arrays.asList(OP_EXPOSE, OP_MODIFY)) - .dynamicEntryAllowed(true) - .dynamicEntryType("String") - .children(Collections.emptyList()) - .build()); - } - if (fullPassthrough || (userAttrs != null && userAttrs.contains("userCredentials"))) { - children.add(InFlowExtensionContextTreeNode.builder() - .key("credentials") - .title("Credentials") - .path("/user/credentials/") - .dataType("Map") - .nodeType(NODE_MAP) - .allowedOperations(Arrays.asList(OP_EXPOSE, OP_MODIFY)) - .dynamicEntryAllowed(true) - .dynamicEntryType("char[]") - .children(Collections.emptyList()) - .build()); - } - if (children.isEmpty()) { - return null; - } + + // ── Modifiable fields: always present, restrict EXPOSE when not configured ────────── + List claimsOps = (fullPassthrough || userAttrs.contains("claims")) + ? Arrays.asList(OP_EXPOSE, OP_MODIFY) + : Collections.singletonList(OP_MODIFY); + children.add(InFlowExtensionContextTreeNode.builder() + .key("claims") + .title("Claims") + .path("/user/claims/") + .dataType("Map") + .nodeType(NODE_MAP) + .allowedOperations(claimsOps) + .dynamicEntryAllowed(true) + .dynamicEntryType("String") + .children(Collections.emptyList()) + .build()); + + List credOps = (fullPassthrough || userAttrs.contains("userCredentials")) + ? Arrays.asList(OP_EXPOSE, OP_MODIFY) + : Collections.singletonList(OP_MODIFY); + children.add(InFlowExtensionContextTreeNode.builder() + .key("credentials") + .title("Credentials") + .path("/user/credentials/") + .dataType("Map") + .nodeType(NODE_MAP) + .allowedOperations(credOps) + .dynamicEntryAllowed(true) + .dynamicEntryType("char[]") + .children(Collections.emptyList()) + .build()); + + // User node is always returned (claims + credentials are always present). return InFlowExtensionContextTreeNode.builder() .key("user") .title("User") @@ -249,30 +260,24 @@ private InFlowExtensionContextTreeNode flowLeaf(String key, String title, String } /** - * Build the "properties" node if {@code "properties"} is in the included attributes. + * Build the "properties" node. + * + *

      Properties is always emitted because modifications bypass the context handover (they + * travel through the executor response and are applied by the task execution node). + * EXPOSE is included only when {@code "properties"} is in the allow-list. */ private InFlowExtensionContextTreeNode buildPropertiesNode(Set attrs) { - if (!attrs.contains("properties")) { - return InFlowExtensionContextTreeNode.builder() - .key("properties") - .title("Properties") - .path("/properties/") - .dataType("Map") - .nodeType(NODE_COMPLEX_MAP) - .allowedOperations(Arrays.asList(OP_MODIFY)) - .dynamicEntryAllowed(true) - .dynamicEntryType("Object") - .children(Collections.emptyList()) - .build(); - } + List ops = attrs.contains("properties") + ? Arrays.asList(OP_EXPOSE, OP_MODIFY) + : Collections.singletonList(OP_MODIFY); return InFlowExtensionContextTreeNode.builder() .key("properties") .title("Properties") .path("/properties/") .dataType("Map") .nodeType(NODE_COMPLEX_MAP) - .allowedOperations(Arrays.asList(OP_EXPOSE, OP_MODIFY)) + .allowedOperations(ops) .dynamicEntryAllowed(true) .dynamicEntryType("Object") .children(Collections.emptyList()) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java index 1ef617c8f9ff..2649b9ca8823 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java @@ -97,14 +97,44 @@ public void testAllowReadOnlyClaimsModificationTrueForNullFlowType() { // ========================= flow branch ========================= + /** + * With an empty allow-list the modifiable fields (claims, credentials, properties) are still + * present but carry only MODIFY — no EXPOSE. Read-only fields (flow branch, user read-only + * scalar) are absent because they have no modify path. + */ @Test - public void testEmptyAllowListProducesEmptyTree() { + public void testEmptyAllowListRestrictsExposeOnly() { InFlowExtensionContextTreeMetadata meta = buildWith( new HashSet<>(), new HashSet<>(), null); - assertTrue(meta.getContextTree().isEmpty(), - "Empty allow-list must produce an empty tree"); + // Modifiable nodes always present. + assertNotNull(findNode(meta, "user"), "user node must be present (has modifiable children)"); + assertNotNull(findNode(meta, "properties"), "properties node must be present (modifiable)"); + + // Read-only-only node absent when nothing configured. + assertNull(findNode(meta, "flow"), "flow node must be absent when no flow attrs configured"); + + // Properties has only MODIFY. + InFlowExtensionContextTreeNode propsNode = findNode(meta, "properties"); + assertFalse(propsNode.getAllowedOperations().contains("EXPOSE"), + "properties must not have EXPOSE when not in allow-list"); + assertTrue(propsNode.getAllowedOperations().contains("MODIFY"), + "properties must have MODIFY regardless of allow-list"); + + // Claims and credentials carry only MODIFY. + List userChildren = findNode(meta, "user").getChildren(); + InFlowExtensionContextTreeNode claimsNode = findChildNode(userChildren, "claims"); + assertNotNull(claimsNode, "claims always present"); + assertFalse(claimsNode.getAllowedOperations().contains("EXPOSE"), + "claims must not have EXPOSE when not in allow-list"); + assertTrue(claimsNode.getAllowedOperations().contains("MODIFY")); + + InFlowExtensionContextTreeNode credNode = findChildNode(userChildren, "credentials"); + assertNotNull(credNode, "credentials always present"); + assertFalse(credNode.getAllowedOperations().contains("EXPOSE"), + "credentials must not have EXPOSE when not in allow-list"); + assertTrue(credNode.getAllowedOperations().contains("MODIFY")); } @Test @@ -139,13 +169,26 @@ public void testFlowNodeAbsentWhenNoFlowAttrsIncluded() { // ========================= user branch ========================= @Test - public void testUserNodeAbsentWhenNoUserAttrsIncluded() { + public void testReadOnlyUserFieldsAbsentWhenNoUserAttrsIncluded() { InFlowExtensionContextTreeMetadata meta = buildWith( new HashSet<>(Arrays.asList("tenantDomain")), new HashSet<>(), null); - assertNull(findNode(meta, "user"), "user node must not appear"); + // User node is always present (claims + credentials always included). + InFlowExtensionContextTreeNode userNode = findNode(meta, "user"); + assertNotNull(userNode, "user node must be present"); + + List children = userNode.getChildren(); + + // Read-only user fields absent when not configured. + assertFalse(hasChildKey(children, "userId"), "userId must not appear"); + assertFalse(hasChildKey(children, "username"), "username must not appear"); + assertFalse(hasChildKey(children, "userStoreDomain"), "userStoreDomain must not appear"); + + // Modifiable fields still present. + assertTrue(hasChildKey(children, "claims"), "claims must always be present"); + assertTrue(hasChildKey(children, "credentials"), "credentials must always be present"); } @Test @@ -159,11 +202,27 @@ public void testUserNodeAppearsWithSelectedUserAttrs() { assertNotNull(userNode, "user node should be present"); List children = userNode.getChildren(); - assertTrue(hasChildKey(children, "username"), "username expected"); - assertTrue(hasChildKey(children, "claims"), "claims expected"); - assertFalse(hasChildKey(children, "userId"), "userId not in allow-list"); - assertFalse(hasChildKey(children, "userStoreDomain"), "userStoreDomain not in allow-list"); - assertFalse(hasChildKey(children, "credentials"), "credentials not in allow-list"); + + // Configured read-only field is exposed. + assertTrue(hasChildKey(children, "username"), "username expected"); + + // Claims configured → EXPOSE+MODIFY. + InFlowExtensionContextTreeNode claimsNode = findChildNode(children, "claims"); + assertNotNull(claimsNode, "claims must be present"); + assertTrue(claimsNode.getAllowedOperations().contains("EXPOSE"), "claims must have EXPOSE"); + assertTrue(claimsNode.getAllowedOperations().contains("MODIFY"), "claims must have MODIFY"); + + // Read-only fields not configured → absent. + assertFalse(hasChildKey(children, "userId"), "userId not in allow-list"); + assertFalse(hasChildKey(children, "userStoreDomain"), "userStoreDomain not in allow-list"); + + // credentials not configured but always present → MODIFY only, no EXPOSE. + InFlowExtensionContextTreeNode credNode = findChildNode(children, "credentials"); + assertNotNull(credNode, "credentials always present"); + assertFalse(credNode.getAllowedOperations().contains("EXPOSE"), + "credentials must not have EXPOSE when not in allow-list"); + assertTrue(credNode.getAllowedOperations().contains("MODIFY"), + "credentials must have MODIFY regardless"); } @Test @@ -188,17 +247,22 @@ public void testFullUserPassthroughShowsAllUserChildren() { // ========================= properties branch ========================= @Test - public void testPropertiesNodeAbsentWhenNotInAllowList() { + public void testPropertiesHasOnlyModifyWhenNotExposed() { InFlowExtensionContextTreeMetadata meta = buildWith( - new HashSet<>(Arrays.asList("tenantDomain")), + new HashSet<>(Arrays.asList("tenantDomain")), // properties not in allow-list new HashSet<>(), null); - assertNull(findNode(meta, "properties"), "properties node must not appear"); + InFlowExtensionContextTreeNode propsNode = findNode(meta, "properties"); + assertNotNull(propsNode, "properties node must always be present"); + assertFalse(propsNode.getAllowedOperations().contains("EXPOSE"), + "properties must not expose when not in allow-list"); + assertTrue(propsNode.getAllowedOperations().contains("MODIFY"), + "properties must always allow MODIFY"); } @Test - public void testPropertiesNodeAppearsWhenInAllowList() { + public void testPropertiesHasExposeAndModifyWhenExposed() { InFlowExtensionContextTreeMetadata meta = buildWith( new HashSet<>(Arrays.asList("properties")), @@ -206,6 +270,79 @@ public void testPropertiesNodeAppearsWhenInAllowList() { InFlowExtensionContextTreeNode propsNode = findNode(meta, "properties"); assertNotNull(propsNode, "properties node should be present"); + assertTrue(propsNode.getAllowedOperations().contains("EXPOSE"), + "properties must have EXPOSE when in allow-list"); + assertTrue(propsNode.getAllowedOperations().contains("MODIFY"), + "properties must always have MODIFY"); + } + + @Test + public void testClaimsHasOnlyModifyWhenNotExposed() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(), + new HashSet<>(), null); // "claims" not in userAttrs + + List userChildren = findNode(meta, "user").getChildren(); + InFlowExtensionContextTreeNode claimsNode = findChildNode(userChildren, "claims"); + assertNotNull(claimsNode); + assertFalse(claimsNode.getAllowedOperations().contains("EXPOSE")); + assertTrue(claimsNode.getAllowedOperations().contains("MODIFY")); + } + + @Test + public void testClaimsHasExposeAndModifyWhenExposed() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(), + new HashSet<>(Arrays.asList("claims")), null); + + List userChildren = findNode(meta, "user").getChildren(); + InFlowExtensionContextTreeNode claimsNode = findChildNode(userChildren, "claims"); + assertNotNull(claimsNode); + assertTrue(claimsNode.getAllowedOperations().contains("EXPOSE")); + assertTrue(claimsNode.getAllowedOperations().contains("MODIFY")); + } + + @Test + public void testCredentialsHasOnlyModifyWhenNotExposed() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(), + new HashSet<>(), null); // "userCredentials" not in userAttrs + + List userChildren = findNode(meta, "user").getChildren(); + InFlowExtensionContextTreeNode credNode = findChildNode(userChildren, "credentials"); + assertNotNull(credNode); + assertFalse(credNode.getAllowedOperations().contains("EXPOSE")); + assertTrue(credNode.getAllowedOperations().contains("MODIFY")); + } + + @Test + public void testCredentialsHasExposeAndModifyWhenExposed() { + + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(), + new HashSet<>(Arrays.asList("userCredentials")), null); + + List userChildren = findNode(meta, "user").getChildren(); + InFlowExtensionContextTreeNode credNode = findChildNode(userChildren, "credentials"); + assertNotNull(credNode); + assertTrue(credNode.getAllowedOperations().contains("EXPOSE")); + assertTrue(credNode.getAllowedOperations().contains("MODIFY")); + } + + @Test + public void testFullPassthroughGivesExposeOnClaimsAndCredentials() { + + // flowUser in attrs → full passthrough → all user fields exposed. + InFlowExtensionContextTreeMetadata meta = buildWith( + new HashSet<>(Arrays.asList("flowUser")), + new HashSet<>(), null); + + List userChildren = findNode(meta, "user").getChildren(); + assertTrue(findChildNode(userChildren, "claims").getAllowedOperations().contains("EXPOSE")); + assertTrue(findChildNode(userChildren, "credentials").getAllowedOperations().contains("EXPOSE")); } // ========================= flowType metadata field ========================= @@ -279,4 +416,18 @@ private boolean hasChildKey(List children, Strin } return false; } + + private InFlowExtensionContextTreeNode findChildNode(List children, + String key) { + + if (children == null) { + return null; + } + for (InFlowExtensionContextTreeNode child : children) { + if (key.equals(child.getKey())) { + return child; + } + } + return null; + } } From 447a663332b3c6be9f6ecbf66d48b0b38c03da79 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Mon, 11 May 2026 08:04:34 +0530 Subject: [PATCH 27/41] Addressed comments and sonarcube warnings --- .../pom.xml | 8 +- .../pom.xml | 8 + .../extensions/InFlowExtensionConstants.java | 44 + .../executor/HierarchicalPrefixMatcher.java | 34 +- .../executor/InFlowExtensionExecutor.java | 404 +++++---- .../executor/InFlowExtensionLogConstants.java | 44 - .../InFlowExtensionRequestBuilder.java | 785 ++++++++++++------ .../InFlowExtensionResponseProcessor.java | 286 +++++-- .../executor/PathTypeAnnotationUtil.java | 86 +- .../internal/InFlowExtensionDataHolder.java | 17 +- .../InFlowExtensionServiceComponent.java | 41 +- .../InFlowExtensionActionConstants.java | 83 -- .../InFlowExtensionActionConverter.java | 111 +-- ...InFlowExtensionActionDTOModelResolver.java | 175 ++-- .../InFlowExtensionContextTreeBuilder.java | 27 +- .../inflow/extensions/model/AccessConfig.java | 13 +- .../InFlowExtensionRequestBuilderTest.java | 52 ++ .../InFlowExtensionResponseProcessorTest.java | 112 ++- .../executor/PathTypeAnnotationUtilTest.java | 36 + .../extensions/model/AccessConfigTest.java | 6 +- .../identity/flow/mgt/FlowMgtService.java | 2 - .../internal/FlowMgtServiceDataHolder.java | 1 - features/flow-orchestration-framework/pom.xml | 1 + 23 files changed, 1517 insertions(+), 859 deletions(-) delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionLogConstants.java delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConstants.java diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml index 12bceaa28730..2680f9fc341a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml @@ -66,10 +66,6 @@ org.wso2.carbon.identity.framework org.wso2.carbon.identity.flow.mgt - - org.wso2.carbon.identity.framework - org.wso2.carbon.identity.claim.metadata.mgt - org.wso2.carbon.identity.framework org.wso2.carbon.identity.central.log.mgt @@ -103,6 +99,10 @@ org.wso2.carbon.identity.framework org.wso2.carbon.identity.user.action + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.claim.metadata.mgt + diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml index 543f50d74a60..a39e0bff1f8d 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml @@ -294,6 +294,14 @@ + + org.apache.maven.plugins + maven-javadoc-plugin + + 21 + + + diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java index 2f276da61e93..e95ed7571091 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java @@ -53,4 +53,48 @@ private InFlowExtensionConstants() { // ---- Miscellaneous ---- public static final String ACTION_ID_METADATA_KEY = "actionId"; + + /** + * Constants for In-Flow Extension action management (action properties stored in + * IDN_ACTION_PROPERTIES, certificate naming, and expose-path limits). + */ + public static final class ActionManagement { + + public static final String ACCESS_CONFIG_EXPOSE = "ACCESS_CONFIG_EXPOSE"; + public static final String ACCESS_CONFIG_MODIFY = "ACCESS_CONFIG_MODIFY"; + public static final String OVERRIDE_KEY_SEPARATOR = ":"; + public static final String ACCESS_CONFIG_EXPOSE_PREFIX = ACCESS_CONFIG_EXPOSE + OVERRIDE_KEY_SEPARATOR; + public static final String ACCESS_CONFIG_MODIFY_PREFIX = ACCESS_CONFIG_MODIFY + OVERRIDE_KEY_SEPARATOR; + public static final int MAX_EXPOSE_PATHS = 50; + public static final String ICON_URL = "ICON_URL"; + public static final String CERTIFICATE = "CERTIFICATE"; + public static final String CERTIFICATE_NAME_PREFIX = "ACTIONS:"; + + private ActionManagement() { } + } + + /** + * Diagnostic log constants for the In-Flow Extension layer. + */ + public static final class Log { + + public static final String COMPONENT_ID = "inflow-extension"; + + private Log() { + + } + + /** + * Action IDs for diagnostic events emitted by the In-Flow Extension layer. + */ + public static final class ActionIDs { + + public static final String EXECUTE = "execute-inflow-extension"; + public static final String PROCESS_RESPONSE = "process-inflow-extension-response"; + + private ActionIDs() { + + } + } + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcher.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcher.java index dd7221c54f27..e61d75868057 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcher.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 @@ -21,34 +21,10 @@ import java.util.List; /** - * Utility class for hierarchical prefix-based path matching for In-Flow Extension access control. - * - *

      Expose and modify path lists always contain exact leaf paths (no trailing {@code /}). - * Two distinct matching operations are needed, served by two explicit methods:

      - *
        - *
      • {@link #anyExposedUnder(String, List)} — area-gate check: is any leaf path - * in the list under a given area prefix (e.g. {@code /user/claims/})?
      • - *
      • {@link #isExposedPath(String, List)} — exact check: is a specific leaf path - * (e.g. {@code /user/claims/http://wso2.org/claims/email}) present in the list?
      • - *
      - * - *

      Prefix hierarchy:

      - *
      - * /user/                           - User context
      - *   /user/claims/{claimURI}        - User claims
      - *   /user/userId                   - User's unique identifier
      - *   /user/userStoreDomain          - User store domain
      - *   /user/credentials/{key}        - User credentials (no key validation required)
      - *
      - * /properties/{key}                - Flow properties (fully extensible)
      - *
      - * /flow/                           - Flow metadata (READ-ONLY)
      - *   /flow/tenantDomain             - Tenant domain
      - *   /flow/applicationId            - Application ID
      - *   /flow/flowType                 - Flow type (REGISTRATION, etc.)
      - *   /flow/callbackUrl              - Callback URL (expose-only)
      - *   /flow/portalUrl                - Portal URL (expose-only)
      - * 
      + * Utility class for hierarchical prefix-based path matching used in In-Flow Extension + * access control. + * Provides area-gate checks via {@link #anyExposedUnder(String, List)} and exact + * leaf-path checks via {@link #isExposedPath(String, List)}. */ public final class HierarchicalPrefixMatcher { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java index 315dd7f76a67..9c5925cc662b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 @@ -52,28 +52,17 @@ import java.util.UUID; /** - * This class is responsible for executing In-Flow Extension actions during flow execution. - * It integrates with the {@link ActionExecutorService} to call external services and process - * their responses. - * - *

      Execution lifecycle:

      - *
        - *
      1. Extract executor metadata: {@code actionId}.
      2. - *
      3. Build a minimal {@link FlowContext} containing only the {@link FlowExecutionContext}. - * The request builder resolves access config / encryption from the action and populates - * additional FlowContext keys for the response processor.
      4. - *
      5. Invoke the external service via {@link ActionExecutorService}.
      6. - *
      7. Map the {@link ActionExecutionStatus} to an {@link ExecutorResponse}. - * On {@code SUCCESS}, pending context updates collected by the response processor - * are extracted from the {@link FlowContext} and forwarded to - * {@code TaskExecutionNode} via {@link ExecutorResponse} fields - * ({@code updatedUserClaims}, {@code userCredentials}, {@code contextProperties}).
      8. - *
      + * Executes In-Flow Extension actions during flow execution by delegating to + * {@link ActionExecutorService} and mapping the result to an {@link ExecutorResponse}. + * On success, pending context updates (claims, credentials, properties) are forwarded + * to the flow engine through the response object. */ public class InFlowExtensionExecutor implements Executor { private static final Log LOG = LogFactory.getLog(InFlowExtensionExecutor.class); private static final String EXECUTOR_NAME = "InFlowExtensionExecutor"; + private static final String CONFIG_PARAM_ACTION_TYPE = "actionType"; + private static final String CONFIG_PARAM_ACTION_ID = "actionId"; @@ -84,29 +73,16 @@ public String getName() { } @Override - @SuppressWarnings("unchecked") public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineException { - ExecutorResponse response = new ExecutorResponse(); - String actionId = getMetadataValue(context, InFlowExtensionConstants.ACTION_ID_METADATA_KEY); if (actionId == null || actionId.isEmpty()) { LOG.warn("No action ID configured for In-Flow Extension executor. Cannot execute."); - if (LoggerUtils.isDiagnosticLogsEnabled()) { - LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( - InFlowExtensionLogConstants.COMPONENT_ID, - InFlowExtensionLogConstants.ActionIDs.EXECUTE) - .resultMessage("In-Flow Extension action execution failed: action ID is not configured.") - .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) - .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) - .resultStatus(DiagnosticLog.ResultStatus.FAILED)); - } - response.setResult(ExecutorStatus.STATUS_ERROR); - response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); - response.setErrorMessage("Extension is not configured."); - response.setErrorDescription("The In-Flow Extension action is missing required configuration. " + + triggerDiagnosticFailure(null, + "In-Flow Extension action execution failed: action ID is not configured."); + return buildErrorResponse("Extension is not configured.", + "The In-Flow Extension action is missing required configuration. " + "Contact your administrator."); - return response; } if (LOG.isDebugEnabled()) { @@ -118,26 +94,17 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE ActionExecutorService actionExecutorService = getActionExecutorService(); if (actionExecutorService == null) { LOG.error("ActionExecutorService is not available. In-Flow Extension cannot execute. actionId: " + actionId); + triggerDiagnosticFailure(actionId, + "In-Flow Extension action execution failed: ActionExecutorService is unavailable."); throw new FlowEngineException("ActionExecutorService is not available."); } if (!actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)) { LOG.debug("In-Flow Extension action execution is disabled."); - if (LoggerUtils.isDiagnosticLogsEnabled()) { - LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( - InFlowExtensionLogConstants.COMPONENT_ID, - InFlowExtensionLogConstants.ActionIDs.EXECUTE) - .resultMessage("In-Flow Extension action execution failed: action type is disabled.") - .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) - .configParam("actionId", actionId) - .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) - .resultStatus(DiagnosticLog.ResultStatus.FAILED)); - } - response.setResult(ExecutorStatus.STATUS_ERROR); - response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); - response.setErrorMessage("Extension execution is disabled."); - response.setErrorDescription("The In-Flow Extension action type is currently disabled on this server."); - return response; + triggerDiagnosticFailure(actionId, + "In-Flow Extension action execution failed: action type is disabled."); + return buildErrorResponse("Extension execution is disabled.", + "The In-Flow Extension action type is currently disabled on this server."); } try { @@ -158,63 +125,21 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE // On success, extract pending context updates collected by the response processor // and forward them to TaskExecutionNode via ExecutorResponse fields. if (ExecutorStatus.STATUS_COMPLETE.equals(executionResponse.getResult())) { - Map pendingClaims = - (Map) flowContext.getValue(InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class); - if (pendingClaims != null && !pendingClaims.isEmpty()) { - executionResponse.setUpdatedUserClaims(pendingClaims); - } - Map pendingCredentials = - (Map) flowContext.getValue(InFlowExtensionConstants.PENDING_CREDENTIALS_KEY, Map.class); - if (pendingCredentials != null && !pendingCredentials.isEmpty()) { - executionResponse.setUserCredentials(pendingCredentials); - } - Map pendingProperties = - (Map) flowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); - if (pendingProperties != null && !pendingProperties.isEmpty()) { - executionResponse.setContextProperty(pendingProperties); - } - if (LOG.isDebugEnabled()) { - LOG.debug("In-Flow Extension action succeeded. actionId: " + actionId - + ", pendingClaims: " + (pendingClaims != null ? pendingClaims.size() : 0) - + ", pendingCredentials: " + (pendingCredentials != null ? pendingCredentials.size() : 0) - + ", pendingProperties: " + (pendingProperties != null ? pendingProperties.size() : 0)); - } + applyPendingContextUpdates(executionResponse, flowContext, actionId); } if (ExecutorStatus.STATUS_RETRY.equals(executionResponse.getResult())) { - Map additionalInfo = executionResponse.getAdditionalInfo(); - if (additionalInfo == null) { - additionalInfo = new HashMap<>(); - } - additionalInfo.put(InFlowExtensionConstants.FAILURE_TYPE_KEY, - InFlowExtensionConstants.IN_FLOW_EXTENSION_FAILURE_TYPE); - executionResponse.setAdditionalInfo(additionalInfo); - if (LOG.isDebugEnabled()) { - LOG.debug("In-Flow Extension action returned FAILED. actionId: " + actionId - + ", reason: " + additionalInfo.get(InFlowExtensionConstants.FAILURE_MESSAGE_KEY)); - } + applyRetryMetadata(executionResponse, actionId); } return executionResponse; } catch (ActionExecutionException e) { logActionExecutionException(e, actionId); - if (LoggerUtils.isDiagnosticLogsEnabled()) { - LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( - InFlowExtensionLogConstants.COMPONENT_ID, - InFlowExtensionLogConstants.ActionIDs.EXECUTE) - .resultMessage("In-Flow Extension action execution failed: " + e.getMessage()) - .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) - .configParam("actionId", actionId) - .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) - .resultStatus(DiagnosticLog.ResultStatus.FAILED)); - } - response.setResult(ExecutorStatus.STATUS_ERROR); - response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); - response.setErrorMessage("An error occurred while processing the extension. Please try again."); - response.setErrorDescription("The external extension service could not complete the request. " + + triggerDiagnosticFailure(actionId, "In-Flow Extension action execution failed: " + e.getMessage()); + return buildErrorResponse("An error occurred while processing the extension. Please try again.", + "The external extension service could not complete the request. " + "If the problem persists, contact your administrator."); - return response; } } @@ -256,96 +181,22 @@ private ExecutorResponse mapExecutionStatus(ActionExecutionStatus executionSt switch (executionStatus.getStatus()) { case SUCCESS: response.setResult(ExecutorStatus.STATUS_COMPLETE); - break; + return response; case FAILED: - response.setResult(ExecutorStatus.STATUS_RETRY); - Failure failure = (Failure) executionStatus.getResponse(); - if (failure != null) { - Map failureInfo = new HashMap<>(); - if (failure.getFailureReason() != null) { - failureInfo.put(InFlowExtensionConstants.FAILURE_MESSAGE_KEY, - failure.getFailureReason()); - } - if (failure.getFailureDescription() != null) { - failureInfo.put(InFlowExtensionConstants.FAILURE_DESCRIPTION_KEY, - failure.getFailureDescription()); - } - response.setAdditionalInfo(failureInfo); - response.setErrorMessage(buildUserFacingErrorMessage(failure)); - } - break; + handleFailedStatus(response, executionStatus); + return response; case ERROR: - response.setResult(ExecutorStatus.STATUS_ERROR); - response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); - Error error = (Error) executionStatus.getResponse(); - if (error != null) { - response.setErrorMessage(stripI18nBraces(error.getErrorMessage())); - response.setErrorDescription(stripI18nBraces(error.getErrorDescription())); - } - break; - - case INCOMPLETE: { - String redirectUrl = flowContext.getValue(InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, String.class); - if (redirectUrl == null || redirectUrl.isEmpty()) { - // Defensive: response processor should have rejected this earlier. - LOG.warn("In-Flow Extension returned INCOMPLETE without a redirect URL."); - if (LoggerUtils.isDiagnosticLogsEnabled()) { - LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( - InFlowExtensionLogConstants.COMPONENT_ID, - InFlowExtensionLogConstants.ActionIDs.PROCESS_RESPONSE) - .resultMessage("In-Flow Extension returned INCOMPLETE without a redirect URL.") - .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) - .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) - .resultStatus(DiagnosticLog.ResultStatus.FAILED)); - } - response.setResult(ExecutorStatus.STATUS_ERROR); - response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); - response.setErrorMessage("Extension returned INCOMPLETE without a redirect URL."); - response.setErrorDescription("The external extension returned an incomplete response. " + - "Please try again."); - break; - } - - // Generate OTFI — keeps the cache-swap mechanism - String otfi = UUID.randomUUID().toString(); - while (otfi.equals(context.getContextIdentifier())) { - otfi = UUID.randomUUID().toString(); - } - Map redirectProps = new HashMap<>(); - redirectProps.put(Constants.OTFI, otfi); - response.setContextProperty(redirectProps); - - String separator = redirectUrl.contains("?") ? "&" : "?"; - String urlWithFlowId = redirectUrl + separator + "flowId=" + otfi; - - Map redirectInfo = new HashMap<>(); - redirectInfo.put(Constants.REDIRECT_URL, urlWithFlowId); - response.setAdditionalInfo(redirectInfo); - - response.setResult(ExecutorStatus.STATUS_EXTERNAL_REDIRECTION); - if (LOG.isDebugEnabled()) { - try { - String host = new java.net.URI(redirectUrl).getHost(); - LOG.debug("In-Flow Extension returned INCOMPLETE. Redirecting to: " + host - + ", flowId (OTFI) generated."); - } catch (java.net.URISyntaxException ignored) { - LOG.debug("In-Flow Extension returned INCOMPLETE with a redirect URL."); - } - } - break; - } + handleErrorStatus(response, executionStatus); + return response; + + case INCOMPLETE: + return handleIncompleteExecutionStatus(response, flowContext, context); default: - LOG.warn("Unknown execution status: " + executionStatus.getStatus()); - response.setResult(ExecutorStatus.STATUS_ERROR); - response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); - response.setErrorMessage("Extension returned an unexpected response."); - response.setErrorDescription("The In-Flow Extension returned an unrecognised status. Please try again."); + return handleUnknownExecutionStatus(response, executionStatus); } - - return response; } /** @@ -369,6 +220,199 @@ private String buildUserFacingErrorMessage(Failure failure) { return "The operation could not be completed due to an external service failure."; } + private ExecutorResponse buildErrorResponse(String errorMessage, String errorDescription) { + + ExecutorResponse response = new ExecutorResponse(); + response.setResult(ExecutorStatus.STATUS_ERROR); + response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); + response.setErrorMessage(errorMessage); + response.setErrorDescription(errorDescription); + return response; + } + + private void applyRetryMetadata(ExecutorResponse response, String actionId) { + + Map additionalInfo = response.getAdditionalInfo(); + if (additionalInfo == null) { + additionalInfo = new HashMap<>(); + } + additionalInfo.put(InFlowExtensionConstants.FAILURE_TYPE_KEY, + InFlowExtensionConstants.IN_FLOW_EXTENSION_FAILURE_TYPE); + response.setAdditionalInfo(additionalInfo); + + if (LOG.isDebugEnabled()) { + LOG.debug("In-Flow Extension action returned FAILED. actionId: " + actionId + + ", reason: " + additionalInfo.get(InFlowExtensionConstants.FAILURE_MESSAGE_KEY)); + } + } + + private void handleFailedStatus(ExecutorResponse response, ActionExecutionStatus executionStatus) { + + response.setResult(ExecutorStatus.STATUS_RETRY); + Failure failure = (Failure) executionStatus.getResponse(); + if (failure == null) { + return; + } + + Map failureInfo = new HashMap<>(); + if (failure.getFailureReason() != null) { + failureInfo.put(InFlowExtensionConstants.FAILURE_MESSAGE_KEY, failure.getFailureReason()); + } + if (failure.getFailureDescription() != null) { + failureInfo.put(InFlowExtensionConstants.FAILURE_DESCRIPTION_KEY, failure.getFailureDescription()); + } + response.setAdditionalInfo(failureInfo); + response.setErrorMessage(buildUserFacingErrorMessage(failure)); + } + + private void handleErrorStatus(ExecutorResponse response, ActionExecutionStatus executionStatus) { + + response.setResult(ExecutorStatus.STATUS_ERROR); + response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); + Error error = (Error) executionStatus.getResponse(); + if (error == null) { + return; + } + + response.setErrorMessage(stripI18nBraces(error.getErrorMessage())); + response.setErrorDescription(stripI18nBraces(error.getErrorDescription())); + } + + private ExecutorResponse handleIncompleteExecutionStatus(ExecutorResponse response, FlowContext flowContext, + FlowExecutionContext context) { + + String redirectUrl = flowContext.getValue(InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, String.class); + if (redirectUrl == null || redirectUrl.isEmpty()) { + // Defensive: response processor should have rejected this earlier. + LOG.debug("In-Flow Extension returned INCOMPLETE without a redirect URL."); + triggerDiagnosticFailure(InFlowExtensionConstants.Log.ActionIDs.PROCESS_RESPONSE, null, + "In-Flow Extension returned INCOMPLETE without a redirect URL."); + return buildErrorResponse("Extension returned INCOMPLETE without a redirect URL.", + "The external extension returned an incomplete response. Please try again."); + } + + String otfi = generateUniqueOtfi(context.getContextIdentifier()); + Map redirectProps = new HashMap<>(); + redirectProps.put(Constants.OTFI, otfi); + response.setContextProperty(redirectProps); + + String urlWithFlowId = appendFlowId(redirectUrl, otfi); + Map redirectInfo = new HashMap<>(); + redirectInfo.put(Constants.REDIRECT_URL, urlWithFlowId); + response.setAdditionalInfo(redirectInfo); + + response.setResult(ExecutorStatus.STATUS_EXTERNAL_REDIRECTION); + triggerDiagnosticSuccess(InFlowExtensionConstants.Log.ActionIDs.PROCESS_RESPONSE, null, + "In-Flow Extension returned INCOMPLETE with a redirect URL."); + + if (LOG.isDebugEnabled()) { + LOG.debug("In-Flow Extension returned INCOMPLETE. Redirect initiated and flowId (OTFI) generated."); + } + + return response; + } + + private ExecutorResponse handleUnknownExecutionStatus(ExecutorResponse response, + ActionExecutionStatus executionStatus) { + + LOG.warn("Unknown execution status: " + executionStatus.getStatus()); + response.setResult(ExecutorStatus.STATUS_ERROR); + response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR.getCode()); + response.setErrorMessage("Extension returned an unexpected response."); + response.setErrorDescription("The In-Flow Extension returned an unrecognised status. Please try again."); + return response; + } + + private String generateUniqueOtfi(String currentContextIdentifier) { + + // Avoid accidental collision with the current context identifier. + String otfi = UUID.randomUUID().toString(); + while (otfi.equals(currentContextIdentifier)) { + otfi = UUID.randomUUID().toString(); + } + return otfi; + } + + private String appendFlowId(String redirectUrl, String otfi) { + + String separator = redirectUrl.contains("?") ? "&" : "?"; + return redirectUrl + separator + "flowId=" + otfi; + } + + @SuppressWarnings("unchecked") + private void applyPendingContextUpdates(ExecutorResponse response, FlowContext flowContext, String actionId) { + + Map pendingClaims = + flowContext.getValue(InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class); + if (pendingClaims != null && !pendingClaims.isEmpty()) { + response.setUpdatedUserClaims(pendingClaims); + } + + Map pendingCredentials = + flowContext.getValue(InFlowExtensionConstants.PENDING_CREDENTIALS_KEY, Map.class); + if (pendingCredentials != null && !pendingCredentials.isEmpty()) { + response.setUserCredentials(pendingCredentials); + } + + Map pendingProperties = + flowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); + if (pendingProperties != null && !pendingProperties.isEmpty()) { + response.setContextProperty(pendingProperties); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("In-Flow Extension action succeeded. actionId: " + actionId + + ", pendingClaims: " + (pendingClaims != null ? pendingClaims.size() : 0) + + ", pendingCredentials: " + (pendingCredentials != null ? pendingCredentials.size() : 0) + + ", pendingProperties: " + (pendingProperties != null ? pendingProperties.size() : 0)); + } + } + + private void triggerDiagnosticFailure(String actionId, String resultMessage) { + + triggerDiagnosticFailure(InFlowExtensionConstants.Log.ActionIDs.EXECUTE, actionId, resultMessage); + } + + private void triggerDiagnosticFailure(String diagnosticActionId, String actionId, String resultMessage) { + + if (!LoggerUtils.isDiagnosticLogsEnabled()) { + return; + } + + DiagnosticLog.DiagnosticLogBuilder builder = new DiagnosticLog.DiagnosticLogBuilder( + InFlowExtensionConstants.Log.COMPONENT_ID, diagnosticActionId) + .resultMessage(resultMessage) + .configParam(CONFIG_PARAM_ACTION_TYPE, ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED); + + if (actionId != null) { + builder.configParam(CONFIG_PARAM_ACTION_ID, actionId); + } + + LoggerUtils.triggerDiagnosticLogEvent(builder); + } + + private void triggerDiagnosticSuccess(String diagnosticActionId, String actionId, String resultMessage) { + + if (!LoggerUtils.isDiagnosticLogsEnabled()) { + return; + } + + DiagnosticLog.DiagnosticLogBuilder builder = new DiagnosticLog.DiagnosticLogBuilder( + InFlowExtensionConstants.Log.COMPONENT_ID, diagnosticActionId) + .resultMessage(resultMessage) + .configParam(CONFIG_PARAM_ACTION_TYPE, ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.SUCCESS); + + if (actionId != null) { + builder.configParam(CONFIG_PARAM_ACTION_ID, actionId); + } + + LoggerUtils.triggerDiagnosticLogEvent(builder); + } + private ActionExecutorService getActionExecutorService() { return InFlowExtensionDataHolder.getInstance().getActionExecutorService(); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionLogConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionLogConstants.java deleted file mode 100644 index 403e59c94d3a..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionLogConstants.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2025, 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.inflow.extensions.executor; - -/** - * Diagnostic log constants for the In-Flow Extension layer. - * - *

      These constants are intentionally separate from - * {@link org.wso2.carbon.identity.action.execution.api.constant.ActionExecutionLogConstants} - * because the events tracked here are flow-engine concerns (executor lifecycle, operation routing, - * JWE decryption) rather than action HTTP-call concerns (which remain under "action-execution").

      - */ -public class InFlowExtensionLogConstants { - - private InFlowExtensionLogConstants() { - } - - public static final String COMPONENT_ID = "inflow-extension"; - - /** - * Action IDs for diagnostic log events emitted by the In-Flow Extension layer. - */ - public static class ActionIDs { - - public static final String EXECUTE = "execute-inflow-extension"; - public static final String PROCESS_RESPONSE = "process-inflow-extension-response"; - } -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilder.java index 243d0e3e805a..584fdd165626 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 @@ -65,6 +65,10 @@ public class InFlowExtensionRequestBuilder implements ActionExecutionRequestBuilder { private static final Log LOG = LogFactory.getLog(InFlowExtensionRequestBuilder.class); + private static final int MAX_DIAGNOSTIC_PATHS = 20; + private static final String DIAGNOSTIC_REASON = "reason"; + private static final String REASON_UNSUPPORTED_ACTION = "unsupported-action-model"; + private static final String REASON_OMITTED_ENCRYPTED_EXPOSE_PATHS = "omitted-encrypted-expose-paths"; @Override public ActionType getSupportedActionType() { @@ -77,88 +81,30 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex ActionExecutionRequestContext actionExecutionContext) throws ActionExecutionRequestBuilderException { - FlowExecutionContext execCtx = flowContext.getValue( - InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, FlowExecutionContext.class); - if (execCtx == null) { - throw new ActionExecutionRequestBuilderException( - "FlowExecutionContext not found in FlowContext."); + FlowExecutionContext execCtx = getFlowExecutionContextOrThrow(flowContext); + ResolvedActionConfig resolvedActionConfig = resolveActionConfig(actionExecutionContext, execCtx.getFlowType()); + if (resolvedActionConfig.isFallback()) { + return buildFallbackRequest(flowContext, execCtx); } - // Resolve action-specific config. - Action rawAction = actionExecutionContext.getAction(); - AccessConfig accessConfig = null; - Encryption encryption = null; - if (rawAction instanceof InFlowExtensionAction) { - InFlowExtensionAction ext = (InFlowExtensionAction) rawAction; - accessConfig = ext.resolveAccessConfig(execCtx.getFlowType()); - encryption = ext.getEncryption(); - } else { - if (LOG.isDebugEnabled()) { - LOG.debug("No InFlowExtensionAction resolved. Proceeding with empty access configuration."); - } - } + AccessConfig accessConfig = resolvedActionConfig.getAccessConfig(); + Encryption encryption = resolvedActionConfig.getEncryption(); - List expose; - if (accessConfig != null && accessConfig.getExpose() != null) { - expose = accessConfig.getExposePaths(); - } else { - expose = Collections.emptyList(); - } - - if (expose.isEmpty() && LOG.isDebugEnabled()) { - LOG.debug("No expose paths configured for In-Flow Extension action. Event will contain no data fields."); - } - - // Store resolved modify paths (with encryption flags) for the response processor. - List modifyPaths = (accessConfig != null && accessConfig.getModify() != null) - ? accessConfig.getModify() : Collections.emptyList(); + List exposePaths = resolveExposePaths(accessConfig); + List modifyPaths = resolveModifyPaths(accessConfig); flowContext.add(InFlowExtensionConstants.MODIFY_PATHS_KEY, modifyPaths); - if (LoggerUtils.isDiagnosticLogsEnabled()) { - LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( - ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, - ActionExecutionLogConstants.ActionIDs.PROCESS_ACTION_REQUEST) - .resultMessage("Building request for In-Flow Extension action.") - .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) - .configParam("flowType", execCtx.getFlowType()) - .configParam("exposePaths", expose.size()) - .configParam("modifyPaths", modifyPaths.size()) - .configParam("outboundEncryption", encryption != null) - .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) - .resultStatus(DiagnosticLog.ResultStatus.SUCCESS)); - } - - // Build allowed operations: REPLACE (from modify paths, if any) + REDIRECT (if enabled). List allowedOperations = buildAllowedOperations(accessConfig, flowContext); + String certificatePEM = resolveOutboundCertificate(accessConfig, encryption); + ExposeResolution exposeResolution = pruneEncryptedExposePathsWithoutCertificate( + exposePaths, accessConfig, certificatePEM); - // Determine certificate PEM for outbound encryption. - String certificatePEM = null; - if (accessConfig != null && encryption != null && encryption.getCertificate() != null) { - certificatePEM = encryption.getCertificate().getCertificateContent(); - } - - // Fail fast: if any expose path is marked encrypted but no certificate is configured, - // proceeding would silently leak sensitive data as plaintext. - if (certificatePEM == null && accessConfig != null) { - for (String exposePath : expose) { - if (accessConfig.isExposePathEncrypted(exposePath)) { - LOG.error("Outbound encryption required for expose path '" + exposePath - + "' but no certificate is configured for this In-Flow Extension action."); - throw new ActionExecutionRequestBuilderException( - "Outbound encryption is configured for expose path '" + exposePath + - "' but no outbound certificate is configured for this action."); - } - } - } - - InFlowExtensionEvent event = buildEvent(execCtx, expose, accessConfig, certificatePEM); + triggerRequestBuildDiagnostic(execCtx, exposeResolution.getEffectiveExposePaths(), + modifyPaths, encryption, exposeResolution.getOmittedEncryptedExposePaths()); - return new ActionExecutionRequest.Builder() - .actionType(ActionType.IN_FLOW_EXTENSION) - .flowId(execCtx.getContextIdentifier()) - .event(event) - .allowedOperations(allowedOperations) - .build(); + InFlowExtensionEvent event = buildEvent(execCtx, exposeResolution.getEffectiveExposePaths(), + accessConfig, certificatePEM); + return buildRequestPayload(execCtx, event, allowedOperations); } /** @@ -178,51 +124,18 @@ private List buildAllowedOperations(AccessConfig accessConfig, List allowedOps = new ArrayList<>(); - if (accessConfig != null && accessConfig.getModify() != null && !accessConfig.getModify().isEmpty()) { - List cleanPaths = new ArrayList<>(); - Map pathTypeAnnotations = new HashMap<>(); - - for (ContextPath modifyPath : accessConfig.getModify()) { - String rawPath = modifyPath.getPath(); - if (rawPath == null) { - continue; - } - - String[] stripped = PathTypeAnnotationUtil.stripAnnotation(rawPath); - String cleanPath = stripped[0]; - String annotation = stripped[1]; - if (annotation != null) { - if (!PathTypeAnnotationUtil.validateAnnotationLimits(annotation)) { - LOG.warn("Annotation for path " + cleanPath - + " exceeds maximum attribute limit. Skipping path."); - continue; - } - pathTypeAnnotations.put(cleanPath, annotation); - } - cleanPaths.add(cleanPath); - } - - if (!cleanPaths.isEmpty()) { - AllowedOperation replaceOp = new AllowedOperation(); - replaceOp.setOp(Operation.REPLACE); - replaceOp.setPaths(cleanPaths); - allowedOps.add(replaceOp); + if (hasModifyPaths(accessConfig)) { + AllowedModifyExtraction extraction = extractAllowedModifyPaths(accessConfig.getModify()); + addReplaceOperationIfAny(allowedOps, extraction.getCleanPaths()); + storeAnnotationsIfAny(flowContext, extraction.getPathTypeAnnotations()); - if (!pathTypeAnnotations.isEmpty()) { - flowContext.add(InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); - } - - if (LOG.isDebugEnabled()) { - LOG.debug("Built REPLACE allowed operation with " + cleanPaths.size() - + " modify paths. Path annotations: " + pathTypeAnnotations.size()); - } + if (LOG.isDebugEnabled() && !extraction.getSkippedPaths().isEmpty()) { + LOG.debug("Skipped " + extraction.getSkippedPaths().size() + + " modify path(s) due to invalid or missing path definitions."); } } - // REDIRECT is always advertised — redirection is unconditionally enabled. - AllowedOperation redirectOp = new AllowedOperation(); - redirectOp.setOp(Operation.REDIRECT); - allowedOps.add(redirectOp); + addRedirectOperation(allowedOps); return allowedOps; } @@ -244,86 +157,14 @@ private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List properties = context.getProperties(); - if (properties != null && !properties.isEmpty()) { - eventBuilder.flowProperties( - filterMap(properties, HierarchicalPrefixMatcher.PROPERTIES_PREFIX, - expose, accessConfig, certificatePEM)); - } - } + applyTenant(eventBuilder, context, expose); + applyOrganization(eventBuilder); + applyApplication(eventBuilder, context, expose); + applyUserAndUserStore(eventBuilder, context, expose, accessConfig, certificatePEM); + applyFlowMetadata(eventBuilder, context, expose); + applyFlowProperties(eventBuilder, context, expose, accessConfig, certificatePEM); return eventBuilder.build(); } @@ -375,53 +216,16 @@ private User buildUser(FlowUser flowUser, List expose, AccessConfig accessConfig, String certificatePEM) throws ActionExecutionRequestBuilderException { - String userId = isLeafExposed(HierarchicalPrefixMatcher.USER_ID_PATH, expose) - ? flowUser.getUserId() : null; - User.Builder userBuilder = new User.Builder(userId); - - if (isAreaExposed(HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX, expose)) { - Map claims = flowUser.getClaims(); - if (claims != null && !claims.isEmpty()) { - List userClaims = new ArrayList<>(); - - for (Map.Entry claim : claims.entrySet()) { - String claimPath = HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX + claim.getKey(); - if (isLeafExposed(claimPath, expose)) { - String claimValue = claim.getValue(); - // Encrypt claim value if the expose path is marked as encrypted. - if (claimValue != null && shouldEncrypt(claimPath, accessConfig, certificatePEM)) { - claimValue = encryptValue(claimValue, certificatePEM); - } - userClaims.add(new UserClaim(claim.getKey(), claimValue)); - } - } - if (!userClaims.isEmpty()) { - userBuilder.claims(userClaims); - } - } + User.Builder userBuilder = new User.Builder(resolveUserId(flowUser, expose)); + List userClaims = buildFilteredClaims(flowUser, expose, accessConfig, certificatePEM); + if (!userClaims.isEmpty()) { + userBuilder.claims(userClaims); } - if (isAreaExposed(HierarchicalPrefixMatcher.USER_CREDENTIALS_PREFIX, expose)) { - Map credentials = flowUser.getUserCredentials(); - if (credentials != null && !credentials.isEmpty()) { - Map filteredCredentials = new HashMap<>(); - for (Map.Entry entry : credentials.entrySet()) { - String credPath = HierarchicalPrefixMatcher.USER_CREDENTIALS_PREFIX + entry.getKey(); - if (isLeafExposed(credPath, expose)) { - String plaintext = new String(entry.getValue()); - java.util.Arrays.fill(entry.getValue(), '\0'); - if (shouldEncrypt(credPath, accessConfig, certificatePEM)) { - filteredCredentials.put(entry.getKey(), - encryptValue(plaintext, certificatePEM).toCharArray()); - } else { - filteredCredentials.put(entry.getKey(), plaintext.toCharArray()); - } - } - } - if (!filteredCredentials.isEmpty()) { - userBuilder.userCredentials(filteredCredentials); - } - } + Map filteredCredentials = + buildFilteredCredentials(flowUser, expose, accessConfig, certificatePEM); + if (!filteredCredentials.isEmpty()) { + userBuilder.userCredentials(filteredCredentials); } return userBuilder.build(); @@ -445,7 +249,7 @@ private Map filterMap(Map map, String areaPrefix, List throws ActionExecutionRequestBuilderException { if (map == null) { - return null; + return Collections.emptyMap(); } Map filtered = new HashMap<>(); @@ -462,6 +266,507 @@ private Map filterMap(Map map, String areaPrefix, List return filtered; } + private FlowExecutionContext getFlowExecutionContextOrThrow(FlowContext flowContext) + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = flowContext.getValue( + InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, FlowExecutionContext.class); + if (execCtx == null) { + throw new ActionExecutionRequestBuilderException("FlowExecutionContext not found in FlowContext."); + } + return execCtx; + } + + private ResolvedActionConfig resolveActionConfig(ActionExecutionRequestContext actionExecutionContext, + String flowType) { + + Action rawAction = actionExecutionContext.getAction(); + if (rawAction instanceof InFlowExtensionAction) { + InFlowExtensionAction ext = (InFlowExtensionAction) rawAction; + return new ResolvedActionConfig(ext.resolveAccessConfig(flowType), ext.getEncryption(), false); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("No InFlowExtensionAction resolved. Falling back to an empty request body."); + } + return new ResolvedActionConfig(null, null, true); + } + + private List resolveExposePaths(AccessConfig accessConfig) { + + if (accessConfig == null || accessConfig.getExpose() == null) { + return Collections.emptyList(); + } + return accessConfig.getExposePaths(); + } + + private List resolveModifyPaths(AccessConfig accessConfig) { + + if (accessConfig == null || accessConfig.getModify() == null) { + return Collections.emptyList(); + } + return accessConfig.getModify(); + } + + private String resolveOutboundCertificate(AccessConfig accessConfig, Encryption encryption) { + + if (accessConfig == null || encryption == null || encryption.getCertificate() == null) { + return null; + } + return encryption.getCertificate().getCertificateContent(); + } + + private ExposeResolution pruneEncryptedExposePathsWithoutCertificate(List exposePaths, + AccessConfig accessConfig, + String certificatePEM) { + + if (certificatePEM != null || accessConfig == null || exposePaths.isEmpty()) { + return new ExposeResolution(exposePaths, Collections.emptyList()); + } + + List effectiveExposePaths = new ArrayList<>(); + List omittedEncryptedExposePaths = new ArrayList<>(); + for (String exposePath : exposePaths) { + if (accessConfig.isExposePathEncrypted(exposePath)) { + omittedEncryptedExposePaths.add(exposePath); + } else { + effectiveExposePaths.add(exposePath); + } + } + + if (!omittedEncryptedExposePaths.isEmpty()) { + LOG.warn("Outbound certificate is not configured. Omitted " + omittedEncryptedExposePaths.size() + + " encrypted expose path(s)."); + triggerOmittedExposeDiagnostic(omittedEncryptedExposePaths); + } + + return new ExposeResolution(effectiveExposePaths, omittedEncryptedExposePaths); + } + + private ActionExecutionRequest buildFallbackRequest(FlowContext flowContext, FlowExecutionContext execCtx) { + + flowContext.add(InFlowExtensionConstants.MODIFY_PATHS_KEY, Collections.emptyList()); + List allowedOperations = buildAllowedOperations(null, flowContext); + triggerFallbackDiagnostic(execCtx); + + InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() + .flowId(execCtx.getContextIdentifier()) + .build(); + return buildRequestPayload(execCtx, event, allowedOperations); + } + + private ActionExecutionRequest buildRequestPayload(FlowExecutionContext execCtx, InFlowExtensionEvent event, + List allowedOperations) { + + return new ActionExecutionRequest.Builder() + .actionType(ActionType.IN_FLOW_EXTENSION) + .flowId(execCtx.getContextIdentifier()) + .event(event) + .allowedOperations(allowedOperations) + .build(); + } + + private void triggerRequestBuildDiagnostic(FlowExecutionContext execCtx, List exposePaths, + List modifyPaths, Encryption encryption, + List omittedEncryptedExposePaths) { + + if (!LoggerUtils.isDiagnosticLogsEnabled()) { + return; + } + + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( + ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, + ActionExecutionLogConstants.ActionIDs.PROCESS_ACTION_REQUEST) + .resultMessage("Building request for In-Flow Extension action.") + .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .configParam("flowType", execCtx.getFlowType()) + .configParam("exposePaths", exposePaths.size()) + .configParam("modifyPaths", modifyPaths.size()) + .configParam("outboundEncryption", encryption != null) + .inputParam("exposePathKeys", limitForDiagnostic(exposePaths)) + .inputParam("modifyPathKeys", limitForDiagnostic(extractModifyPathKeys(modifyPaths))) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.SUCCESS); + + if (!omittedEncryptedExposePaths.isEmpty()) { + diagnosticLogBuilder + .configParam(DIAGNOSTIC_REASON, REASON_OMITTED_ENCRYPTED_EXPOSE_PATHS) + .inputParam("omittedEncryptedExposePaths", limitForDiagnostic(omittedEncryptedExposePaths)); + } + + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + + private List extractModifyPathKeys(List modifyPaths) { + + List modifyPathKeys = new ArrayList<>(); + for (ContextPath modifyPath : modifyPaths) { + if (modifyPath != null && modifyPath.getPath() != null) { + modifyPathKeys.add(modifyPath.getPath()); + } + } + return modifyPathKeys; + } + + private List limitForDiagnostic(List values) { + + if (values.size() <= MAX_DIAGNOSTIC_PATHS) { + return values; + } + return new ArrayList<>(values.subList(0, MAX_DIAGNOSTIC_PATHS)); + } + + private void triggerFallbackDiagnostic(FlowExecutionContext execCtx) { + + if (!LoggerUtils.isDiagnosticLogsEnabled()) { + return; + } + + LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( + ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, + ActionExecutionLogConstants.ActionIDs.PROCESS_ACTION_REQUEST) + .resultMessage("No InFlowExtensionAction resolved. Built minimal fallback request.") + .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .configParam("flowType", execCtx.getFlowType()) + .configParam(DIAGNOSTIC_REASON, REASON_UNSUPPORTED_ACTION) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.SUCCESS)); + } + + private void triggerOmittedExposeDiagnostic(List omittedEncryptedExposePaths) { + + if (!LoggerUtils.isDiagnosticLogsEnabled()) { + return; + } + + LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( + ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, + ActionExecutionLogConstants.ActionIDs.PROCESS_ACTION_REQUEST) + .resultMessage("Omitted encrypted expose paths because outbound certificate is not configured.") + .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .configParam(DIAGNOSTIC_REASON, REASON_OMITTED_ENCRYPTED_EXPOSE_PATHS) + .inputParam("omittedEncryptedExposePaths", limitForDiagnostic(omittedEncryptedExposePaths)) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.SUCCESS)); + } + + private boolean hasModifyPaths(AccessConfig accessConfig) { + + return accessConfig != null && accessConfig.getModify() != null && !accessConfig.getModify().isEmpty(); + } + + private AllowedModifyExtraction extractAllowedModifyPaths(List modifyPaths) { + + List cleanPaths = new ArrayList<>(); + Map pathTypeAnnotations = new HashMap<>(); + List skippedPaths = new ArrayList<>(); + + for (ContextPath modifyPath : modifyPaths) { + String rawPath = modifyPath == null ? null : modifyPath.getPath(); + if (rawPath == null) { + skippedPaths.add(""); + } else { + String[] strippedPath = PathTypeAnnotationUtil.stripAnnotation(rawPath); + String cleanPath = strippedPath[0]; + String annotation = strippedPath[1]; + + boolean isAnnotationValid = annotation == null || PathTypeAnnotationUtil + .validateAnnotationLimits(annotation); + if (isAnnotationValid) { + if (annotation != null) { + pathTypeAnnotations.put(cleanPath, annotation); + } + cleanPaths.add(cleanPath); + } else { + LOG.warn("Annotation for path " + cleanPath + + " exceeds maximum attribute limit. Skipping path."); + skippedPaths.add(cleanPath); + } + } + } + + return new AllowedModifyExtraction(cleanPaths, pathTypeAnnotations, skippedPaths); + } + + private void addReplaceOperationIfAny(List allowedOperations, List cleanPaths) { + + if (cleanPaths.isEmpty()) { + return; + } + + AllowedOperation replaceOp = new AllowedOperation(); + replaceOp.setOp(Operation.REPLACE); + replaceOp.setPaths(cleanPaths); + allowedOperations.add(replaceOp); + } + + private void storeAnnotationsIfAny(FlowContext flowContext, Map pathTypeAnnotations) { + + if (!pathTypeAnnotations.isEmpty()) { + flowContext.add(InFlowExtensionConstants.PATH_TYPE_ANNOTATIONS_KEY, pathTypeAnnotations); + } + } + + private void addRedirectOperation(List allowedOperations) { + + AllowedOperation redirectOp = new AllowedOperation(); + redirectOp.setOp(Operation.REDIRECT); + allowedOperations.add(redirectOp); + } + + private void applyTenant(InFlowExtensionEvent.Builder eventBuilder, FlowExecutionContext context, + List expose) { + + if (!isLeafExposed(HierarchicalPrefixMatcher.FLOW_TENANT_PATH, expose)) { + return; + } + + String tenantDomain = context.getTenantDomain(); + if (tenantDomain != null) { + int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); + eventBuilder.tenant(new Tenant(String.valueOf(tenantId), tenantDomain)); + } + } + + private void applyOrganization(InFlowExtensionEvent.Builder eventBuilder) { + + org.wso2.carbon.identity.core.context.model.Organization coreOrg = + IdentityContext.getThreadLocalIdentityContext().getOrganization(); + if (coreOrg == null) { + return; + } + + eventBuilder.organization(new Organization.Builder() + .id(coreOrg.getId()) + .name(coreOrg.getName()) + .orgHandle(coreOrg.getOrganizationHandle()) + .depth(coreOrg.getDepth()) + .build()); + } + + private void applyApplication(InFlowExtensionEvent.Builder eventBuilder, FlowExecutionContext context, + List expose) { + + if (!isLeafExposed(HierarchicalPrefixMatcher.FLOW_APP_ID_PATH, expose)) { + return; + } + + String appId = context.getApplicationId(); + if (appId != null) { + eventBuilder.application(new Application(appId, null)); + } + } + + private void applyUserAndUserStore(InFlowExtensionEvent.Builder eventBuilder, FlowExecutionContext context, + List expose, AccessConfig accessConfig, + String certificatePEM) throws ActionExecutionRequestBuilderException { + + if (!isAreaExposed(HierarchicalPrefixMatcher.USER_PREFIX, expose)) { + return; + } + + FlowUser flowUser = context.getFlowUser(); + if (flowUser == null) { + return; + } + + eventBuilder.user(buildUser(flowUser, expose, accessConfig, certificatePEM)); + if (isLeafExposed(HierarchicalPrefixMatcher.USER_STORE_DOMAIN_PATH, expose) + && flowUser.getUserStoreDomain() != null) { + eventBuilder.userStore(new UserStore(flowUser.getUserStoreDomain())); + } + } + + private void applyFlowMetadata(InFlowExtensionEvent.Builder eventBuilder, FlowExecutionContext context, + List expose) { + + if (isLeafExposed(HierarchicalPrefixMatcher.FLOW_TYPE_PATH, expose)) { + eventBuilder.flowType(context.getFlowType()); + } + + eventBuilder.flowId(context.getContextIdentifier()); + + if (isLeafExposed(HierarchicalPrefixMatcher.FLOW_CALLBACK_URL_PATH, expose) + && context.getCallbackUrl() != null) { + eventBuilder.callbackUrl(context.getCallbackUrl()); + } + + if (isLeafExposed(HierarchicalPrefixMatcher.FLOW_PORTAL_URL_PATH, expose) + && context.getPortalUrl() != null) { + eventBuilder.portalUrl(context.getPortalUrl()); + } + } + + private void applyFlowProperties(InFlowExtensionEvent.Builder eventBuilder, FlowExecutionContext context, + List expose, AccessConfig accessConfig, + String certificatePEM) throws ActionExecutionRequestBuilderException { + + if (!isAreaExposed(HierarchicalPrefixMatcher.PROPERTIES_PREFIX, expose)) { + return; + } + + Map properties = context.getProperties(); + if (properties != null && !properties.isEmpty()) { + eventBuilder.flowProperties( + filterMap(properties, HierarchicalPrefixMatcher.PROPERTIES_PREFIX, + expose, accessConfig, certificatePEM)); + } + } + + private String resolveUserId(FlowUser flowUser, List expose) { + + if (isLeafExposed(HierarchicalPrefixMatcher.USER_ID_PATH, expose)) { + return flowUser.getUserId(); + } + return null; + } + + private List buildFilteredClaims(FlowUser flowUser, List expose, + AccessConfig accessConfig, String certificatePEM) + throws ActionExecutionRequestBuilderException { + + if (!isAreaExposed(HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX, expose)) { + return Collections.emptyList(); + } + + Map claims = flowUser.getClaims(); + if (claims == null || claims.isEmpty()) { + return Collections.emptyList(); + } + + List userClaims = new ArrayList<>(); + for (Map.Entry claim : claims.entrySet()) { + String claimPath = HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX + claim.getKey(); + if (isLeafExposed(claimPath, expose)) { + String claimValue = claim.getValue(); + if (claimValue != null && shouldEncrypt(claimPath, accessConfig, certificatePEM)) { + claimValue = encryptValue(claimValue, certificatePEM); + } + userClaims.add(new UserClaim(claim.getKey(), claimValue)); + } + } + return userClaims; + } + + private Map buildFilteredCredentials(FlowUser flowUser, List expose, + AccessConfig accessConfig, String certificatePEM) + throws ActionExecutionRequestBuilderException { + + if (!isAreaExposed(HierarchicalPrefixMatcher.USER_CREDENTIALS_PREFIX, expose)) { + return Collections.emptyMap(); + } + + Map credentials = flowUser.getUserCredentials(); + if (credentials == null || credentials.isEmpty()) { + return Collections.emptyMap(); + } + + Map filteredCredentials = new HashMap<>(); + for (Map.Entry entry : credentials.entrySet()) { + String credentialPath = HierarchicalPrefixMatcher.USER_CREDENTIALS_PREFIX + entry.getKey(); + if (isLeafExposed(credentialPath, expose)) { + char[] credentialValue = entry.getValue(); + String plaintext = new String(credentialValue); + java.util.Arrays.fill(credentialValue, '\0'); + + filteredCredentials.put(entry.getKey(), + toEncryptedOrPlainCredentialChars(plaintext, credentialPath, accessConfig, certificatePEM)); + } + } + return filteredCredentials; + } + + private char[] toEncryptedOrPlainCredentialChars(String plaintext, String credentialPath, + AccessConfig accessConfig, String certificatePEM) + throws ActionExecutionRequestBuilderException { + + if (shouldEncrypt(credentialPath, accessConfig, certificatePEM)) { + return encryptValue(plaintext, certificatePEM).toCharArray(); + } + return plaintext.toCharArray(); + } + + private static class ResolvedActionConfig { + + private final AccessConfig accessConfig; + private final Encryption encryption; + private final boolean fallback; + + private ResolvedActionConfig(AccessConfig accessConfig, Encryption encryption, boolean fallback) { + + this.accessConfig = accessConfig; + this.encryption = encryption; + this.fallback = fallback; + } + + private AccessConfig getAccessConfig() { + + return accessConfig; + } + + private Encryption getEncryption() { + + return encryption; + } + + private boolean isFallback() { + + return fallback; + } + } + + private static class ExposeResolution { + + private final List effectiveExposePaths; + private final List omittedEncryptedExposePaths; + + private ExposeResolution(List effectiveExposePaths, List omittedEncryptedExposePaths) { + + this.effectiveExposePaths = effectiveExposePaths; + this.omittedEncryptedExposePaths = omittedEncryptedExposePaths; + } + + private List getEffectiveExposePaths() { + + return effectiveExposePaths; + } + + private List getOmittedEncryptedExposePaths() { + + return omittedEncryptedExposePaths; + } + } + + private static class AllowedModifyExtraction { + + private final List cleanPaths; + private final Map pathTypeAnnotations; + private final List skippedPaths; + + private AllowedModifyExtraction(List cleanPaths, Map pathTypeAnnotations, + List skippedPaths) { + + this.cleanPaths = cleanPaths; + this.pathTypeAnnotations = pathTypeAnnotations; + this.skippedPaths = skippedPaths; + } + + private List getCleanPaths() { + + return cleanPaths; + } + + private Map getPathTypeAnnotations() { + + return pathTypeAnnotations; + } + + private List getSkippedPaths() { + + return skippedPaths; + } + } + /** * Check if any exposed leaf path falls under the given area prefix. * Used as a gate before iterating a data block (claims, credentials, properties). diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessor.java index ea839c01c820..2224cace85da 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessor.java @@ -24,6 +24,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.identity.action.execution.api.constant.ActionExecutionLogConstants; +import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; +import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; +import org.wso2.carbon.identity.flow.inflow.extensions.internal.InFlowExtensionDataHolder; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionResponseProcessorException; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionResponseContext; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionStatus; @@ -78,6 +81,12 @@ public class InFlowExtensionResponseProcessor implements ActionExecutionResponse private static final Log LOG = LogFactory.getLog(InFlowExtensionResponseProcessor.class); + /** Shared error message for null-value REPLACE operations. */ + private static final String ERROR_VALUE_REQUIRED_FOR_REPLACE = "Value is required for REPLACE operation."; + + /** Diagnostic log parameter key for the action type. */ + private static final String DIAG_PARAM_ACTION_TYPE = "actionType"; + @Override public ActionType getSupportedActionType() { @@ -122,7 +131,8 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon for (PerformableOperation operation : operations) { operation = decryptOperationValueIfNeeded(operation, accessConfig, tenantDomain); results.add(processOperation( - operation, pathTypeAnnotations, pendingClaims, pendingCredentials, pendingProperties)); + operation, pathTypeAnnotations, pendingClaims, pendingCredentials, + pendingProperties, tenantDomain)); } } else { if (LOG.isDebugEnabled()) { @@ -160,15 +170,28 @@ public ActionExecutionStatus processSuccessResponse(FlowContext flowCon * @param pendingClaims Accumulator map for user claim updates. * @param pendingCredentials Accumulator map for user credential updates. * @param pendingProperties Accumulator map for flow property updates. + * @param tenantDomain Tenant domain, used for claim URI validation. * @return The result of the operation execution. */ private OperationExecutionResult processOperation(PerformableOperation operation, Map pathTypeAnnotations, Map pendingClaims, Map pendingCredentials, - Map pendingProperties) { + Map pendingProperties, + String tenantDomain) { + // Reject null/empty path early to prevent NPE in subsequent startsWith checks. String path = operation.getPath(); + if (path == null || path.isEmpty()) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Operation path is null or empty."); + } + + // Only REPLACE is supported; reject any other operation type. + if (operation.getOp() != Operation.REPLACE) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Unsupported operation type: " + operation.getOp() + ". Only REPLACE is supported."); + } // Check if operation is on a read-only area. if (HierarchicalPrefixMatcher.isReadOnly(path)) { @@ -180,7 +203,7 @@ private OperationExecutionResult processOperation(PerformableOperation operation if (path.startsWith(InFlowExtensionConstants.PROPERTIES_PATH_PREFIX)) { return handlePropertyOperation(operation, pathTypeAnnotations, pendingProperties); } else if (path.startsWith(InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX)) { - return handleUserClaimOperation(operation, pendingClaims); + return handleUserClaimOperation(operation, pendingClaims, tenantDomain); } else if (path.startsWith(InFlowExtensionConstants.USER_CREDENTIALS_PATH_PREFIX)) { return handleUserCredentialOperation(operation, pendingCredentials); } @@ -215,7 +238,7 @@ private OperationExecutionResult handlePropertyOperation(PerformableOperation op if (operation.getValue() == null) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Value is required for REPLACE operation."); + ERROR_VALUE_REQUIRED_FOR_REPLACE); } // Validate complex object structure against annotation schema before coercion. @@ -243,16 +266,24 @@ private OperationExecutionResult handlePropertyOperation(PerformableOperation op } /** - * Handle REPLACE operation on user claims — collect into pending claims map. + * Handle REPLACE operation on user claims — validate the claim URI then collect into pending + * claims map. Validation gates: + *
        + *
      1. Claim URI must be in the WSO2 local claim dialect ({@code http://wso2.org/claims/}).
      2. + *
      3. Identity-system claims ({@code http://wso2.org/claims/identity/}) are rejected.
      4. + *
      5. If {@link org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService} + * is available, the claim URI must correspond to a registered local claim; unknown + * claims are rejected. When the service is unavailable the check is skipped + * (fail-open).
      6. + *
      * The value is always stringified via {@code String.valueOf()}. - * Claim URI validation is intentionally omitted — validation is the responsibility - * of flow validators, not the response processor. * * @param operation The performable operation. * @param pendingClaims Accumulator map for user claim updates. + * @param tenantDomain Tenant domain for claim existence lookup. */ private OperationExecutionResult handleUserClaimOperation(PerformableOperation operation, - Map pendingClaims) { + Map pendingClaims, String tenantDomain) { String claimUri = extractNameFromPath(operation.getPath(), InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX); @@ -264,7 +295,25 @@ private OperationExecutionResult handleUserClaimOperation(PerformableOperation o if (operation.getValue() == null) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Value is required for REPLACE operation."); + ERROR_VALUE_REQUIRED_FOR_REPLACE); + } + + // Validate claim URI — must be local dialect, not an identity sub-claim. + if (!claimUri.startsWith(PathTypeAnnotationUtil.LOCAL_CLAIM_DIALECT_PREFIX)) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Claim URI must be in the local dialect (" + + PathTypeAnnotationUtil.LOCAL_CLAIM_DIALECT_PREFIX + "): " + claimUri); + } + if (claimUri.startsWith(PathTypeAnnotationUtil.IDENTITY_CLAIM_URI_PREFIX)) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + "Identity-system claims cannot be modified by In-Flow Extensions: " + claimUri); + } + + // Verify claim existence via ClaimMetadataManagementService when available. + String claimValidationFailure = validateLocalClaimExists(claimUri, tenantDomain); + if (claimValidationFailure != null) { + return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, + claimValidationFailure); } pendingClaims.put(claimUri, String.valueOf(operation.getValue())); @@ -272,6 +321,39 @@ private OperationExecutionResult handleUserClaimOperation(PerformableOperation o "User claim replace applied."); } + /** + * Validate that the given claim URI corresponds to a registered local claim in the tenant. + * Returns {@code null} if the claim is valid or if the service is unavailable (fail-open). + * Returns a descriptive failure message if validation fails. + * + * @param claimUri Local claim URI to check. + * @param tenantDomain Tenant domain for the lookup. + * @return Failure reason string, or {@code null} when validation passes or is skipped. + */ + private String validateLocalClaimExists(String claimUri, String tenantDomain) { + + org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService claimService = + InFlowExtensionDataHolder.getInstance().getClaimMetadataManagementService(); + if (claimService == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("ClaimMetadataManagementService is unavailable. Skipping claim existence check for: " + + claimUri); + } + return null; + } + try { + java.util.Optional localClaim = claimService.getLocalClaim(claimUri, tenantDomain); + if (!localClaim.isPresent()) { + return "Unknown local claim URI. Claim is not registered in the system: " + claimUri; + } + return null; + } catch (ClaimMetadataException e) { + LOG.warn("Failed to look up claim URI '" + claimUri + "' in tenant '" + tenantDomain + + "'. Skipping claim existence check.", e); + return null; + } + } + /** * Handle REPLACE operation on user credentials — collect into pending credentials map. * No key validation is applied; any credential key is accepted. The value is converted @@ -294,7 +376,7 @@ private OperationExecutionResult handleUserCredentialOperation(PerformableOperat if (operation.getValue() == null) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, - "Value is required for REPLACE operation."); + ERROR_VALUE_REQUIRED_FOR_REPLACE); } pendingCredentials.put(credentialKey, String.valueOf(operation.getValue()).toCharArray()); @@ -325,70 +407,123 @@ public ActionExecutionStatus processIncompleteResponse(FlowContext f ActionExecutionResponseContext responseContext) throws ActionExecutionResponseProcessorException { + // Contract: INCOMPLETE must carry a REDIRECT op. Every other op is intentionally + // discarded — the extension is expected to resend them on the resume call after redirect. List operations = responseContext.getActionInvocationResponse().getOperations(); + IncompleteOpScan scan = scanIncompleteOperations(operations); + + validateRedirectPresent(scan.redirectUrl); + logIgnoredIncompleteOps(scan.ignoredOpCount); + + flowContext.add(InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, scan.redirectUrl); + debugLogRedirectHost(scan.redirectUrl); + logIncompleteSuccess(); + + return new IncompleteStatus.Builder() + .responseContext(Collections.emptyMap()) + .build(); + } - // Contract: INCOMPLETE must carry a REDIRECT op. If present, every other op is - // intentionally discarded — the extension is expected to resend (possibly with - // different values) on the resume call after the user returns from the redirect. - // REDIRECT operations carry their target in the dedicated `url` field - // (PerformableOperation rejects path/value for REDIRECT and rejects url for everything else). + /** + * Scan the operation list from an INCOMPLETE response to extract the redirect URL and count + * non-REDIRECT operations that will be ignored. + * + * @param operations List of operations from the INCOMPLETE response (may be null). + * @return Scan result holding the redirect URL and ignored-op count. + */ + private IncompleteOpScan scanIncompleteOperations(List operations) { + + if (operations == null) { + return new IncompleteOpScan(null, 0); + } String redirectUrl = null; int ignoredOpCount = 0; - if (operations != null) { - for (PerformableOperation op : operations) { - if (op.getOp() == Operation.REDIRECT) { - redirectUrl = op.getUrl(); - } else { - ignoredOpCount++; - } + for (PerformableOperation op : operations) { + if (op.getOp() == Operation.REDIRECT) { + redirectUrl = op.getUrl(); + } else { + ignoredOpCount++; } } + return new IncompleteOpScan(redirectUrl, ignoredOpCount); + } - if (redirectUrl == null || redirectUrl.isEmpty()) { - LOG.warn("In-Flow Extension INCOMPLETE response is missing a REDIRECT operation."); - if (LoggerUtils.isDiagnosticLogsEnabled()) { - LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( - InFlowExtensionLogConstants.COMPONENT_ID, - InFlowExtensionLogConstants.ActionIDs.PROCESS_RESPONSE) - .resultMessage("INCOMPLETE response from In-Flow Extension is missing a REDIRECT operation.") - .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) - .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) - .resultStatus(DiagnosticLog.ResultStatus.FAILED)); - } - throw new ActionExecutionResponseProcessorException( - "INCOMPLETE response from In-Flow Extension must contain a REDIRECT operation."); + /** + * Assert that a redirect URL was found in the INCOMPLETE response; throw and emit diagnostics + * if it is absent. + */ + private void validateRedirectPresent(String redirectUrl) + throws ActionExecutionResponseProcessorException { + + if (redirectUrl != null && !redirectUrl.isEmpty()) { + return; } + LOG.warn("In-Flow Extension INCOMPLETE response is missing a REDIRECT operation."); + if (LoggerUtils.isDiagnosticLogsEnabled()) { + LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( + InFlowExtensionConstants.Log.COMPONENT_ID, + InFlowExtensionConstants.Log.ActionIDs.PROCESS_RESPONSE) + .resultMessage( + "INCOMPLETE response from In-Flow Extension is missing a REDIRECT operation.") + .configParam(DIAG_PARAM_ACTION_TYPE, ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED)); + } + throw new ActionExecutionResponseProcessorException( + "INCOMPLETE response from In-Flow Extension must contain a REDIRECT operation."); + } + + /** Log the number of non-REDIRECT ops discarded in an INCOMPLETE response, if any. */ + private void logIgnoredIncompleteOps(int ignoredOpCount) { if (ignoredOpCount > 0 && LOG.isDebugEnabled()) { LOG.debug("Ignored " + ignoredOpCount + " non-REDIRECT operation(s) on INCOMPLETE response. " - + "REPLACE ops on the redirect call are by-contract dropped — the extension is " - + "expected to resend them on the resume call after callback."); + + "REPLACE ops are by-contract dropped — the extension is expected to resend " + + "them on the resume call after callback."); } + } - flowContext.add(InFlowExtensionConstants.PENDING_REDIRECT_URL_KEY, redirectUrl); + /** Log the redirect URL host at DEBUG level after parsing the URI. */ + private void debugLogRedirectHost(String redirectUrl) { - if (LOG.isDebugEnabled()) { - try { - String host = new java.net.URI(redirectUrl).getHost(); - LOG.debug("In-Flow Extension INCOMPLETE: redirect URL host resolved: " + host); - } catch (java.net.URISyntaxException ignored) { - LOG.debug("In-Flow Extension INCOMPLETE: redirect URL stored in flow context."); - } + if (!LOG.isDebugEnabled()) { + return; + } + try { + String host = new java.net.URI(redirectUrl).getHost(); + LOG.debug("In-Flow Extension INCOMPLETE: redirect URL host resolved: " + host); + } catch (java.net.URISyntaxException ignored) { + LOG.debug("In-Flow Extension INCOMPLETE: redirect URL stored in flow context."); } + } + + /** Emit a diagnostic success event after a valid INCOMPLETE response is processed. */ + private void logIncompleteSuccess() { + if (LoggerUtils.isDiagnosticLogsEnabled()) { LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( - InFlowExtensionLogConstants.COMPONENT_ID, - InFlowExtensionLogConstants.ActionIDs.PROCESS_RESPONSE) - .resultMessage("In-Flow Extension INCOMPLETE response processed. Redirect URL stored in flow context.") - .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) + InFlowExtensionConstants.Log.COMPONENT_ID, + InFlowExtensionConstants.Log.ActionIDs.PROCESS_RESPONSE) + .resultMessage( + "In-Flow Extension INCOMPLETE response processed. Redirect URL stored in flow context.") + .configParam(DIAG_PARAM_ACTION_TYPE, ActionType.IN_FLOW_EXTENSION.getDisplayName()) .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) .resultStatus(DiagnosticLog.ResultStatus.SUCCESS)); } + } - return new IncompleteStatus.Builder() - .responseContext(Collections.emptyMap()) - .build(); + /** Lightweight value holder for the result of scanning INCOMPLETE response operations. */ + private static final class IncompleteOpScan { + + private final String redirectUrl; + private final int ignoredOpCount; + + private IncompleteOpScan(String redirectUrl, int ignoredOpCount) { + + this.redirectUrl = redirectUrl; + this.ignoredOpCount = ignoredOpCount; + } } @Override @@ -504,24 +639,25 @@ private PerformableOperation decryptOperationValueIfNeeded(PerformableOperation return operation; } - // Only decrypt string values that look like JWE compact serialization. + // For encrypted modify paths the value MUST be a JWE compact serialization. + // Accepting plaintext would make the encryption flag advisory; it is enforced here. Object value = operation.getValue(); if (!(value instanceof String)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Value for encrypted path " + operation.getPath() + - " is not a String. Using as-is."); - } - return operation; + emitEncryptionContractViolation(operation.getPath(), + "Value for encrypted modify path is not a String."); + throw new ActionExecutionResponseProcessorException( + "Value for encrypted modify path '" + operation.getPath() + + "' must be a JWE-encrypted string, but received a non-String value."); } String stringValue = (String) value; if (!JWEEncryptionUtil.isJWEEncrypted(stringValue)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Value for encrypted path " + operation.getPath() + - " is not JWE-encrypted. Using as-is."); - } - return operation; + emitEncryptionContractViolation(operation.getPath(), + "Value for encrypted modify path is not JWE-encrypted."); + throw new ActionExecutionResponseProcessorException( + "Value for encrypted modify path '" + operation.getPath() + + "' must be JWE-encrypted, but received a plaintext value."); } try { @@ -539,8 +675,8 @@ private PerformableOperation decryptOperationValueIfNeeded(PerformableOperation + "', tenant: " + tenantDomain, e); if (LoggerUtils.isDiagnosticLogsEnabled()) { LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( - InFlowExtensionLogConstants.COMPONENT_ID, - InFlowExtensionLogConstants.ActionIDs.PROCESS_RESPONSE) + InFlowExtensionConstants.Log.COMPONENT_ID, + InFlowExtensionConstants.Log.ActionIDs.PROCESS_RESPONSE) .resultMessage("Failed to decrypt inbound JWE value for modify path.") .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) .inputParam("path", operation.getPath()) @@ -552,4 +688,24 @@ private PerformableOperation decryptOperationValueIfNeeded(PerformableOperation } } + // ========================= Decryption diagnostics helpers ========================= + + /** + * Emit a diagnostic failure event for a broken encryption contract on a modify path. + * Extracted to keep {@link #decryptOperationValueIfNeeded} readable. + */ + private void emitEncryptionContractViolation(String path, String reason) { + + if (LoggerUtils.isDiagnosticLogsEnabled()) { + LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( + InFlowExtensionConstants.Log.COMPONENT_ID, + InFlowExtensionConstants.Log.ActionIDs.PROCESS_RESPONSE) + .resultMessage(reason) + .configParam(DIAG_PARAM_ACTION_TYPE, ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .inputParam("path", path) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED)); + } + } + } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtil.java index 06b51247c229..012566394ed8 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtil.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtil.java @@ -113,6 +113,10 @@ public static String[] stripAnnotation(String rawPath) { public static Object coerceValue(String path, Object value, Map pathTypeAnnotations) { + if (value == null) { + return null; + } + String annotation = pathTypeAnnotations.get(path); if (annotation == null) { @@ -135,7 +139,7 @@ public static Object coerceValue(String path, Object value, List rawList = (List) resolvedList; List stringList = new ArrayList<>(); for (Object item : rawList) { - stringList.add(String.valueOf(item)); + stringList.add(item == null ? null : String.valueOf(item)); } return stringList; } @@ -342,51 +346,73 @@ private static Map parseAnnotationAttributes(String inner) { @SuppressWarnings("unchecked") private static boolean validateSingleComplexObject(Object value, Map schema) { - if (!(value instanceof Map)) { + if (!isMapAndWithinAttributeLimit(value)) { return false; } Map map = (Map) value; - // Attribute count limit. - if (map.size() > MAX_ATTRIBUTES_PER_OBJECT) { + if (!containsOnlySchemaKeys(map, schema)) { return false; } - // All keys in the value must be defined in the schema. - for (String key : map.keySet()) { - if (!schema.containsKey(key)) { + for (Map.Entry entry : map.entrySet()) { + if (!validateEntryValueAgainstType(entry, schema)) { return false; } } - // Validate single nesting level: each attribute value must be primary or primary array. - for (Map.Entry entry : map.entrySet()) { - String type = schema.get(entry.getKey()); - Object attrValue = entry.getValue(); + return true; + } - if (type != null && type.endsWith("[]")) { - // Array attribute: validate it's a list of primitives within item limit. - if (!(attrValue instanceof List)) { - return false; - } - List list = (List) attrValue; - if (list.size() > MAX_ARRAY_ITEMS) { - return false; - } - for (Object item : list) { - if (item instanceof Map || item instanceof List) { - return false; - } - } - } else { - // Primary attribute: must not be a nested structure. - if (attrValue instanceof Map || attrValue instanceof List) { - return false; - } + private static boolean isMapAndWithinAttributeLimit(Object value) { + + return value instanceof Map && ((Map) value).size() <= MAX_ATTRIBUTES_PER_OBJECT; + } + + private static boolean containsOnlySchemaKeys(Map valueMap, Map schema) { + + for (String key : valueMap.keySet()) { + if (!schema.containsKey(key)) { + return false; } } + return true; + } + + private static boolean validateEntryValueAgainstType(Map.Entry entry, Map schema) { + + String type = schema.get(entry.getKey()); + Object attrValue = entry.getValue(); + + if (type != null && type.endsWith("[]")) { + return validatePrimaryArrayAttribute(attrValue); + } + return !isNestedStructure(attrValue); + } + @SuppressWarnings("unchecked") + private static boolean validatePrimaryArrayAttribute(Object value) { + + if (!(value instanceof List)) { + return false; + } + + List list = (List) value; + if (list.size() > MAX_ARRAY_ITEMS) { + return false; + } + + for (Object item : list) { + if (isNestedStructure(item)) { + return false; + } + } return true; } + + private static boolean isNestedStructure(Object value) { + + return value instanceof Map || value instanceof List; + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionDataHolder.java index e272e2dbe831..9c23f6ce71c8 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionDataHolder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionDataHolder.java @@ -21,17 +21,22 @@ import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; import org.wso2.carbon.identity.certificate.management.service.CertificateManagementService; +import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; /** * Data holder for the In-Flow Extension bundle. + * + *

      This singleton is used by DS bind/unbind methods to expose dynamic OSGi references + * to classes outside the component lifecycle methods.

      */ -public class InFlowExtensionDataHolder { +public final class InFlowExtensionDataHolder { private static final InFlowExtensionDataHolder instance = new InFlowExtensionDataHolder(); private ActionExecutorService actionExecutorService; private ActionManagementService actionManagementService; private CertificateManagementService certificateManagementService; + private ClaimMetadataManagementService claimMetadataManagementService; private InFlowExtensionDataHolder() { @@ -72,4 +77,14 @@ public void setCertificateManagementService(CertificateManagementService certifi this.certificateManagementService = certificateManagementService; } + public ClaimMetadataManagementService getClaimMetadataManagementService() { + + return claimMetadataManagementService; + } + + public void setClaimMetadataManagementService(ClaimMetadataManagementService claimMetadataManagementService) { + + this.claimMetadataManagementService = claimMetadataManagementService; + } + } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionServiceComponent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionServiceComponent.java index e1e8258ca769..65cefb682f07 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionServiceComponent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionServiceComponent.java @@ -35,6 +35,7 @@ import org.wso2.carbon.identity.action.management.api.service.ActionDTOModelResolver; import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; import org.wso2.carbon.identity.certificate.management.service.CertificateManagementService; +import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; import org.wso2.carbon.identity.flow.inflow.extensions.executor.InFlowExtensionExecutor; import org.wso2.carbon.identity.flow.inflow.extensions.executor.InFlowExtensionRequestBuilder; @@ -98,7 +99,10 @@ protected void setActionManagementService(ActionManagementService actionManageme protected void unsetActionManagementService(ActionManagementService actionManagementService) { - LOG.debug("Unsetting the ActionManagementService in the In-Flow Extension component."); + if (LOG.isDebugEnabled()) { + LOG.debug("Unsetting the ActionManagementService in the In-Flow Extension component. Service: " + + actionManagementService); + } InFlowExtensionDataHolder.getInstance().setActionManagementService(null); } @@ -117,7 +121,10 @@ protected void setActionExecutorService(ActionExecutorService actionExecutorServ protected void unsetActionExecutorService(ActionExecutorService actionExecutorService) { - LOG.debug("Unsetting the ActionExecutorService in the In-Flow Extension component."); + if (LOG.isDebugEnabled()) { + LOG.debug("Unsetting the ActionExecutorService in the In-Flow Extension component. Service: " + + actionExecutorService); + } InFlowExtensionDataHolder.getInstance().setActionExecutorService(null); } @@ -138,7 +145,35 @@ protected void setCertificateManagementService(CertificateManagementService cert protected void unsetCertificateManagementService( CertificateManagementService certificateManagementService) { - LOG.debug("Unsetting the CertificateManagementService in the In-Flow Extension component."); + if (LOG.isDebugEnabled()) { + LOG.debug("Unsetting the CertificateManagementService in the In-Flow Extension component. Service: " + + certificateManagementService); + } InFlowExtensionDataHolder.getInstance().setCertificateManagementService(null); } + + @Reference( + name = "ClaimMetadataManagementService", + service = ClaimMetadataManagementService.class, + cardinality = ReferenceCardinality.OPTIONAL, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetClaimMetadataManagementService" + ) + protected void setClaimMetadataManagementService( + ClaimMetadataManagementService claimMetadataManagementService) { + + LOG.debug("Setting the ClaimMetadataManagementService in the In-Flow Extension component."); + InFlowExtensionDataHolder.getInstance() + .setClaimMetadataManagementService(claimMetadataManagementService); + } + + protected void unsetClaimMetadataManagementService( + ClaimMetadataManagementService claimMetadataManagementService) { + + if (LOG.isDebugEnabled()) { + LOG.debug("Unsetting the ClaimMetadataManagementService in the In-Flow Extension component. Service: " + + claimMetadataManagementService); + } + InFlowExtensionDataHolder.getInstance().setClaimMetadataManagementService(null); + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConstants.java deleted file mode 100644 index feebefa0e01b..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConstants.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2025, 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.inflow.extensions.management; - -/** - * Constants for In-Flow Extension action management. - */ -public class InFlowExtensionActionConstants { - - /** - * Property key for the expose path prefixes stored in IDN_ACTION_PROPERTIES. - * Stored as a BLOB (JSON-serialized list of strings). - */ - public static final String ACCESS_CONFIG_EXPOSE = "ACCESS_CONFIG_EXPOSE"; - - /** - * Property key for the allowed operations stored in IDN_ACTION_PROPERTIES. - * Stored as a BLOB (JSON-serialized list of operation descriptors). - */ - public static final String ACCESS_CONFIG_MODIFY = "ACCESS_CONFIG_MODIFY"; - - /** - * Separator used between property key and flow type in override keys. - * For example: "ACCESS_CONFIG_EXPOSE:REGISTRATION" - */ - public static final String OVERRIDE_KEY_SEPARATOR = ":"; - - /** - * Prefix for flow-type-specific expose override properties. - * Full key format: "ACCESS_CONFIG_EXPOSE:<FLOW_TYPE>" - */ - public static final String ACCESS_CONFIG_EXPOSE_PREFIX = ACCESS_CONFIG_EXPOSE + OVERRIDE_KEY_SEPARATOR; - - /** - * Prefix for flow-type-specific allowed operations override properties. - * Full key format: "ACCESS_CONFIG_ALLOWED_OPERATIONS:<FLOW_TYPE>" - */ - public static final String ACCESS_CONFIG_MODIFY_PREFIX = - ACCESS_CONFIG_MODIFY + OVERRIDE_KEY_SEPARATOR; - - /** - * Maximum number of expose path prefixes allowed per action. - */ - public static final int MAX_EXPOSE_PATHS = 50; - - /** - * Property key for the action's icon URL stored in IDN_ACTION_PROPERTIES. - * Stored as a PRIMITIVE string. - */ - public static final String ICON_URL = "ICON_URL"; - - /** - * Property key for the external service's certificate stored in IDN_ACTION_PROPERTIES. - * During persistence the certificate object is replaced with its UUID (PRIMITIVE string); - * during retrieval the UUID is resolved back to the full Certificate object. - */ - public static final String CERTIFICATE = "CERTIFICATE"; - - /** - * Naming prefix for certificates stored via CertificateManagementService. - * Full name format: {@code "ACTIONS:"}. - */ - public static final String CERTIFICATE_NAME_PREFIX = "ACTIONS:"; - - private InFlowExtensionActionConstants() { - } -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConverter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConverter.java index 7e1f0f33e589..dce336862abe 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConverter.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConverter.java @@ -32,12 +32,12 @@ import java.util.List; import java.util.Map; -import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY; -import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY_PREFIX; -import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.CERTIFICATE; -import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ICON_URL; -import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE; -import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE_PREFIX; +import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_EXPOSE; +import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_EXPOSE_PREFIX; +import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_MODIFY; +import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_MODIFY_PREFIX; +import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.CERTIFICATE; +import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ICON_URL; /** * ActionConverter implementation for In-Flow Extension actions. @@ -66,63 +66,72 @@ public Action.ActionTypes getSupportedActionType() { @Override public ActionDTO buildActionDTO(Action action) { - if (!(action instanceof InFlowExtensionAction)) { + if (!(action instanceof InFlowExtensionAction inFlowExtensionAction)) { return new ActionDTO.Builder(action).build(); } - InFlowExtensionAction inFlowExtensionAction = (InFlowExtensionAction) action; - AccessConfig accessConfig = inFlowExtensionAction.getAccessConfig(); - Map properties = new HashMap<>(); - // Default access config (no prefix). - if (accessConfig != null) { - if (accessConfig.getExpose() != null) { - properties.put(ACCESS_CONFIG_EXPOSE, - new ActionProperty.BuilderForService(accessConfig.getExpose()).build()); - } - if (accessConfig.getModify() != null) { - properties.put(ACCESS_CONFIG_MODIFY, - new ActionProperty.BuilderForService(accessConfig.getModify()).build()); - } + putDefaultAccessConfigProperties(properties, inFlowExtensionAction.getAccessConfig()); + putEncryptionProperty(properties, inFlowExtensionAction.getEncryption()); + if (inFlowExtensionAction.getIconUrl() != null) { + properties.put(ICON_URL, + new ActionProperty.BuilderForService(inFlowExtensionAction.getIconUrl()).build()); } + putFlowTypeOverrideProperties(properties, inFlowExtensionAction.getFlowTypeOverrides()); - // Encryption certificate (separate from access config). - Encryption encryption = inFlowExtensionAction.getEncryption(); - if (encryption != null && encryption.getCertificate() != null) { + return new ActionDTO.Builder(inFlowExtensionAction) + .properties(properties) + .build(); + } + + private void putDefaultAccessConfigProperties(Map properties, AccessConfig accessConfig) { + + if (accessConfig == null) { + return; + } + if (accessConfig.getExpose() != null) { + properties.put(ACCESS_CONFIG_EXPOSE, + new ActionProperty.BuilderForService(accessConfig.getExpose()).build()); + } + if (accessConfig.getModify() != null) { + properties.put(ACCESS_CONFIG_MODIFY, + new ActionProperty.BuilderForService(accessConfig.getModify()).build()); + } + } + + private void putEncryptionProperty(Map properties, Encryption encryption) { + + if (encryption == null) { + return; + } + if (encryption.getCertificate() != null) { properties.put(CERTIFICATE, new ActionProperty.BuilderForService(encryption.getCertificate()).build()); - } else if (encryption != null) { + } else { // Encryption object present but no certificate — signals explicit removal. properties.put(CERTIFICATE, new ActionProperty.BuilderForService("").build()); } + } - // Icon URL. - if (inFlowExtensionAction.getIconUrl() != null) { - properties.put(ICON_URL, - new ActionProperty.BuilderForService(inFlowExtensionAction.getIconUrl()).build()); - } + private void putFlowTypeOverrideProperties(Map properties, + Map overrides) { - // Per-flow-type overrides using prefixed keys. - Map overrides = inFlowExtensionAction.getFlowTypeOverrides(); - if (overrides != null) { - for (Map.Entry entry : overrides.entrySet()) { - String flowType = entry.getKey(); - AccessConfig overrideConfig = entry.getValue(); - if (overrideConfig.getExpose() != null) { - properties.put(ACCESS_CONFIG_EXPOSE_PREFIX + flowType, - new ActionProperty.BuilderForService(overrideConfig.getExpose()).build()); - } - if (overrideConfig.getModify() != null) { - properties.put(ACCESS_CONFIG_MODIFY_PREFIX + flowType, - new ActionProperty.BuilderForService(overrideConfig.getModify()).build()); - } + if (overrides == null) { + return; + } + for (Map.Entry entry : overrides.entrySet()) { + String flowType = entry.getKey(); + AccessConfig overrideConfig = entry.getValue(); + if (overrideConfig.getExpose() != null) { + properties.put(ACCESS_CONFIG_EXPOSE_PREFIX + flowType, + new ActionProperty.BuilderForService(overrideConfig.getExpose()).build()); + } + if (overrideConfig.getModify() != null) { + properties.put(ACCESS_CONFIG_MODIFY_PREFIX + flowType, + new ActionProperty.BuilderForService(overrideConfig.getModify()).build()); } } - - return new ActionDTO.Builder(inFlowExtensionAction) - .properties(properties) - .build(); } /** @@ -149,15 +158,15 @@ public Action buildAction(ActionDTO actionDTO) { // Encryption certificate (separate from access config). Encryption encryption = null; Object certValue = actionDTO.getPropertyValue(CERTIFICATE); - if (certValue instanceof Certificate) { - encryption = new Encryption((Certificate) certValue); + if (certValue instanceof Certificate certificate) { + encryption = new Encryption(certificate); } // Icon URL. String iconUrl = null; Object iconUrlValue = actionDTO.getPropertyValue(ICON_URL); - if (iconUrlValue instanceof String) { - iconUrl = (String) iconUrlValue; + if (iconUrlValue instanceof String iconUrlStr) { + iconUrl = iconUrlStr; } // Reconstruct per-flow-type overrides from prefixed keys. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionDTOModelResolver.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionDTOModelResolver.java index 3f26fe5ee804..67c90dc5858f 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionDTOModelResolver.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionDTOModelResolver.java @@ -44,14 +44,14 @@ import java.util.Set; import static java.util.Collections.emptyList; -import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY; -import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_MODIFY_PREFIX; -import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.CERTIFICATE; -import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ICON_URL; -import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE; -import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.ACCESS_CONFIG_EXPOSE_PREFIX; -import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.CERTIFICATE_NAME_PREFIX; -import static org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConstants.MAX_EXPOSE_PATHS; +import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_EXPOSE; +import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_EXPOSE_PREFIX; +import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_MODIFY; +import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_MODIFY_PREFIX; +import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.CERTIFICATE; +import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.CERTIFICATE_NAME_PREFIX; +import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ICON_URL; +import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.MAX_EXPOSE_PATHS; /** * ActionDTOModelResolver implementation for In-Flow Extension actions. @@ -113,35 +113,40 @@ public ActionDTO resolveForAddOperation(ActionDTO actionDTO, String tenantDomain // Handle icon URL: pass through as a PRIMITIVE string. Object iconUrlValue = actionDTO.getPropertyValue(ICON_URL); - if (iconUrlValue instanceof String && !((String) iconUrlValue).isEmpty()) { - properties.put(ICON_URL, new ActionProperty.BuilderForDAO((String) iconUrlValue).build()); + if (iconUrlValue instanceof String iconUrlStr && !iconUrlStr.isEmpty()) { + properties.put(ICON_URL, new ActionProperty.BuilderForDAO(iconUrlStr).build()); } // Handle per-flow-type override properties (prefixed keys). - if (actionDTO.getProperties() != null) { - for (Map.Entry entry : actionDTO.getProperties().entrySet()) { - String key = entry.getKey(); - if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX)) { - Object overrideExpose = actionDTO.getPropertyValue(key); - if (overrideExpose != null) { - List validatedOverrideExpose = validateExpose(overrideExpose); - properties.put(key, createBlobProperty(validatedOverrideExpose)); - } - } else if (key.startsWith(ACCESS_CONFIG_MODIFY_PREFIX)) { - Object overrideModify = actionDTO.getPropertyValue(key); - if (overrideModify != null) { - List validatedOverrideModify = validateExpose(overrideModify); - properties.put(key, createBlobProperty(validatedOverrideModify)); - } - } - } - } + resolveAddOverrideProperties(actionDTO, properties); return new ActionDTO.Builder(actionDTO) .properties(properties) .build(); } + private void resolveAddOverrideProperties(ActionDTO actionDTO, Map properties) + throws ActionDTOModelResolverException { + + if (actionDTO.getProperties() == null) { + return; + } + for (Map.Entry entry : actionDTO.getProperties().entrySet()) { + String key = entry.getKey(); + if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX)) { + Object overrideExpose = actionDTO.getPropertyValue(key); + if (overrideExpose != null) { + properties.put(key, createBlobProperty(validateExpose(overrideExpose))); + } + } else if (key.startsWith(ACCESS_CONFIG_MODIFY_PREFIX)) { + Object overrideModify = actionDTO.getPropertyValue(key); + if (overrideModify != null) { + properties.put(key, createBlobProperty(validateExpose(overrideModify))); + } + } + } + } + @Override public ActionDTO resolveForGetOperation(ActionDTO actionDTO, String tenantDomain) throws ActionDTOModelResolverException { @@ -172,11 +177,7 @@ public ActionDTO resolveForGetOperation(ActionDTO actionDTO, String tenantDomain if (actionDTO.getProperties() != null) { for (Map.Entry entry : actionDTO.getProperties().entrySet()) { String key = entry.getKey(); - if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX) - && actionDTO.getPropertyValue(key) != null) { - properties.put(key, deserializeExposeProperty( - ((BinaryObject) actionDTO.getPropertyValue(key)).getJSONString())); - } else if (key.startsWith(ACCESS_CONFIG_MODIFY_PREFIX) + if ((key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX) || key.startsWith(ACCESS_CONFIG_MODIFY_PREFIX)) && actionDTO.getPropertyValue(key) != null) { properties.put(key, deserializeExposeProperty( ((BinaryObject) actionDTO.getPropertyValue(key)).getJSONString())); @@ -233,51 +234,63 @@ public ActionDTO resolveForUpdateOperation(ActionDTO updatingActionDTO, ActionDT // Handle certificate update. handleCertificateUpdate(updatingActionDTO, existingActionDTO, properties, tenantDomain); + resolveUpdateIconUrl(updatingActionDTO, existingActionDTO, properties); + carryForwardExistingOverrides(existingActionDTO, properties); + overlayUpdatedOverrides(updatingActionDTO, properties); + + return new ActionDTO.Builder(updatingActionDTO) + .properties(properties) + .build(); + } + + private void resolveUpdateIconUrl(ActionDTO updatingActionDTO, ActionDTO existingActionDTO, + Map properties) + throws ActionDTOModelResolverException { - // Handle icon URL update (PUT semantics: carry forward existing if not provided). Object updatingIconUrl = updatingActionDTO.getPropertyValue(ICON_URL); - if (updatingIconUrl instanceof String && !((String) updatingIconUrl).isEmpty()) { - properties.put(ICON_URL, new ActionProperty.BuilderForDAO((String) updatingIconUrl).build()); + if (updatingIconUrl instanceof String updatingIconUrlStr && !updatingIconUrlStr.isEmpty()) { + properties.put(ICON_URL, new ActionProperty.BuilderForDAO(updatingIconUrlStr).build()); } else if (existingActionDTO.getPropertyValue(ICON_URL) != null) { properties.put(ICON_URL, new ActionProperty.BuilderForDAO( existingActionDTO.getPropertyValue(ICON_URL).toString()).build()); } + } - // Handle per-flow-type override properties. Since DAO treats update as PUT (full replace), - // we must carry forward all existing overrides that are not explicitly being updated. - // First, carry forward all existing per-flow-type overrides. - if (existingActionDTO.getProperties() != null) { - for (Map.Entry entry : existingActionDTO.getProperties().entrySet()) { - String key = entry.getKey(); - if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX) - || key.startsWith(ACCESS_CONFIG_MODIFY_PREFIX)) { - properties.put(key, createBlobProperty(existingActionDTO.getPropertyValue(key))); - } + private void carryForwardExistingOverrides(ActionDTO existingActionDTO, + Map properties) + throws ActionDTOModelResolverException { + + if (existingActionDTO.getProperties() == null) { + return; + } + for (Map.Entry entry : existingActionDTO.getProperties().entrySet()) { + String key = entry.getKey(); + if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX) || key.startsWith(ACCESS_CONFIG_MODIFY_PREFIX)) { + properties.put(key, createBlobProperty(existingActionDTO.getPropertyValue(key))); } } - // Then, overlay with any explicitly updated per-flow-type overrides. - if (updatingActionDTO.getProperties() != null) { - for (Map.Entry entry : updatingActionDTO.getProperties().entrySet()) { - String key = entry.getKey(); - if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX)) { - Object overrideExpose = updatingActionDTO.getPropertyValue(key); - if (overrideExpose != null) { - List validatedOverrideExpose = validateExpose(overrideExpose); - properties.put(key, createBlobProperty(validatedOverrideExpose)); - } - } else if (key.startsWith(ACCESS_CONFIG_MODIFY_PREFIX)) { - Object overrideModify = updatingActionDTO.getPropertyValue(key); - if (overrideModify != null) { - List validatedOverrideModify = validateExpose(overrideModify); - properties.put(key, createBlobProperty(validatedOverrideModify)); - } + } + + private void overlayUpdatedOverrides(ActionDTO updatingActionDTO, Map properties) + throws ActionDTOModelResolverException { + + if (updatingActionDTO.getProperties() == null) { + return; + } + for (Map.Entry entry : updatingActionDTO.getProperties().entrySet()) { + String key = entry.getKey(); + if (key.startsWith(ACCESS_CONFIG_EXPOSE_PREFIX)) { + Object overrideExpose = updatingActionDTO.getPropertyValue(key); + if (overrideExpose != null) { + properties.put(key, createBlobProperty(validateExpose(overrideExpose))); + } + } else if (key.startsWith(ACCESS_CONFIG_MODIFY_PREFIX)) { + Object overrideModify = updatingActionDTO.getPropertyValue(key); + if (overrideModify != null) { + properties.put(key, createBlobProperty(validateExpose(overrideModify))); } } } - - return new ActionDTO.Builder(updatingActionDTO) - .properties(properties) - .build(); } @Override @@ -338,8 +351,8 @@ private List validateExpose(Object exposeValue) throws ActionDTOMod String path = (String) map.get("path"); boolean encrypted = map.containsKey("encrypted") && toBooleanSafe(map.get("encrypted")); result.add(new ContextPath(path, encrypted)); - } else if (item instanceof ContextPath) { - result.add((ContextPath) item); + } else if (item instanceof ContextPath contextPath) { + result.add(contextPath); } else { throw new ActionDTOModelResolverClientException("Invalid expose format.", "Each expose entry must be an object with 'path' and optional 'encrypted' fields."); @@ -453,7 +466,7 @@ private void handleCertificateUpdate(ActionDTO updatingActionDTO, ActionDTO exis Object existingCertValue = existingActionDTO.getPropertyValue(CERTIFICATE); // Empty string signals explicit certificate removal. - boolean isExplicitRemoval = newCertValue instanceof String && ((String) newCertValue).isEmpty(); + boolean isExplicitRemoval = newCertValue instanceof String s && s.isEmpty(); if (isExplicitRemoval && existingCertValue != null) { // Explicitly clearing the certificate — delete the existing one. @@ -520,8 +533,8 @@ private void handleCertificateDelete(ActionDTO deletingActionDTO, String tenantD */ private String extractCertificateId(Object certValue) { - if (certValue instanceof Certificate) { - return ((Certificate) certValue).getId(); + if (certValue instanceof Certificate certificate) { + return certificate.getId(); } return certValue.toString(); } @@ -532,18 +545,18 @@ private String extractCertificateId(Object certValue) { */ private String extractCertificatePEM(Object certValue) throws ActionDTOModelResolverClientException { - if (certValue instanceof Certificate) { - return ((Certificate) certValue).getCertificateContent(); + if (certValue instanceof Certificate certificate) { + return certificate.getCertificateContent(); } else if (certValue instanceof Map) { Map certMap = (Map) certValue; Object content = certMap.get("certificateContent"); - if (content instanceof String) { - return (String) content; + if (content instanceof String pem) { + return pem; } throw new ActionDTOModelResolverClientException("Invalid certificate format.", "Certificate object must contain a 'certificateContent' field."); - } else if (certValue instanceof String) { - return (String) certValue; + } else if (certValue instanceof String pem) { + return pem; } throw new ActionDTOModelResolverClientException("Invalid certificate format.", "Certificate must be a PEM string, a Certificate object, or a map with 'certificateContent'."); @@ -580,11 +593,11 @@ private ActionProperty deserializeExposeProperty(String jsonString) throws Actio */ private static boolean toBooleanSafe(Object value) { - if (value instanceof Boolean) { - return (Boolean) value; + if (value instanceof Boolean b) { + return b; } - if (value instanceof String) { - return Boolean.parseBoolean((String) value); + if (value instanceof String s) { + return Boolean.parseBoolean(s); } return false; } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java index 76a7b01a2a7a..1ff545a58277 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java @@ -48,6 +48,7 @@ public class InFlowExtensionContextTreeBuilder { private static final String OP_EXPOSE = "EXPOSE"; private static final String OP_MODIFY = "MODIFY"; + private static final String DATA_TYPE_STRING = "String"; // Flow context attributes that appear under the "flow" branch of the tree. private static final List FLOW_BRANCH_ATTRS = @@ -75,18 +76,15 @@ public InFlowExtensionContextTreeMetadata build(String flowType) { List tree = new ArrayList<>(); - InFlowExtensionContextTreeNode userNode = buildUserNode(attrs, userAttrs); - if (userNode != null) { - tree.add(userNode); - } + // User and properties nodes are always non-null (always construct and return a node). + tree.add(buildUserNode(userAttrs)); + tree.add(buildPropertiesNode(attrs)); + + // Flow node is conditionally non-null (returns null if no flow attributes are configured). InFlowExtensionContextTreeNode flowNode = buildFlowNode(attrs); if (flowNode != null) { tree.add(flowNode); } - InFlowExtensionContextTreeNode propsNode = buildPropertiesNode(attrs); - if (propsNode != null) { - tree.add(propsNode); - } return new InFlowExtensionContextTreeMetadata( flowType, @@ -110,8 +108,7 @@ static boolean resolveAllowReadOnlyClaimsModification(String flowType) { return true; } switch (flowType) { - case FLOW_REGISTRATION: - case FLOW_INVITED_USER_REGISTRATION: + case FLOW_REGISTRATION, FLOW_INVITED_USER_REGISTRATION: return true; case FLOW_PASSWORD_RECOVERY: default: @@ -134,7 +131,7 @@ static boolean resolveAllowReadOnlyClaimsModification(String flowType) { * *

      {@code userAttrs == null} signals full-passthrough (show and expose everything). */ - private InFlowExtensionContextTreeNode buildUserNode(Set attrs, Set userAttrs) { + private InFlowExtensionContextTreeNode buildUserNode(Set userAttrs) { // userAttrs == null → full passthrough set by the caller (build()). boolean fullPassthrough = (userAttrs == null); @@ -147,7 +144,7 @@ private InFlowExtensionContextTreeNode buildUserNode(Set attrs, Set attrs, Set attrs, Set getExpose() { * Returns the flat list of expose path strings (without encryption metadata). * Convenience method for components that only need path prefixes. * - * @return List of path strings, or {@code null} if expose is not configured. + * @return List of path strings, or an empty list if expose is not configured. */ public List getExposePaths() { if (expose == null) { - return null; + return Collections.emptyList(); } - return expose.stream().map(ContextPath::getPath).collect(Collectors.toList()); + return expose.stream().map(ContextPath::getPath).toList(); } /** @@ -102,14 +101,14 @@ public List getModify() { * Returns the flat list of modify path strings (without encryption metadata). * Convenience method for components that only need path strings. * - * @return List of path strings, or {@code null} if modify is not configured. + * @return List of path strings, or an empty list if modify is not configured. */ public List getModifyPaths() { if (modify == null) { - return null; + return Collections.emptyList(); } - return modify.stream().map(ContextPath::getPath).collect(Collectors.toList()); + return modify.stream().map(ContextPath::getPath).toList(); } /** diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilderTest.java index a6a083383a12..908d48744c45 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilderTest.java @@ -30,6 +30,7 @@ import org.wso2.carbon.identity.action.execution.api.model.AllowedOperation; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; import org.wso2.carbon.identity.action.execution.api.model.Operation; +import org.wso2.carbon.identity.action.management.api.model.Action; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.flow.inflow.extensions.model.*; import org.wso2.carbon.identity.certificate.management.model.Certificate; @@ -179,6 +180,31 @@ public void testBuildRequestWithNoAccessConfig() assertEquals(ops.get(0).getOp(), Operation.REDIRECT); } + @Test + public void testBuildRequestWithNonInFlowActionFallsBackToMinimalPayload() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequestContext reqCtx = mock(ActionExecutionRequestContext.class); + when(reqCtx.getAction()).thenReturn(mock(Action.class)); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); + + assertNotNull(request); + assertEquals(request.getActionType(), ActionType.IN_FLOW_EXTENSION); + assertEquals(request.getAllowedOperations().size(), 1); + assertEquals(request.getAllowedOperations().get(0).getOp(), Operation.REDIRECT); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event); + assertEquals(event.getFlowId(), execCtx.getContextIdentifier()); + assertNull(event.getUser()); + assertNull(event.getFlowType()); + } + @Test public void testBuildRequestWithEmptyModifyPaths() throws ActionExecutionRequestBuilderException { @@ -651,6 +677,32 @@ public void testCredentialEncryptedWhenExposePathMarkedEncrypted() } } + @Test + public void testEncryptedExposePathIsOmittedWhenCertificateIsMissing() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + execCtx.setProperty("riskScore", "85"); + + AccessConfig accessConfig = new AccessConfig( + Arrays.asList( + new ContextPath("/properties/riskScore", true), + new ContextPath("/properties/existingProp", false)), + null); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event.getFlowProperties()); + assertFalse(event.getFlowProperties().containsKey("riskScore"), + "Encrypted expose path should be omitted when outbound certificate is missing."); + assertEquals(event.getFlowProperties().get("existingProp"), "existingValue"); + } + // ========================= Outbound encryption failure ========================= @Test(expectedExceptions = ActionExecutionRequestBuilderException.class) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessorTest.java index 88ede1b73332..2a96adb6c372 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessorTest.java @@ -37,6 +37,9 @@ import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; import org.wso2.carbon.identity.action.execution.api.model.Success; +import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; +import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; +import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; import org.wso2.carbon.identity.flow.execution.engine.Constants; import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; import org.wso2.carbon.identity.flow.inflow.extensions.internal.InFlowExtensionDataHolder; @@ -69,6 +72,7 @@ public class InFlowExtensionResponseProcessorTest { private InFlowExtensionResponseProcessor responseProcessor; private MockedStatic loggerUtilsMock; private MockedStatic holderMock; + private InFlowExtensionDataHolder holderInstance; private FlowContext capturedFlowContext; @BeforeMethod @@ -78,7 +82,7 @@ public void setUp() throws Exception { loggerUtilsMock = mockStatic(LoggerUtils.class); loggerUtilsMock.when(LoggerUtils::isDiagnosticLogsEnabled).thenReturn(false); - InFlowExtensionDataHolder holderInstance = mock(InFlowExtensionDataHolder.class); + holderInstance = mock(InFlowExtensionDataHolder.class); holderMock = mockStatic(InFlowExtensionDataHolder.class); holderMock.when(InFlowExtensionDataHolder::getInstance).thenReturn(holderInstance); } @@ -384,24 +388,30 @@ public void testUserClaimReplaceIdentityClaimRejected() FlowExecutionContext execCtx = createFlowExecutionContext(); - // Identity claim should be rejected. + // Identity claim should be rejected by the identity-claim prefix guard. PerformableOperation claimOp = createOperation( Operation.REPLACE, "/user/claims/http://wso2.org/claims/identity/accountLocked", "true"); ActionExecutionStatus status = executeSuccessResponse( execCtx, claimOp, Collections.emptyMap()); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - // Claim should NOT be set. - assertNull(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/identity/accountLocked")); + // Claim must NOT be staged in the pending map. + assertNull(capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class)); } @Test public void testUserClaimReplaceNonExistentClaimRejected() - throws ActionExecutionResponseProcessorException { + throws ActionExecutionResponseProcessorException, ClaimMetadataException { FlowExecutionContext execCtx = createFlowExecutionContext(); - // Claim not in the mocked local claim list should be rejected. + // Mock ClaimMetadataManagementService to return null for an unknown claim URI. + ClaimMetadataManagementService claimService = mock(ClaimMetadataManagementService.class); + when(holderInstance.getClaimMetadataManagementService()).thenReturn(claimService); + when(claimService.getLocalClaim("http://wso2.org/claims/nonexistent", "carbon.super")) + .thenReturn(java.util.Optional.empty()); + + // Claim not registered in the system should be rejected. PerformableOperation claimOp = createOperation( Operation.REPLACE, "/user/claims/http://wso2.org/claims/nonexistent", "value"); @@ -409,7 +419,8 @@ public void testUserClaimReplaceNonExistentClaimRejected() execCtx, claimOp, Collections.emptyMap()); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - assertNull(execCtx.getFlowUser().getClaims().get("http://wso2.org/claims/nonexistent")); + // Claim must NOT be staged in the pending map. + assertNull(capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class)); } @Test @@ -418,7 +429,7 @@ public void testUserClaimReplaceNonLocalDialectRejected() FlowExecutionContext execCtx = createFlowExecutionContext(); - // Non-local dialect claim should be rejected. + // Non-local dialect claim should be rejected by the local-dialect prefix guard. PerformableOperation claimOp = createOperation( Operation.REPLACE, "/user/claims/urn:ietf:params:scim:schemas:core:2.0:User:name.givenName", "John"); @@ -426,8 +437,8 @@ public void testUserClaimReplaceNonLocalDialectRejected() execCtx, claimOp, Collections.emptyMap()); assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - assertNull(execCtx.getFlowUser().getClaims().get( - "urn:ietf:params:scim:schemas:core:2.0:User:name.givenName")); + // Claim must NOT be staged in the pending map. + assertNull(capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class)); } @Test @@ -559,6 +570,44 @@ public void testUnknownPathPrefix() throws ActionExecutionResponseProcessorExcep assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); } + @Test + public void testNullPathOperationFails() throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + // Operation with null path must fail gracefully without NPE. + PerformableOperation op = new PerformableOperation(); + op.setOp(Operation.REPLACE); + // path is intentionally not set (null). + + ActionExecutionStatus status = executeSuccessResponse(execCtx, op, Collections.emptyMap()); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + // Nothing should be staged in any pending map. + assertNull(capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class)); + assertNull(capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_CLAIMS_KEY, Map.class)); + assertNull(capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_CREDENTIALS_KEY, Map.class)); + } + + @Test + public void testUnsupportedOperationTypeOnValidPathFails() + throws ActionExecutionResponseProcessorException { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + + // ADD (non-REPLACE) on a valid properties path must be rejected before routing. + PerformableOperation op = new PerformableOperation(); + op.setOp(Operation.ADD); + op.setPath("/properties/riskScore"); + op.setValue("90"); + + ActionExecutionStatus status = executeSuccessResponse(execCtx, op, Collections.emptyMap()); + + assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); + // Property must NOT be staged. + assertNull(capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class)); + } + // ========================= processSuccessResponse — Multiple operations ========================= @Test @@ -907,23 +956,44 @@ public void testDecryptionFailureThrowsException() } @Test - public void testNonStringValueForEncryptedPathHandledGracefully() - throws ActionExecutionResponseProcessorException { + public void testNonStringValueForEncryptedPathThrowsException() { FlowExecutionContext execCtx = createFlowExecutionContext(); List modifyPaths = Arrays.asList(new ContextPath("/properties/data", true)); - // Non-string value (Integer) for an encrypted path — should pass through without decryption. + // Non-string value (Integer) for an encrypted path must now be rejected with an exception + // because accepting non-JWE values would make the encryption flag advisory. PerformableOperation op = createOperation(Operation.REPLACE, "/properties/data", 42); - ActionExecutionStatus status = executeSuccessResponseWithModifyPaths( - execCtx, Collections.singletonList(op), Collections.emptyMap(), modifyPaths); + try { + executeSuccessResponseWithModifyPaths( + execCtx, Collections.singletonList(op), Collections.emptyMap(), modifyPaths); + throw new AssertionError("Expected ActionExecutionResponseProcessorException was not thrown."); + } catch (ActionExecutionResponseProcessorException e) { + assertTrue(e.getMessage().contains("/properties/data"), + "Exception message should reference the offending path."); + } + } - assertEquals(status.getStatus(), ActionExecutionStatus.Status.SUCCESS); - // Value should be coerced to String (default behavior for properties). - Map pendingProps = - capturedFlowContext.getValue(InFlowExtensionConstants.PENDING_PROPERTIES_KEY, Map.class); - assertNotNull(pendingProps); - assertEquals(pendingProps.get("data"), "42"); + @Test + public void testPlaintextStringForEncryptedPathThrowsException() { + + FlowExecutionContext execCtx = createFlowExecutionContext(); + List modifyPaths = Arrays.asList(new ContextPath("/properties/secret", true)); + + // Plain-text string (not JWE-formatted) for an encrypted modify path must be rejected. + PerformableOperation op = createOperation(Operation.REPLACE, "/properties/secret", "plaintext-value"); + + try (MockedStatic jweUtilMock = mockStatic(JWEEncryptionUtil.class)) { + jweUtilMock.when(() -> JWEEncryptionUtil.isJWEEncrypted(anyString())).thenReturn(false); + try { + executeSuccessResponseWithModifyPaths( + execCtx, Collections.singletonList(op), Collections.emptyMap(), modifyPaths); + throw new AssertionError("Expected ActionExecutionResponseProcessorException was not thrown."); + } catch (ActionExecutionResponseProcessorException e) { + assertTrue(e.getMessage().contains("/properties/secret"), + "Exception message should reference the offending path."); + } + } } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtilTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtilTest.java index 7727e8b085fe..903d857c0567 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtilTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtilTest.java @@ -206,6 +206,42 @@ public void testCoerceValueStringValue() { assertEquals(result, "test"); } + @Test + public void testCoerceValueNoAnnotationNullValue() { + + Object result = PathTypeAnnotationUtil.coerceValue( + "/properties/score", null, Collections.emptyMap()); + assertNull(result); + } + + @Test + public void testCoerceValuePrimaryTypeAnnotationNullValue() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/score", "Integer"); + + Object result = PathTypeAnnotationUtil.coerceValue("/properties/score", null, annotations); + assertNull(result); + } + + @Test + public void testCoerceValueMultivaluedPrimaryPreservesNullElements() { + + Map annotations = new HashMap<>(); + annotations.put("/properties/tags", "[String]"); + + List input = Arrays.asList("tag1", null, "tag3"); + Object result = PathTypeAnnotationUtil.coerceValue("/properties/tags", input, annotations); + + assertTrue(result instanceof List); + @SuppressWarnings("unchecked") + List list = (List) result; + assertEquals(list.size(), 3); + assertEquals(list.get(0), "tag1"); + assertNull(list.get(1)); + assertEquals(list.get(2), "tag3"); + } + // ========================= validateAnnotationLimits ========================= @Test diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfigTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfigTest.java index f68a99b98cda..2aba57fc4ad3 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfigTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfigTest.java @@ -41,8 +41,10 @@ public void testAccessConfigWithNullLists() { AccessConfig config = new AccessConfig(null, null); assertNull(config.getExpose()); assertNull(config.getModify()); - assertNull(config.getExposePaths()); - assertNull(config.getModifyPaths()); + assertNotNull(config.getExposePaths()); + assertTrue(config.getExposePaths().isEmpty()); + assertNotNull(config.getModifyPaths()); + assertTrue(config.getModifyPaths().isEmpty()); } @Test diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowMgtService.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowMgtService.java index 11208f233c06..50e370f5e1ca 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowMgtService.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/FlowMgtService.java @@ -91,9 +91,7 @@ public void updateFlow(FlowDTO flowDTO, int tenantID) clearFlowResolveCache(flowDTO.getFlowType(), tenantID); GraphConfig flowConfig = new GraphBuilder().withSteps(flowDTO.getSteps()).build(); - FLOW_DAO.updateFlow(flowDTO.getFlowType(), flowConfig, tenantID, DEFAULT_FLOW_NAME); - AuditLog.AuditLogBuilder auditLogBuilder = new AuditLog.AuditLogBuilder(getInitiatorId(), LoggerUtils.getInitiatorType(getInitiatorId()), flowConfig.getId(), diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceDataHolder.java index 371cf58366b2..22109cfcf196 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceDataHolder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.mgt/src/main/java/org/wso2/carbon/identity/flow/mgt/internal/FlowMgtServiceDataHolder.java @@ -23,7 +23,6 @@ import org.wso2.carbon.identity.organization.management.service.OrganizationManager; import org.wso2.carbon.identity.organization.resource.hierarchy.traverse.service.OrgResourceResolverService; - /** * A singleton class to hold the data of the flow management service. */ diff --git a/features/flow-orchestration-framework/pom.xml b/features/flow-orchestration-framework/pom.xml index 82c3d862ce9c..b62f173b3ee0 100644 --- a/features/flow-orchestration-framework/pom.xml +++ b/features/flow-orchestration-framework/pom.xml @@ -35,6 +35,7 @@ org.wso2.carbon.identity.flow.mgt.server.feature org.wso2.carbon.identity.flow.orchestration.framework.feature org.wso2.carbon.identity.flow.execution.engine.server.feature + org.wso2.carbon.identity.flow.inflow.extensions.server.feature From 6d9d92545ee91b6434175a01aab518d5f26eb60b Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Mon, 11 May 2026 08:09:55 +0530 Subject: [PATCH 28/41] fixed a minor import issue java.util.stream.Collectors --- .../identity/flow/inflow/extensions/model/AccessConfig.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfig.java index 37b454498508..648616a889ed 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfig.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfig.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; /** * Access Configuration for In-Flow Extension actions. @@ -84,7 +85,7 @@ public List getExposePaths() { if (expose == null) { return Collections.emptyList(); } - return expose.stream().map(ContextPath::getPath).toList(); + return expose.stream().map(ContextPath::getPath).collect(Collectors.toList()); } /** @@ -108,7 +109,7 @@ public List getModifyPaths() { if (modify == null) { return Collections.emptyList(); } - return modify.stream().map(ContextPath::getPath).toList(); + return modify.stream().map(ContextPath::getPath).collect(Collectors.toList()); } /** From 54bfdf42ef2b936026a6732baf4aa80648ccb42a Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Mon, 11 May 2026 10:44:42 +0530 Subject: [PATCH 29/41] corrected copyright headers --- .../extensions/executor/InFlowExtensionResponseProcessor.java | 2 +- .../flow/inflow/extensions/executor/JWEEncryptionUtil.java | 2 +- .../flow/inflow/extensions/executor/PathTypeAnnotationUtil.java | 2 +- .../extensions/management/InFlowExtensionActionConverter.java | 2 +- .../management/InFlowExtensionActionDTOModelResolver.java | 2 +- .../identity/flow/inflow/extensions/model/AccessConfig.java | 2 +- .../identity/flow/inflow/extensions/model/ContextPath.java | 2 +- .../identity/flow/inflow/extensions/model/Encryption.java | 2 +- .../flow/inflow/extensions/model/InFlowExtensionAction.java | 2 +- .../flow/inflow/extensions/model/InFlowExtensionEvent.java | 2 +- .../flow/inflow/extensions/model/InFlowExtensionRequest.java | 2 +- .../flow/inflow/extensions/model/OperationExecutionResult.java | 2 +- .../extensions/executor/HierarchicalPrefixMatcherTest.java | 2 +- .../inflow/extensions/executor/InFlowExtensionExecutorTest.java | 2 +- .../extensions/executor/InFlowExtensionRequestBuilderTest.java | 2 +- .../executor/InFlowExtensionResponseProcessorTest.java | 2 +- .../inflow/extensions/executor/PathTypeAnnotationUtilTest.java | 2 +- .../identity/flow/inflow/extensions/model/AccessConfigTest.java | 2 +- .../flow/inflow/extensions/model/InFlowExtensionEventTest.java | 2 +- .../inflow/extensions/model/OperationExecutionResultTest.java | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessor.java index 2224cace85da..1df1c1a4f765 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/JWEEncryptionUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/JWEEncryptionUtil.java index e38d93464124..d456fbfd6861 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/JWEEncryptionUtil.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/JWEEncryptionUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtil.java index 012566394ed8..5d0de0248d4e 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtil.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConverter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConverter.java index dce336862abe..ab23dd441f2f 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConverter.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConverter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionDTOModelResolver.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionDTOModelResolver.java index 67c90dc5858f..dd8e6a7609f8 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionDTOModelResolver.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionDTOModelResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfig.java index 648616a889ed..d970a3245add 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfig.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/ContextPath.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/ContextPath.java index 964c0767c63a..603ea3c159ec 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/ContextPath.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/ContextPath.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/Encryption.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/Encryption.java index 21247cdb12d9..a9296bcfb4e3 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/Encryption.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/Encryption.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionAction.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionAction.java index 60e67feafede..30a297000df3 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionAction.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionAction.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEvent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEvent.java index 1a26573be74d..c1f3a1ccabee 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEvent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionRequest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionRequest.java index 4aab7195933e..0f36f68c0a69 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionRequest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResult.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResult.java index a80c6c2ece30..73eeb00ac073 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResult.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResult.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcherTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcherTest.java index 4126048778f5..ea938ea70f5c 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcherTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcherTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java index 10376b65f710..d18ed6661272 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilderTest.java index 908d48744c45..489687ed4783 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessorTest.java index 2a96adb6c372..8a698ac866f5 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtilTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtilTest.java index 903d857c0567..409dc05acbfd 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtilTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtilTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfigTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfigTest.java index 2aba57fc4ad3..47d30a127cf1 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfigTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfigTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEventTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEventTest.java index e7c5d3d00f46..97ad067bb1f3 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEventTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEventTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResultTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResultTest.java index 0c0c67b0b3ca..8e1b3f1fa8e2 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResultTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResultTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * 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 From 7239b921a31f8be0893160ac5ec414ec4ef1a162 Mon Sep 17 00:00:00 2001 From: ThejithaR Date: Mon, 11 May 2026 17:36:39 +0530 Subject: [PATCH 30/41] Removed the deployment.toml based context filtering - moved to constant policy --- .../pom.xml | 1 - .../config/FlowContextHandoverConfig.java | 177 ---------- .../config/FlowContextHandoverConstants.java | 47 --- .../FlowExecutionEngineDataHolder.java | 25 +- .../config/FlowContextHandoverConfigTest.java | 153 --------- .../FlowContextHandoverConfigTestHelper.java | 54 --- .../FlowExecutionContextFilterTest.java | 320 ------------------ .../src/test/resources/testng.xml | 6 - .../pom.xml | 7 +- .../extensions/InFlowExtensionConstants.java | 63 ++++ .../executor/InFlowExtensionExecutor.java | 14 +- .../InFlowExtensionContextTreeBuilder.java | 2 +- .../InFlowExtensionContextTreeService.java | 5 +- .../model/FlowContextHandoverConfig.java | 118 +++++++ .../InFlowExtensionContextFilterUtil.java} | 80 ++--- .../extensions/InFlowExtensionTestUtils.java | 31 +- .../executor/InFlowExtensionExecutorTest.java | 10 - .../FlowContextHandoverConfigTestHelper.java | 2 +- ...InFlowExtensionContextTreeBuilderTest.java | 2 +- .../resources/identity.xml.j2 | 25 -- 20 files changed, 233 insertions(+), 909 deletions(-) delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfig.java delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConstants.java delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTest.java delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTestHelper.java delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilterTest.java create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/FlowContextHandoverConfig.java rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilter.java => org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/util/InFlowExtensionContextFilterUtil.java} (68%) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml index 2680f9fc341a..c88ff2a0a700 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml @@ -180,7 +180,6 @@ !org.wso2.carbon.identity.flow.execution.internal, org.wso2.carbon.identity.flow.execution.engine.util, org.wso2.carbon.identity.flow.execution.engine, - org.wso2.carbon.identity.flow.execution.engine.config, org.wso2.carbon.identity.flow.execution.engine.internal, org.wso2.carbon.identity.flow.execution.engine.core, org.wso2.carbon.identity.flow.execution.engine.cache, diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfig.java deleted file mode 100644 index 9e8c3d37379e..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfig.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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.config; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.wso2.carbon.identity.core.util.IdentityConfigParser; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Loads and exposes the global flow execution context handover filtering configuration from - * {@code identity.xml} (templated from {@code deployment.toml}). - * - *

      Two flat allow-lists are read: - *

        - *
      • {@code FlowExecutionContextHandover.IncludedAttributes.IncludedAttribute} — - * top-level property names of {@link - * org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext} - * to expose.
      • - *
      • {@code FlowExecutionContextHandover.IncludedUserAttributes.IncludedUserAttribute} — - * property names of {@link - * org.wso2.carbon.identity.flow.execution.engine.model.FlowUser} to expose.
      • - *
      - * - *

      If {@code "flowUser"} is present in {@code includedAttributes} the entire - * {@code FlowUser} is passed through and {@code includedUserAttributes} is ignored - * ({@link #isFullUserPassthrough()} returns {@code true}).

      - * - *

      When the configuration is absent or empty a permissive fallback (all known attributes) - * is used so that upgrading a server without updating deployment.toml preserves current - * behaviour.

      - */ -public final class FlowContextHandoverConfig { - - private static final Log LOG = LogFactory.getLog(FlowContextHandoverConfig.class); - - private final Set includedAttributes; - private final Set includedUserAttributes; - private final boolean fullUserPassthrough; - - /** - * Construct by reading from the carbon configuration ({@code IdentityConfigParser}). - * Call once during component activation; the result is immutable. - */ - public FlowContextHandoverConfig() { - - Set attrs = readList(FlowContextHandoverConstants.INCLUDED_ATTRIBUTES_KEY); - Set userAttrs = readList(FlowContextHandoverConstants.INCLUDED_USER_ATTRIBUTES_KEY); - - if (attrs.isEmpty()) { - // Permissive fallback — no config present, expose everything. - LOG.warn("No 'FlowExecutionContextHandover.IncludedAttributes' configured in identity.xml. " - + "Falling back to permissive mode (all attributes exposed)."); - FlowContextHandoverConfig permissive = permissive(); - this.includedAttributes = permissive.includedAttributes; - this.includedUserAttributes = permissive.includedUserAttributes; - this.fullUserPassthrough = permissive.fullUserPassthrough; - return; - } - - this.includedAttributes = Collections.unmodifiableSet(attrs); - this.includedUserAttributes = Collections.unmodifiableSet(userAttrs); - this.fullUserPassthrough = attrs.contains(FlowContextHandoverConstants.ATTR_FLOW_USER); - - if (LOG.isDebugEnabled()) { - LOG.debug("FlowContextHandoverConfig loaded. includedAttributes=" + includedAttributes - + ", includedUserAttributes=" + includedUserAttributes - + ", fullUserPassthrough=" + fullUserPassthrough); - } - } - - /** - * Private constructor used by {@link #permissive()}. - */ - private FlowContextHandoverConfig(Set includedAttributes, - Set includedUserAttributes, - boolean fullUserPassthrough) { - - this.includedAttributes = Collections.unmodifiableSet(includedAttributes); - this.includedUserAttributes = Collections.unmodifiableSet(includedUserAttributes); - this.fullUserPassthrough = fullUserPassthrough; - } - - /** - * Factory that creates a permissive config allowing all known attributes on both - * {@code FlowExecutionContext} and {@code FlowUser}. Used as a safe fallback when no - * configuration is present, preserving behaviour for servers upgrading without updating - * deployment.toml. - * - * @return a permissive {@link FlowContextHandoverConfig}. - */ - public static FlowContextHandoverConfig permissive() { - - Set allContext = new HashSet<>(FlowExecutionContextFilter.getKnownContextAttributes()); - Set allUser = new HashSet<>(FlowExecutionContextFilter.getKnownUserAttributes()); - return new FlowContextHandoverConfig(allContext, allUser, false); - } - - /** - * Top-level property names of {@code FlowExecutionContext} that are included in the - * filtered copy handed to downstream consumers. - * - * @return unmodifiable set of attribute names. - */ - public Set getIncludedAttributes() { - - return includedAttributes; - } - - /** - * Property names of {@code FlowUser} that are included in the filtered copy. - * Ignored when {@link #isFullUserPassthrough()} is {@code true}. - * - * @return unmodifiable set of user attribute names. - */ - public Set getIncludedUserAttributes() { - - return includedUserAttributes; - } - - /** - * Returns {@code true} when {@code "flowUser"} is present in {@code includedAttributes}, - * meaning the entire {@code FlowUser} object is passed through and - * {@code includedUserAttributes} is not consulted. - * - * @return whether the full FlowUser passes through unchanged. - */ - public boolean isFullUserPassthrough() { - - return fullUserPassthrough; - } - - // ---- private helpers ---- - - @SuppressWarnings("unchecked") - private static Set readList(String key) { - - Object raw = IdentityConfigParser.getInstance().getConfiguration().get(key); - List items = new ArrayList<>(); - if (raw instanceof List) { - items = (List) raw; - } else if (raw instanceof String) { - String s = ((String) raw).trim(); - if (!s.isEmpty()) { - items.add(s); - } - } - Set result = new HashSet<>(); - for (String item : items) { - if (item != null && !item.trim().isEmpty()) { - result.add(item.trim()); - } - } - return result; - } -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConstants.java deleted file mode 100644 index 8a8dfe50ede1..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConstants.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.config; - -/** - * Constants used by the flow execution context handover filtering. - */ -public final class FlowContextHandoverConstants { - - private FlowContextHandoverConstants() { - - } - - // ---- identity.xml config keys (used by IdentityUtil.getPropertyAsList) ---- - public static final String HANDOVER_ROOT = "FlowExecutionContextHandover"; - public static final String INCLUDED_ATTRIBUTES_KEY = - HANDOVER_ROOT + ".IncludedAttributes.IncludedAttribute"; - public static final String INCLUDED_USER_ATTRIBUTES_KEY = - HANDOVER_ROOT + ".IncludedUserAttributes.IncludedUserAttribute"; - - // ---- Special attribute names ---- - /** When present in {@code includedAttributes}, the entire {@code FlowUser} passes through - * and {@code includedUserAttributes} is ignored. */ - public static final String ATTR_FLOW_USER = "flowUser"; - - /** Engine-internal flow id; always copied regardless of configuration. */ - public static final String ATTR_CONTEXT_IDENTIFIER = "contextIdentifier"; - - /** User-credentials property name; the only Map field requiring per-entry char[] cloning. */ - public static final String ATTR_USER_CREDENTIALS = "userCredentials"; -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java index 31ef18547817..f1775598ea45 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java @@ -21,7 +21,6 @@ import org.wso2.carbon.identity.application.mgt.ApplicationManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.event.services.IdentityEventService; -import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; import org.wso2.carbon.identity.flow.execution.engine.listener.FlowExecutionListener; import org.wso2.carbon.identity.flow.mgt.FlowMgtService; @@ -49,7 +48,6 @@ public class FlowExecutionEngineDataHolder { private FederatedAssociationManager federatedAssociationManager; private IdentityEventService identityEventService; private List flowExecutionListeners = new ArrayList<>(); - private FlowContextHandoverConfig flowContextHandoverConfig; private FlowExecutionEngineDataHolder() { @@ -221,26 +219,5 @@ public void setIdentityEventService(IdentityEventService identityEventService) { this.identityEventService = identityEventService; } - - /** - * Get the flow execution context handover config. Lazily initialised on first access - * so that {@code IdentityConfigParser} is guaranteed to be loaded before we read it. - * - * @return the handover config, never null. - */ - public synchronized FlowContextHandoverConfig getFlowContextHandoverConfig() { - - if (flowContextHandoverConfig == null) { - flowContextHandoverConfig = new FlowContextHandoverConfig(); - } - return flowContextHandoverConfig; - } - - /** - * Override the handover config. Intended for tests only. - */ - public synchronized void setFlowContextHandoverConfig(FlowContextHandoverConfig flowContextHandoverConfig) { - - this.flowContextHandoverConfig = flowContextHandoverConfig; - } } + diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTest.java deleted file mode 100644 index b5c3377c62b6..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTest.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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.config; - -import org.testng.annotations.Test; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.assertTrue; - -/** - * Unit tests for {@link FlowContextHandoverConfig}. - * - *

      Tests that do not read {@code identity.xml} use - * {@link FlowContextHandoverConfigTestHelper} to build instances with explicit allow-lists, - * or use the {@link FlowContextHandoverConfig#permissive()} factory which seeds allow-lists - * from the static maps on {@link FlowExecutionContextFilter}.

      - */ -public class FlowContextHandoverConfigTest { - - // ========================= permissive() factory ========================= - - @Test - public void testPermissiveIncludesAllKnownContextAttributes() { - - FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); - - // At minimum all well-known FlowExecutionContext properties must be present. - Set attrs = cfg.getIncludedAttributes(); - assertNotNull(attrs); - assertFalse(attrs.isEmpty()); - - // These are the canonical FlowExecutionContext property names exposed by the filter. - assertTrue(attrs.contains("tenantDomain"), "permissive missing tenantDomain"); - assertTrue(attrs.contains("applicationId"), "permissive missing applicationId"); - assertTrue(attrs.contains("flowType"), "permissive missing flowType"); - assertTrue(attrs.contains("callbackUrl"), "permissive missing callbackUrl"); - assertTrue(attrs.contains("portalUrl"), "permissive missing portalUrl"); - assertTrue(attrs.contains("properties"), "permissive missing properties"); - } - - @Test - public void testPermissiveIncludesAllKnownUserAttributes() { - - FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); - - Set userAttrs = cfg.getIncludedUserAttributes(); - assertNotNull(userAttrs); - assertFalse(userAttrs.isEmpty()); - - // Canonical FlowUser property names. - assertTrue(userAttrs.contains("username"), "permissive missing username"); - assertTrue(userAttrs.contains("userId"), "permissive missing userId"); - assertTrue(userAttrs.contains("userStoreDomain"), "permissive missing userStoreDomain"); - assertTrue(userAttrs.contains("claims"), "permissive missing claims"); - assertTrue(userAttrs.contains("userCredentials"), "permissive missing userCredentials"); - } - - @Test - public void testPermissiveDoesNotSetFullUserPassthrough() { - - // permissive() does NOT put "flowUser" in includedAttributes — it seeds individual - // user attributes instead. So isFullUserPassthrough() must be false. - FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); - assertFalse(cfg.isFullUserPassthrough()); - } - - // ========================= fullUserPassthrough flag ========================= - - @Test - public void testFullUserPassthroughTrueWhenFlowUserInAttrs() { - - Set attrs = new HashSet<>(Arrays.asList("flowUser", "tenantDomain")); - FlowContextHandoverConfig cfg = FlowContextHandoverConfigTestHelper.of(attrs, new HashSet<>()); - - assertTrue(cfg.isFullUserPassthrough()); - } - - @Test - public void testFullUserPassthroughFalseWhenFlowUserAbsent() { - - Set attrs = new HashSet<>(Arrays.asList("tenantDomain", "applicationId")); - FlowContextHandoverConfig cfg = FlowContextHandoverConfigTestHelper.of(attrs, new HashSet<>()); - - assertFalse(cfg.isFullUserPassthrough()); - } - - // ========================= allow-list contents ========================= - - @Test - public void testIncludedAttributesReflectInput() { - - Set attrs = new HashSet<>(Arrays.asList("tenantDomain", "properties")); - Set userAttrs = new HashSet<>(Arrays.asList("username", "claims")); - - FlowContextHandoverConfig cfg = FlowContextHandoverConfigTestHelper.of(attrs, userAttrs); - - assertTrue(cfg.getIncludedAttributes().contains("tenantDomain")); - assertTrue(cfg.getIncludedAttributes().contains("properties")); - assertFalse(cfg.getIncludedAttributes().contains("applicationId")); - - assertTrue(cfg.getIncludedUserAttributes().contains("username")); - assertTrue(cfg.getIncludedUserAttributes().contains("claims")); - assertFalse(cfg.getIncludedUserAttributes().contains("userId")); - } - - @Test - public void testIncludedAttributesSetIsUnmodifiable() { - - FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); - - try { - cfg.getIncludedAttributes().add("injected"); - // If no exception, check the value wasn't actually added (some impls are backed - // by unmodifiable wrappers that silently discard — but the API contract is that - // clients must not rely on mutation, so either throwing or silent discard is fine). - } catch (UnsupportedOperationException e) { - // Expected — the set is unmodifiable. - } - } - - @Test - public void testIncludedUserAttributesSetIsUnmodifiable() { - - FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); - - try { - cfg.getIncludedUserAttributes().add("injected"); - } catch (UnsupportedOperationException e) { - // Expected. - } - } -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTestHelper.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTestHelper.java deleted file mode 100644 index ca82b4ae59a7..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowContextHandoverConfigTestHelper.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.config; - -import java.lang.reflect.Constructor; -import java.util.Set; - -/** - * Test-only helper that instantiates {@link FlowContextHandoverConfig} with explicit - * allow-lists without touching {@code IdentityConfigParser}. Lives in the same package - * as the production class so it can access the package-private constructor via reflection - * from a predictable location. - */ -public final class FlowContextHandoverConfigTestHelper { - - private FlowContextHandoverConfigTestHelper() { - - } - - /** - * Construct a {@link FlowContextHandoverConfig} with the given allow-lists. - * {@code fullUserPassthrough} is derived from whether {@code "flowUser"} is in - * {@code attrs}, mirroring the production constructor logic. - */ - public static FlowContextHandoverConfig of(Set attrs, Set userAttrs) { - - try { - Constructor ctor = - FlowContextHandoverConfig.class.getDeclaredConstructor( - Set.class, Set.class, boolean.class); - ctor.setAccessible(true); - boolean fullPassthrough = attrs.contains(FlowContextHandoverConstants.ATTR_FLOW_USER); - return ctor.newInstance(attrs, userAttrs, fullPassthrough); - } catch (Exception e) { - throw new RuntimeException("Cannot construct FlowContextHandoverConfig for test", e); - } - } -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilterTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilterTest.java deleted file mode 100644 index b41ca4a920cc..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilterTest.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * 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.config; - -import org.testng.annotations.Test; -import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; -import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.assertNotSame; -import static org.testng.Assert.assertNull; -import static org.testng.Assert.assertTrue; - -/** - * Unit tests for {@link FlowExecutionContextFilter}. - * - *

      Uses the {@link FlowContextHandoverConfig#permissive()} factory and the private - * constructor variant (accessed via a local helper) to avoid touching - * {@code IdentityConfigParser} — no OSGi or Mockito static setup required.

      - */ -public class FlowExecutionContextFilterTest { - - // ========================= null guards ========================= - - @Test - public void testNullOriginalReturnsNull() { - - assertNull(FlowExecutionContextFilter.filter(null, FlowContextHandoverConfig.permissive())); - } - - @Test - public void testNullConfigFallsBackToPermissive() { - - FlowExecutionContext orig = createFullyPopulatedContext(); - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, null); - - // Permissive fallback — every whitelisted field should round-trip. - assertEquals(copy.getTenantDomain(), "carbon.super"); - assertEquals(copy.getApplicationId(), "app-1"); - assertEquals(copy.getFlowType(), "REGISTRATION"); - assertEquals(copy.getCallbackUrl(), "https://callback"); - assertEquals(copy.getPortalUrl(), "https://portal"); - assertEquals(copy.getFlowUser().getUserId(), "user-1"); - assertEquals(copy.getFlowUser().getClaims().get("http://wso2.org/claims/email"), "u@e.com"); - assertEquals(copy.getProperties().get("p1"), "v1"); - } - - // ========================= contextIdentifier ========================= - - @Test - public void testContextIdentifierAlwaysCopied() { - - FlowExecutionContext orig = createFullyPopulatedContext(); - - // Even with an empty allow-list contextIdentifier must round-trip. - FlowContextHandoverConfig strict = configFrom(new HashSet<>(), new HashSet<>()); - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, strict); - - assertEquals(copy.getContextIdentifier(), "ctx-1"); - } - - // ========================= flow-branch toggles ========================= - - @Test - public void testAllFlowAttrsExcludedWhenNoneInAllowList() { - - FlowExecutionContext orig = createFullyPopulatedContext(); - // Allow-list has only user attrs — no flow attrs. - FlowContextHandoverConfig cfg = configFrom( - new HashSet<>(Arrays.asList("username", "claims")), - new HashSet<>(Arrays.asList("username", "claims"))); - - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - - assertNull(copy.getTenantDomain()); - assertNull(copy.getApplicationId()); - assertNull(copy.getFlowType()); - assertNull(copy.getCallbackUrl()); - assertNull(copy.getPortalUrl()); - } - - @Test - public void testFlowAttrSelective() { - - FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverConfig cfg = configFrom( - new HashSet<>(Arrays.asList("tenantDomain", "flowType", "portalUrl")), - new HashSet<>()); - - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - - assertEquals(copy.getTenantDomain(), "carbon.super"); - assertNull(copy.getApplicationId()); - assertEquals(copy.getFlowType(), "REGISTRATION"); - assertNull(copy.getCallbackUrl()); - assertEquals(copy.getPortalUrl(), "https://portal"); - } - - // ========================= user-branch toggles ========================= - - @Test - public void testUserAbsentFromAllowListYieldsEmptyFlowUser() { - - FlowExecutionContext orig = createFullyPopulatedContext(); - // No "flowUser" and no user attrs — user branch omitted. - FlowContextHandoverConfig cfg = configFrom( - new HashSet<>(Arrays.asList("tenantDomain")), - new HashSet<>()); - - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - - // FlowUser must be a non-null empty shell so downstream code doesn't NPE. - assertNotNull(copy.getFlowUser()); - assertNull(copy.getFlowUser().getUserId()); - assertNull(copy.getFlowUser().getUserStoreDomain()); - assertTrue(copy.getFlowUser().getClaims() == null || copy.getFlowUser().getClaims().isEmpty()); - assertTrue(copy.getFlowUser().getUserCredentials() == null - || copy.getFlowUser().getUserCredentials().isEmpty()); - } - - @Test - public void testPerLeafUserToggles() { - - FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverConfig cfg = configFrom( - new HashSet<>(Arrays.asList("flowUser")), // flowUser in context attrs - new HashSet<>(Arrays.asList("userStoreDomain", "claims"))); // userId excluded - - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - - // fullUserPassthrough == true because "flowUser" is in includedAttributes - // → entire user is passed through regardless of includedUserAttributes - assertEquals(copy.getFlowUser().getUserId(), "user-1"); - assertEquals(copy.getFlowUser().getUserStoreDomain(), "PRIMARY"); - assertEquals(copy.getFlowUser().getClaims().get("http://wso2.org/claims/email"), "u@e.com"); - } - - @Test - public void testIndividualUserAttrSelection() { - - FlowExecutionContext orig = createFullyPopulatedContext(); - // No "flowUser" — use individual user attr list. - FlowContextHandoverConfig cfg = configFrom( - new HashSet<>(Arrays.asList("tenantDomain")), // no "flowUser" - new HashSet<>(Arrays.asList("userStoreDomain", "claims"))); - - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - - assertNull(copy.getFlowUser().getUserId()); - assertEquals(copy.getFlowUser().getUserStoreDomain(), "PRIMARY"); - assertEquals(copy.getFlowUser().getClaims().get("http://wso2.org/claims/email"), "u@e.com"); - assertTrue(copy.getFlowUser().getUserCredentials() == null - || copy.getFlowUser().getUserCredentials().isEmpty()); - } - - // ========================= defensive copies ========================= - - @Test - public void testCredentialsAreDeepCopied() { - - FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); - - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - - char[] origPwd = orig.getFlowUser().getUserCredentials().get("password"); - char[] copyPwd = copy.getFlowUser().getUserCredentials().get("password"); - - assertNotSame(origPwd, copyPwd); - assertEquals(copyPwd, "secret".toCharArray()); - - // Wiping the copy must not affect the original. - Arrays.fill(copyPwd, '\0'); - assertEquals(origPwd, "secret".toCharArray()); - } - - @Test - public void testClaimsMapIsDefensivelyCopied() { - - FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); - - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - - copy.getFlowUser().addClaim("http://wso2.org/claims/leak", "oops"); - assertNull(orig.getFlowUser().getClaims().get("http://wso2.org/claims/leak")); - } - - @Test - public void testPropertiesAreDefensivelyCopied() { - - FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverConfig cfg = FlowContextHandoverConfig.permissive(); - - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - - copy.setProperty("p2", "leak"); - assertNull(orig.getProperty("p2")); - } - - // ========================= properties branch ========================= - - @Test - public void testPropertiesExcludedWhenNotInAllowList() { - - FlowExecutionContext orig = createFullyPopulatedContext(); - // "properties" not in allow-list. - FlowContextHandoverConfig cfg = configFrom( - new HashSet<>(Arrays.asList("tenantDomain")), - new HashSet<>()); - - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - - assertTrue(copy.getProperties() == null || copy.getProperties().isEmpty()); - } - - @Test - public void testPropertiesIncludedWhenInAllowList() { - - FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverConfig cfg = configFrom( - new HashSet<>(Arrays.asList("properties")), - new HashSet<>()); - - FlowExecutionContext copy = FlowExecutionContextFilter.filter(orig, cfg); - - assertEquals(copy.getProperty("p1"), "v1"); - } - - // ========================= original untouched ========================= - - @Test - public void testOriginalUntouchedAcrossAllToggles() { - - FlowExecutionContext orig = createFullyPopulatedContext(); - FlowContextHandoverConfig strict = configFrom(new HashSet<>(), new HashSet<>()); - - FlowExecutionContextFilter.filter(orig, strict); - - assertEquals(orig.getTenantDomain(), "carbon.super"); - assertEquals(orig.getApplicationId(), "app-1"); - assertEquals(orig.getFlowType(), "REGISTRATION"); - assertEquals(orig.getCallbackUrl(), "https://callback"); - assertEquals(orig.getPortalUrl(), "https://portal"); - assertEquals(orig.getFlowUser().getUserId(), "user-1"); - assertEquals(orig.getProperty("p1"), "v1"); - } - - // ========================= helpers ========================= - - /** - * Build a minimal {@link FlowContextHandoverConfig} from raw sets without touching - * {@code IdentityConfigParser}. Uses the permissive factory as a baseline and overrides - * via reflection would be heavy — instead we rely on the fact that - * {@code FlowContextHandoverConfig.permissive()} already covers the all-on case, and - * create targeted configs via a thin wrapper that directly exercises the filter with - * controlled allow-lists by passing them to the filter via the engine's {@code permissive()} - * overridden in a subclass if needed. - * - *

      Since {@link FlowContextHandoverConfig} has no public constructor taking sets, we - * call {@code permissive()} as the base and then exercise filter behaviour by passing - * allow-list information through a thin anonymous subclass. However, the class is - * {@code final}, so we instead test the filter contract indirectly by constructing real - * configs through the only available factory that doesn't read identity.xml.

      - * - *

      For selective allow-list tests we use a package-private test helper in the same - * package ({@code engine.config}) that exposes the private constructor.

      - */ - private static FlowContextHandoverConfig configFrom(Set attrs, Set userAttrs) { - - return FlowContextHandoverConfigTestHelper.of(attrs, userAttrs); - } - - private FlowExecutionContext createFullyPopulatedContext() { - - FlowExecutionContext ctx = new FlowExecutionContext(); - ctx.setContextIdentifier("ctx-1"); - ctx.setTenantDomain("carbon.super"); - ctx.setApplicationId("app-1"); - ctx.setFlowType("REGISTRATION"); - ctx.setCallbackUrl("https://callback"); - ctx.setPortalUrl("https://portal"); - - FlowUser user = new FlowUser(); - user.setUserId("user-1"); - user.setUserStoreDomain("PRIMARY"); - user.addClaim("http://wso2.org/claims/email", "u@e.com"); - - Map creds = new HashMap<>(); - creds.put("password", "secret".toCharArray()); - user.setUserCredentials(creds); - - ctx.setFlowUser(user); - ctx.setProperty("p1", "v1"); - return ctx; - } -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml index 035a941187d0..11195f2fd1ce 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/test/resources/testng.xml @@ -31,10 +31,4 @@ - - - - - - diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml index a39e0bff1f8d..23d0a6c989a0 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml @@ -132,7 +132,8 @@ ${project.artifactId} ${project.artifactId} - org.wso2.carbon.identity.flow.inflow.extensions.internal + org.wso2.carbon.identity.flow.inflow.extensions.internal, + org.wso2.carbon.identity.flow.inflow.extensions.util javax.xml.parsers; version="${javax.xml.parsers.import.pkg.version}", @@ -157,10 +158,6 @@ javax.servlet.http; version="${imp.pkg.version.javax.servlet}", org.wso2.carbon.identity.flow.execution.engine; version="${carbon.identity.package.import.version.range}", - org.wso2.carbon.identity.flow.execution.engine.config; - version="${carbon.identity.package.import.version.range}", - org.wso2.carbon.identity.flow.execution.engine.internal; - version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.flow.execution.engine.exception; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.flow.execution.engine.graph; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java index e95ed7571091..a620e514dc9e 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java @@ -18,6 +18,11 @@ package org.wso2.carbon.identity.flow.inflow.extensions; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + /** * Constants for the In-Flow Extension executor pipeline. * @@ -97,4 +102,62 @@ private ActionIDs() { } } } + + /** + * Compile-time default handover policy constants. + * + *

      These constants define which {@code FlowExecutionContext} and {@code FlowUser} + * fields are handed to the action framework during in-flow extension execution. + * {@code "properties"} is intentionally excluded from {@link #INCLUDED_ATTRIBUTES}: + * it is always modifiable via the executor response path (context tree always exposes + * it with MODIFY ops), but must not be forwarded to external services by default.

      + * + *

      When the toml-based dynamic config PR is merged, these constants serve as the + * documented defaults for {@code identity.xml.j2}.

      + */ + public static final class HandoverPolicy { + + private HandoverPolicy() { } + + /** Attribute name for the {@code flowUser} field. When present in + * {@link #INCLUDED_ATTRIBUTES}, {@code fullUserPassthrough} is set to true. */ + public static final String ATTR_FLOW_USER = "flowUser"; + + /** Context identifier; always copied by the filter regardless of config. */ + public static final String ATTR_CONTEXT_IDENTIFIER = "contextIdentifier"; + + /** User-credentials property name; requires per-entry {@code char[]} cloning. */ + public static final String ATTR_USER_CREDENTIALS = "userCredentials"; + + /** + * Top-level {@code FlowExecutionContext} fields that are handed to the action framework. + * Corresponds to the future toml key: + * {@code flow_execution_context.handover.filtering.included_attributes}. + */ + public static final Set INCLUDED_ATTRIBUTES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList( + "contextIdentifier", + "tenantDomain", + "applicationId", + "flowType", + "callbackUrl", + "portalUrl", + "flowUser" // presence sets fullUserPassthrough = true + // "properties" intentionally excluded — sensitive flow-state data + ))); + + /** + * {@code FlowUser} fields that are handed over when full-passthrough is not active. + * Corresponds to the future toml key: + * {@code flow_execution_context.handover.filtering.included_user_attributes}. + */ + public static final Set INCLUDED_USER_ATTRIBUTES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList( + "userId", + "username", + "userStoreDomain", + "claims", + "userCredentials" + ))); + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java index 9c5925cc662b..ff89a11d48dd 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java @@ -34,11 +34,10 @@ import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; import org.wso2.carbon.utils.DiagnosticLog; import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; -import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; -import org.wso2.carbon.identity.flow.execution.engine.config.FlowExecutionContextFilter; 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.inflow.extensions.internal.InFlowExtensionDataHolder; +import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.inflow.extensions.util.InFlowExtensionContextFilterUtil; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; @@ -108,11 +107,10 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE } try { - // Resolve the handover config and hand the action framework only a - // FILTERED copy of the FlowExecutionContext (non-whitelisted fields nulled out). - FlowContextHandoverConfig handoverConfig = FlowExecutionEngineDataHolder.getInstance() - .getFlowContextHandoverConfig(); - FlowExecutionContext filteredContext = FlowExecutionContextFilter.filter(context, handoverConfig); + // Hand the action framework only a FILTERED copy of the FlowExecutionContext + // (non-whitelisted fields nulled out). Policy is sourced from compile-time constants. + FlowExecutionContext filteredContext = InFlowExtensionContextFilterUtil.filter( + context, FlowContextHandoverConfig.defaultPolicy()); FlowContext flowContext = FlowContext.create() .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, filteredContext); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java index 1ff545a58277..f65e85adeb13 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java @@ -18,7 +18,7 @@ package org.wso2.carbon.identity.flow.inflow.extensions.metadata; -import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; import java.util.ArrayList; import java.util.Arrays; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeService.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeService.java index c1d291df971e..8765e89079d8 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeService.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeService.java @@ -18,8 +18,7 @@ package org.wso2.carbon.identity.flow.inflow.extensions.metadata; -import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; -import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; +import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; /** * Public-API entry point for retrieving the controlled In-Flow Extension context tree. @@ -52,6 +51,6 @@ public static InFlowExtensionContextTreeService getInstance() { public InFlowExtensionContextTreeMetadata buildContextTree(String flowType) { return new InFlowExtensionContextTreeBuilder( - FlowExecutionEngineDataHolder.getInstance().getFlowContextHandoverConfig()).build(flowType); + FlowContextHandoverConfig.defaultPolicy()).build(flowType); } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/FlowContextHandoverConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/FlowContextHandoverConfig.java new file mode 100644 index 000000000000..e1ddc736a0ba --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/FlowContextHandoverConfig.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * 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.inflow.extensions.model; + +import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; + +import java.util.Collections; +import java.util.Set; + +/** + * Immutable snapshot of which {@code FlowExecutionContext} attributes are allowed to be + * handed over to the action framework during an in-flow extension execution. + * + *

      The default policy is sourced from compile-time constants in + * {@link InFlowExtensionConstants.HandoverPolicy}. When the dynamic toml-based + * configuration PR is merged, these constants will serve as the documented defaults.

      + */ +public final class FlowContextHandoverConfig { + + private final Set includedAttributes; + private final Set includedUserAttributes; + private final boolean fullUserPassthrough; + + private FlowContextHandoverConfig(Set includedAttributes, + Set includedUserAttributes, + boolean fullUserPassthrough) { + + this.includedAttributes = Collections.unmodifiableSet(includedAttributes); + this.includedUserAttributes = Collections.unmodifiableSet(includedUserAttributes); + this.fullUserPassthrough = fullUserPassthrough; + } + + /** + * Construct a config from explicit attribute sets. + * {@code fullUserPassthrough} is true when {@link InFlowExtensionConstants.HandoverPolicy#ATTR_FLOW_USER} + * is present in {@code attrs}, meaning the entire FlowUser passes through without per-field filtering. + * + * @param attrs top-level context attributes to include; null is treated as empty. + * @param userAttrs FlowUser attributes to include; null is treated as empty. + * @return a new immutable {@link FlowContextHandoverConfig}. + */ + public static FlowContextHandoverConfig of(Set attrs, Set userAttrs) { + + Set resolvedAttrs = (attrs != null) ? attrs : Collections.emptySet(); + Set resolvedUserAttrs = (userAttrs != null) ? userAttrs : Collections.emptySet(); + boolean fullPassthrough = resolvedAttrs.contains( + InFlowExtensionConstants.HandoverPolicy.ATTR_FLOW_USER); + return new FlowContextHandoverConfig(resolvedAttrs, resolvedUserAttrs, fullPassthrough); + } + + /** + * Returns the default handover policy built from compile-time constants defined in + * {@link InFlowExtensionConstants.HandoverPolicy}. + * + *

      This is the factory method called at runtime by the executor and the context tree + * service. To change the effective policy, update the constants in + * {@link InFlowExtensionConstants.HandoverPolicy}.

      + * + * @return a new {@link FlowContextHandoverConfig} reflecting the default policy. + */ + public static FlowContextHandoverConfig defaultPolicy() { + + return of( + InFlowExtensionConstants.HandoverPolicy.INCLUDED_ATTRIBUTES, + InFlowExtensionConstants.HandoverPolicy.INCLUDED_USER_ATTRIBUTES + ); + } + + /** + * Returns the set of top-level {@code FlowExecutionContext} attribute names that may be + * handed over to the action framework. + * + * @return unmodifiable set of allowed attribute names. + */ + public Set getIncludedAttributes() { + + return includedAttributes; + } + + /** + * Returns the set of {@code FlowUser} attribute names that may be handed over when + * {@link #isFullUserPassthrough()} is false. + * + * @return unmodifiable set of allowed user attribute names. + */ + public Set getIncludedUserAttributes() { + + return includedUserAttributes; + } + + /** + * Returns true when {@link InFlowExtensionConstants.HandoverPolicy#ATTR_FLOW_USER} is + * present in {@link #getIncludedAttributes()}, meaning the entire {@code FlowUser} object + * is passed through without per-field inspection. + * + * @return true if the full FlowUser should pass through; false if per-field filtering applies. + */ + public boolean isFullUserPassthrough() { + + return fullUserPassthrough; + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/util/InFlowExtensionContextFilterUtil.java similarity index 68% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilter.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/util/InFlowExtensionContextFilterUtil.java index 09b1f02c2748..1a307c79989b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/config/FlowExecutionContextFilter.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/util/InFlowExtensionContextFilterUtil.java @@ -1,27 +1,29 @@ /* - * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. * * 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 + * 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 + * 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.config; +package org.wso2.carbon.identity.flow.inflow.extensions.util; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; 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.inflow.extensions.InFlowExtensionConstants.HandoverPolicy; +import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; import java.beans.IntrospectionException; import java.beans.Introspector; @@ -36,23 +38,25 @@ /** * Builds a filtered defensive copy of a {@link FlowExecutionContext} containing only the - * attributes permitted by the supplied {@link FlowContextHandoverConfig}. Used at the - * engine boundary by downstream consumers (e.g. the In-Flow Extension executor) to bound - * the data surface visible to external code. + * attributes permitted by the supplied {@link FlowContextHandoverConfig}. + * + *

      This class mirrors the engine's {@code FlowExecutionContextFilter} but lives in + * the inflow module to avoid a cross-bundle dependency on {@code engine.config.*}. When the + * toml-based dynamic configuration PR is merged, this class can be removed and the engine's + * filter used directly.

      * *

      Implementation reflects over the JavaBean property descriptors of - * {@link FlowExecutionContext} and {@link FlowUser}, so any new getter/setter pair added - * to those models is automatically eligible for whitelisting via deployment.toml without - * a filter change. The original context is never mutated; non-permitted attributes are - * left null/empty on the copy.

      + * {@link FlowExecutionContext} and {@link FlowUser}. Descriptors are cached on class load. + * The original context is never mutated; non-permitted attributes are left null/empty on + * the copy.

      * - *

      Map fields receive a defensive shallow {@link HashMap} copy. The {@code userCredentials} - * field receives a per-entry {@code char[]} clone so that the request builder's - * post-extraction {@code Arrays.fill(...,'\0')} wipe zeroes the copy, not the source.

      + *

      Map fields receive a defensive shallow {@link HashMap} copy. The + * {@code userCredentials} field receives a per-entry {@code char[]} clone so that the + * request builder's post-extraction wipe zeroes the copy, not the source.

      */ -public final class FlowExecutionContextFilter { +public final class InFlowExtensionContextFilterUtil { - private static final Log LOG = LogFactory.getLog(FlowExecutionContextFilter.class); + private static final Log LOG = LogFactory.getLog(InFlowExtensionContextFilterUtil.class); private static final Map CONTEXT_PROPERTIES; private static final Map USER_PROPERTIES; @@ -62,7 +66,7 @@ public final class FlowExecutionContextFilter { USER_PROPERTIES = Collections.unmodifiableMap(introspect(FlowUser.class)); } - private FlowExecutionContextFilter() { + private InFlowExtensionContextFilterUtil() { } @@ -70,36 +74,33 @@ private FlowExecutionContextFilter() { * Build a filtered copy of {@code original} according to {@code config}. * * @param original the source context (untouched). - * @param config the handover config; if null, a permissive fallback is used. + * @param config the handover policy. * @return a new {@link FlowExecutionContext} carrying only whitelisted attributes, * or {@code null} if {@code original} is {@code null}. */ - public static FlowExecutionContext filter(FlowExecutionContext original, FlowContextHandoverConfig config) { + public static FlowExecutionContext filter(FlowExecutionContext original, + FlowContextHandoverConfig config) { if (original == null) { return null; } - if (config == null) { - config = FlowContextHandoverConfig.permissive(); - } FlowExecutionContext copy = new FlowExecutionContext(); - // contextIdentifier is engine-internal and always propagated. + // contextIdentifier is engine-internal and always propagated regardless of config. copy.setContextIdentifier(original.getContextIdentifier()); - // Top-level attributes (skip flowUser — handled separately below; - // skip contextIdentifier — already copied). + // Top-level attributes (flowUser and contextIdentifier are handled separately). for (String name : config.getIncludedAttributes()) { - if (FlowContextHandoverConstants.ATTR_FLOW_USER.equals(name) - || FlowContextHandoverConstants.ATTR_CONTEXT_IDENTIFIER.equals(name)) { + if (HandoverPolicy.ATTR_FLOW_USER.equals(name) + || HandoverPolicy.ATTR_CONTEXT_IDENTIFIER.equals(name)) { continue; } copyProperty(CONTEXT_PROPERTIES, name, original, copy); } // User attributes — a fresh non-null FlowUser is always set on the copy so that - // request builders / response processors don't have to null-guard the user. + // request builders / response processors don't need to null-guard the user object. FlowUser dstUser = new FlowUser(); copy.setFlowUser(dstUser); FlowUser srcUser = original.getFlowUser(); @@ -115,24 +116,6 @@ public static FlowExecutionContext filter(FlowExecutionContext original, FlowCon return copy; } - /** - * Known JavaBean property names on {@link FlowExecutionContext} (read+writable). - * Used by {@link FlowContextHandoverConfig#permissive()} to seed an "everything allowed" - * baseline. - */ - public static Set getKnownContextAttributes() { - - return CONTEXT_PROPERTIES.keySet(); - } - - /** - * Known JavaBean property names on {@link FlowUser} (read+writable). - */ - public static Set getKnownUserAttributes() { - - return USER_PROPERTIES.keySet(); - } - private static void copyProperty(Map descriptors, String name, T source, T destination) { @@ -166,7 +149,7 @@ private static Object defensivelyCopy(String name, Object value) { if (!(value instanceof Map)) { return value; } - if (FlowContextHandoverConstants.ATTR_USER_CREDENTIALS.equals(name)) { + if (HandoverPolicy.ATTR_USER_CREDENTIALS.equals(name)) { Map src = (Map) value; Map out = new LinkedHashMap<>(); for (Map.Entry entry : src.entrySet()) { @@ -182,7 +165,8 @@ private static Map introspect(Class beanClass) { Map result = new HashMap<>(); try { - for (PropertyDescriptor pd : Introspector.getBeanInfo(beanClass, Object.class).getPropertyDescriptors()) { + for (PropertyDescriptor pd : + Introspector.getBeanInfo(beanClass, Object.class).getPropertyDescriptors()) { if (pd.getReadMethod() != null && pd.getWriteMethod() != null) { result.put(pd.getName(), pd); } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionTestUtils.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionTestUtils.java index 184c759d5731..df63f77f8cb3 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionTestUtils.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionTestUtils.java @@ -18,28 +18,20 @@ package org.wso2.carbon.identity.flow.inflow.extensions; -import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; -import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConstants; +import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; -import java.lang.reflect.Constructor; import java.util.Arrays; import java.util.HashSet; import java.util.Set; /** - * Test-only helper that builds {@link FlowContextHandoverConfig} instances without - * touching {@code IdentityConfigParser} or triggering {@code FlowExecutionContextFilter}'s - * static initialiser (which fails in the inflow test classpath due to missing - * authentication-framework dependencies). - * - *

      Uses reflection to call the private 3-arg constructor on - * {@link FlowContextHandoverConfig} directly.

      + * Test-only helper that builds {@link FlowContextHandoverConfig} instances using the + * inflow module's own factory method — no reflection, no engine class dependencies. */ public final class InFlowExtensionTestUtils { /** - * Commonly used attribute sets that cover all fields the filter exposes without needing - * {@code FlowExecutionContextFilter.getKnownContextAttributes()}. + * Commonly used attribute sets that cover all fields exposed by the filter. */ public static final Set ALL_CONTEXT_ATTRS = new HashSet<>(Arrays.asList( "tenantDomain", "applicationId", "flowType", "callbackUrl", "portalUrl", @@ -53,9 +45,7 @@ private InFlowExtensionTestUtils() { } /** - * Construct a permissive {@link FlowContextHandoverConfig} without triggering - * {@code FlowContextHandoverConfig.permissive()} (which calls - * {@code FlowExecutionContextFilter}'s static initialiser). + * Construct a permissive {@link FlowContextHandoverConfig} covering all known fields. */ public static FlowContextHandoverConfig permissiveConfig() { @@ -67,15 +57,6 @@ public static FlowContextHandoverConfig permissiveConfig() { */ public static FlowContextHandoverConfig configOf(Set attrs, Set userAttrs) { - try { - Constructor ctor = - FlowContextHandoverConfig.class.getDeclaredConstructor( - Set.class, Set.class, boolean.class); - ctor.setAccessible(true); - boolean fullPassthrough = attrs.contains(FlowContextHandoverConstants.ATTR_FLOW_USER); - return ctor.newInstance(attrs, userAttrs, fullPassthrough); - } catch (Exception e) { - throw new RuntimeException("Cannot construct FlowContextHandoverConfig for test", e); - } + return FlowContextHandoverConfig.of(attrs, userAttrs); } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java index d18ed6661272..1264361abf1b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java @@ -36,9 +36,6 @@ import org.wso2.carbon.identity.flow.execution.engine.Constants; import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; -import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; -import org.wso2.carbon.identity.flow.execution.engine.internal.FlowExecutionEngineDataHolder; -import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionTestUtils; import org.wso2.carbon.identity.flow.inflow.extensions.internal.InFlowExtensionDataHolder; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; @@ -88,11 +85,6 @@ public void setUp() { holderMock = mockStatic(InFlowExtensionDataHolder.class); holderMock.when(InFlowExtensionDataHolder::getInstance).thenReturn(holderInstance); - // Set a permissive handover config directly on the engine DataHolder so tests remain - // focused on status mapping rather than filtering (avoids cross-module mockStatic). - FlowExecutionEngineDataHolder.getInstance() - .setFlowContextHandoverConfig(InFlowExtensionTestUtils.permissiveConfig()); - loggerUtilsMock = mockStatic(LoggerUtils.class); loggerUtilsMock.when(LoggerUtils::isDiagnosticLogsEnabled).thenReturn(false); } @@ -103,8 +95,6 @@ public void tearDown() throws Exception { loggerUtilsMock.close(); holderMock.close(); mocks.close(); - // Reset the handover config so it doesn't leak between test classes. - FlowExecutionEngineDataHolder.getInstance().setFlowContextHandoverConfig(null); } // ========================= getName ========================= diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/FlowContextHandoverConfigTestHelper.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/FlowContextHandoverConfigTestHelper.java index c6a67abdaede..422bf15a7ef6 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/FlowContextHandoverConfigTestHelper.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/FlowContextHandoverConfigTestHelper.java @@ -18,7 +18,7 @@ package org.wso2.carbon.identity.flow.inflow.extensions.metadata; -import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionTestUtils; import java.util.Set; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java index 2649b9ca8823..74c52d9003fd 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java @@ -19,7 +19,7 @@ package org.wso2.carbon.identity.flow.inflow.extensions.metadata; import org.testng.annotations.Test; -import org.wso2.carbon.identity.flow.execution.engine.config.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; import java.util.Arrays; import java.util.HashSet; diff --git a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 index cb9c548c4a57..4510052e7929 100644 --- a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 +++ b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml.j2 @@ -2527,31 +2527,6 @@ - - - - {% if flow_execution_context.handover.filtering.included_attributes is defined %} - {% for attr in flow_execution_context.handover.filtering.included_attributes %} - {{attr}} - {% endfor %} - {% endif %} - - - {% if flow_execution_context.handover.filtering.included_user_attributes is defined %} - {% for attr in flow_execution_context.handover.filtering.included_user_attributes %} - {{attr}} - {% endfor %} - {% endif %} - - - {{external_api_client.http_client.connection_timeout}} From 8238705a8a1d8035cf87b666d42a7a786682facc Mon Sep 17 00:00:00 2001 From: Kumuditha - KD Date: Wed, 20 May 2026 08:32:15 +0530 Subject: [PATCH 31/41] Address review comments --- ...ActionExecutionServiceComponentHolder.java | 1 - .../api/service/ActionManagementService.java | 32 ---------- .../api/service/ActionValidator.java | 37 +++++++++++ .../service/impl/DefaultActionValidator.java | 43 +++++++++++++ .../impl/ActionManagementServiceImpl.java | 63 ++++--------------- .../CacheBackedActionManagementService.java | 7 --- .../internal/util/ActionManagementConfig.java | 6 +- components/entitlement/pom.xml | 2 +- .../pom.xml | 1 - .../flow/execution/engine/Constants.java | 7 ++- .../flow/execution/engine/model/FlowUser.java | 8 --- .../executor/InFlowExtensionExecutor.java | 9 +-- 12 files changed, 107 insertions(+), 109 deletions(-) diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java index 05f2708d7fc9..2f819537109e 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java @@ -18,7 +18,6 @@ package org.wso2.carbon.identity.action.execution.internal.component; -import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; import org.wso2.carbon.identity.rule.evaluation.api.service.RuleEvaluationService; import org.wso2.carbon.identity.secret.mgt.core.SecretManager; diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java index fa370a5ffe27..237a900d8277 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionManagementService.java @@ -129,36 +129,4 @@ Action updateAction(String actionType, String actionId, Action action, String te Action updateActionEndpointAuthentication(String actionType, String actionId, Authentication authentication, String tenantDomain) throws ActionMgtException; - /** - * Check whether the given action name is available (unique) within the specified action type. - * When {@code excludeActionId} is {@code null} the check covers all existing actions of that - * type (creation scenario). When non-null the action with that ID is excluded from the - * uniqueness check (update scenario). - * - * @param actionType Action Type path parameter. - * @param name Action name to check. - * @param excludeActionId Action ID to exclude from the uniqueness check, or {@code null} for - * creation scenarios where no action should be excluded. - * @param tenantDomain Tenant domain. - * @return {@code true} if the name is not already used by another action of the same type - * (i.e., the caller may safely use this name); {@code false} if the name is already taken. - * @throws ActionMgtException If an error occurs while checking name availability. - */ - boolean isActionNameAvailable(String actionType, String name, String excludeActionId, String tenantDomain) - throws ActionMgtException; - - /** - * Convenience overload for creation scenarios — delegates to - * {@link #isActionNameAvailable(String, String, String, String)} with {@code excludeActionId = null}. - * - * @param actionType Action Type path parameter. - * @param name Action name to check. - * @param tenantDomain Tenant domain. - * @return {@code true} if the name is available; {@code false} if already taken. - * @throws ActionMgtException If an error occurs while checking name availability. - */ - default boolean isActionNameAvailable(String actionType, String name, String tenantDomain) - throws ActionMgtException { - return isActionNameAvailable(actionType, name, null, tenantDomain); - } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionValidator.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionValidator.java index 517830f1145b..9ae26e0c37ca 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionValidator.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/ActionValidator.java @@ -20,6 +20,9 @@ import org.wso2.carbon.identity.action.management.api.exception.ActionMgtException; import org.wso2.carbon.identity.action.management.api.model.Action; +import org.wso2.carbon.identity.action.management.api.model.ActionDTO; + +import java.util.List; /** * This interface to the validate action in the Action management service layer. @@ -42,6 +45,22 @@ public interface ActionValidator { void doPreAddActionValidations(Action.ActionTypes actionType, String actionVersion, Action action) throws ActionMgtException; + /** + * Perform pre validations on action model when creating an action, including tenant-scoped checks. + * Overload that receives the pre-fetched list of existing actions for validations such as name uniqueness. + * Default delegates to {@link #doPreAddActionValidations(Action.ActionTypes, String, Action)} for + * backward compatibility with existing implementations. + * + * @param action Action creation model. + * @param existingActionsOfType Existing actions of the same type in the tenant. + * @throws ActionMgtException if action model is invalid. + */ + default void doPreAddActionValidations(Action.ActionTypes actionType, String actionVersion, Action action, + List existingActionsOfType) throws ActionMgtException { + + doPreAddActionValidations(actionType, actionVersion, action); + } + /** * Perform pre validations on action model when updating an existing action. * This is specifically used during HTTP PATCH operation and only validate non-null and non-empty fields. @@ -51,4 +70,22 @@ void doPreAddActionValidations(Action.ActionTypes actionType, String actionVersi */ void doPreUpdateActionValidations(Action.ActionTypes actionType, String actionVersion, Action action) throws ActionMgtException; + + /** + * Perform pre validations on action model when updating an existing action, including tenant-scoped checks. + * Overload that receives the pre-fetched list of existing actions for validations such as name uniqueness. + * Default delegates to {@link #doPreUpdateActionValidations(Action.ActionTypes, String, Action)} for + * backward compatibility. + * + * @param action Action update model. + * @param excludeId Action ID to exclude from uniqueness check. + * @param existingActionsOfType Existing actions of the same type in the tenant. + * @throws ActionMgtException if action model is invalid. + */ + default void doPreUpdateActionValidations(Action.ActionTypes actionType, String actionVersion, Action action, + String excludeId, List existingActionsOfType) + throws ActionMgtException { + + doPreUpdateActionValidations(actionType, actionVersion, action); + } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/impl/DefaultActionValidator.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/impl/DefaultActionValidator.java index 586b5ee7ea32..72a970409117 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/impl/DefaultActionValidator.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/impl/DefaultActionValidator.java @@ -27,6 +27,7 @@ import org.wso2.carbon.identity.action.management.api.exception.ActionMgtException; import org.wso2.carbon.identity.action.management.api.model.Action; import org.wso2.carbon.identity.action.management.api.model.Action.ActionTypes; +import org.wso2.carbon.identity.action.management.api.model.ActionDTO; import org.wso2.carbon.identity.action.management.api.model.Authentication; import org.wso2.carbon.identity.action.management.api.service.ActionValidator; import org.wso2.carbon.identity.action.management.internal.component.ActionMgtServiceComponentHolder; @@ -93,6 +94,16 @@ public void doPreAddActionValidations(Action.ActionTypes actionType, String acti isRulesApplicableForActionVersion(actionVersion, action); } + @Override + public void doPreAddActionValidations(Action.ActionTypes actionType, String actionVersion, Action action, + List existingActionsOfType) throws ActionMgtException { + + doPreAddActionValidations(actionType, actionVersion, action); + if (ActionTypes.IN_FLOW_EXTENSION.equals(actionType)) { + validateActionNameUniqueness(action.getName(), null, existingActionsOfType); + } + } + /** * Perform pre validations on action model when updating an existing action. * This is specifically used during HTTP PATCH operation and only validate non-null and non-empty fields. @@ -122,6 +133,17 @@ public void doPreUpdateActionValidations(Action.ActionTypes actionType, String a isRulesApplicableForActionVersion(actionVersion, action); } + @Override + public void doPreUpdateActionValidations(Action.ActionTypes actionType, String actionVersion, Action action, + String excludeId, List existingActionsOfType) + throws ActionMgtException { + + doPreUpdateActionValidations(actionType, actionVersion, action); + if (action.getName() != null && ActionTypes.IN_FLOW_EXTENSION.equals(actionType)) { + validateActionNameUniqueness(action.getName(), excludeId, existingActionsOfType); + } + } + /** * Perform pre validations on endpoint authentication model. * @@ -249,6 +271,27 @@ private void validateAllowedParameters(List allowedParametersInAction) t } } + /** + * Validate that the action name is unique within the given list of existing actions. + * + * @param name Action name to validate. + * @param excludeId Action ID to exclude (for update). Null for creation. + * @param existing Existing actions of the same type in the tenant. + * @throws ActionMgtClientException If a duplicate name is found. + */ + public void validateActionNameUniqueness(String name, String excludeId, List existing) + throws ActionMgtClientException { + + boolean duplicateExists = existing.stream() + .filter(dto -> excludeId == null || !excludeId.equals(dto.getId())) + .anyMatch(dto -> name.equalsIgnoreCase(dto.getName())); + + if (duplicateExists) { + throw ActionManagementExceptionHandler.handleClientException( + ErrorMessage.ERROR_ACTION_NAME_ALREADY_EXISTS, name); + } + } + /** * Validate whether required fields exist. * diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java index 3d5bab88eefe..42c6d9e9ca43 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java @@ -42,6 +42,7 @@ import org.wso2.carbon.identity.core.util.IdentityUtil; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; @@ -75,14 +76,14 @@ public Action addAction(String actionType, Action action, String tenantDomain) t int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); String resolvedActionType = getActionTypeFromPath(actionType); Action.ActionTypes castedActionType = Action.ActionTypes.valueOf(resolvedActionType); + List existingActions = ActionTypes.IN_FLOW_EXTENSION.equals(castedActionType) + ? DAO_FACADE.getActionsByActionType(resolvedActionType, tenantId) + : Collections.emptyList(); ActionValidatorFactory.getActionValidator(castedActionType).doPreAddActionValidations( - castedActionType, ActionManagementConfig.getInstance().getLatestVersion(castedActionType), action); + castedActionType, ActionManagementConfig.getInstance().getLatestVersion(castedActionType), action, + existingActions); // Check whether the maximum allowed actions per type is reached. validateMaxActionsPerType(resolvedActionType, tenantDomain); - // Check whether the action name is unique within the action type for in-flow extensions. - if (resolvedActionType.equals(ActionTypes.IN_FLOW_EXTENSION.getActionType())) { - validateActionNameUniqueness(resolvedActionType, action.getName(), null, tenantId); - } String generatedActionId = UUID.randomUUID().toString(); ActionDTO creatingActionDTO = buildActionDTOForCreation(resolvedActionType, generatedActionId, action); @@ -164,12 +165,12 @@ public Action updateAction(String actionType, String actionId, Action action, St String resolvedActionType = getActionTypeFromPath(actionType); ActionDTO existingActionDTO = checkIfActionExists(resolvedActionType, actionId, tenantDomain); Action.ActionTypes castedActionType = Action.ActionTypes.valueOf(resolvedActionType); + List existingActions = ActionTypes.IN_FLOW_EXTENSION.equals(castedActionType) + ? DAO_FACADE.getActionsByActionType(resolvedActionType, tenantId) + : Collections.emptyList(); ActionValidatorFactory.getActionValidator(castedActionType).doPreUpdateActionValidations( - castedActionType, resolveActionVersionAtUpdating(action, existingActionDTO), action); - if (action.getName() != null && - Action.ActionTypes.IN_FLOW_EXTENSION.equals(castedActionType)) { - validateActionNameUniqueness(resolvedActionType, action.getName(), actionId, tenantId); - } + castedActionType, resolveActionVersionAtUpdating(action, existingActionDTO), action, actionId, + existingActions); ActionDTO updatingActionDTO = buildActionDTOForUpdate(resolvedActionType, actionId, action); DAO_FACADE.updateAction(updatingActionDTO, existingActionDTO, tenantId); @@ -180,25 +181,6 @@ public Action updateAction(String actionType, String actionId, Action action, St return buildAction(resolvedActionType, updatedActionDTO); } - @Override - public boolean isActionNameAvailable(String actionType, String name, String excludeActionId, - String tenantDomain) throws ActionMgtException { - - if (name == null) { - throw ActionManagementExceptionHandler.handleClientException( - ErrorMessage.ERROR_INVALID_ACTION_REQUEST_FIELD, "Action name"); - } - // actionType is the URL path param (e.g., "inFlowExtension"); resolve to the internal enum name. - String resolvedActionType = getActionTypeFromPath(actionType); - int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); - List actionDTOS = DAO_FACADE.getActionsByActionType(resolvedActionType, tenantId); - // noneMatch returns true when no existing action has this name → name is available. - // When excludeActionId is null (creation), no action is excluded from the check. - return actionDTOS.stream() - .noneMatch(dto -> name.equalsIgnoreCase(dto.getName()) && - (excludeActionId == null || !excludeActionId.equals(dto.getId()))); - } - private String resolveActionVersionAtUpdating(Action updatingAction, ActionDTO existingActionDTO) { String updatingActionVersion = updatingAction.getActionVersion(); @@ -381,29 +363,6 @@ private ActionDTO checkIfActionExists(String actionType, String actionId, String return actionDTO; } - /** - * Validate that the action name is unique within the given action type. - * - * @param actionType Action type. - * @param name Action name to validate. - * @param excludeId Action ID to exclude (for update). Null for creation. - * @param tenantId Tenant ID. - * @throws ActionMgtException If a duplicate name is found. - */ - private void validateActionNameUniqueness(String actionType, String name, String excludeId, int tenantId) - throws ActionMgtException { - - List existingActions = DAO_FACADE.getActionsByActionType(actionType, tenantId); - boolean duplicateExists = existingActions.stream() - .filter(dto -> excludeId == null || !excludeId.equals(dto.getId())) - .anyMatch(dto -> name.equalsIgnoreCase(dto.getName())); - - if (duplicateExists) { - throw ActionManagementExceptionHandler.handleClientException( - ErrorMessage.ERROR_ACTION_NAME_ALREADY_EXISTS, name); - } - } - /** * For action creation operation, builds an `ActionDTO` object based on the provided action type, action ID, and * action model. This method resolves the action type and status, applies necessary transformations, and constructs diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java index 19180ca29400..5e2e0dd0f3b6 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/CacheBackedActionManagementService.java @@ -182,13 +182,6 @@ public Action updateActionEndpointAuthentication(String actionType, String actio return updatedAction; } - @Override - public boolean isActionNameAvailable(String actionType, String name, String excludeActionId, - String tenantDomain) throws ActionMgtException { - - return ACTION_MGT_SERVICE.isActionNameAvailable(actionType, name, excludeActionId, tenantDomain); - } - private void updateCache(Action action, ActionCacheEntry entry, ActionTypeCacheKey cacheKey, String tenantDomain) { if (LOG.isDebugEnabled()) { diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/util/ActionManagementConfig.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/util/ActionManagementConfig.java index fd4acd913c41..3bc9b4e50104 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/util/ActionManagementConfig.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/util/ActionManagementConfig.java @@ -147,7 +147,6 @@ public enum ActionTypeConfig { ), IN_FLOW_EXTENSION( "Actions.Types.InFlowExtension.ActionRequest.ExcludedHeaders.Header", - "Actions.Types.InFlowExtension.ActionRequest.ExcludedParameters.Parameter", "Actions.Types.InFlowExtension.Version.Latest" ); @@ -162,6 +161,11 @@ public enum ActionTypeConfig { this.latestVersionProperty = latestVersionProperty; } + ActionTypeConfig(String excludedHeadersProperty, String latestVersionProperty) { + + this(excludedHeadersProperty, null, latestVersionProperty); + } + public String getExcludedHeadersProperty() { return excludedHeadersProperty; diff --git a/components/entitlement/pom.xml b/components/entitlement/pom.xml index c555f126527f..475a71040ad7 100644 --- a/components/entitlement/pom.xml +++ b/components/entitlement/pom.xml @@ -21,7 +21,7 @@ org.wso2.carbon.identity.framework identity-framework - 7.7.0-SNAPSHOT + 7.11.70-SNAPSHOT ../../pom.xml diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml index c88ff2a0a700..805446fdb916 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/pom.xml @@ -180,7 +180,6 @@ !org.wso2.carbon.identity.flow.execution.internal, org.wso2.carbon.identity.flow.execution.engine.util, org.wso2.carbon.identity.flow.execution.engine, - org.wso2.carbon.identity.flow.execution.engine.internal, org.wso2.carbon.identity.flow.execution.engine.core, org.wso2.carbon.identity.flow.execution.engine.cache, org.wso2.carbon.identity.flow.execution.engine.store, diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java index 9dbf462c7df9..8da24b0446a6 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/Constants.java @@ -163,7 +163,7 @@ public enum ErrorMessages { "Error while loading claim metadata.", "Error occurred loading the claim metadata for tenant: %s."), ERROR_CODE_INFLOW_EXTENSION_ERROR("65033", - "%s", + "In-Flow Extension error.", "%s"), @@ -215,7 +215,10 @@ public enum ErrorMessages { "The provided username: %s does not meet the configured format requirements."), ERROR_CODE_PASSWORD_FORMAT_VALIDATION_FAILED("60016", "Password does not meet the required format.", - "The provided password does not meet the configured format requirements.") + "The provided password does not meet the configured format requirements."), + ERROR_CODE_INFLOW_EXTENSION_FAILURE("60017", + "%s", + "%s") ; private static final String ERROR_PREFIX = "FE"; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java index f8c7ad362b63..feda31a6dfd7 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java @@ -109,14 +109,6 @@ public void addClaims(Map claims) { this.claims.putAll(claims); } - public void setClaims(Map claims) { - - this.claims.clear(); - if (claims != null) { - this.claims.putAll(claims); - } - } - public Object getClaim(String claimUri) { return this.claims.get(claimUri); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java index ff89a11d48dd..e8fd287bc029 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java @@ -35,6 +35,7 @@ import org.wso2.carbon.utils.DiagnosticLog; import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; import org.wso2.carbon.identity.flow.execution.engine.exception.FlowEngineException; +import org.wso2.carbon.identity.flow.execution.engine.util.FlowExecutionEngineUtils; import org.wso2.carbon.identity.flow.inflow.extensions.internal.InFlowExtensionDataHolder; import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; import org.wso2.carbon.identity.flow.inflow.extensions.util.InFlowExtensionContextFilterUtil; @@ -76,7 +77,6 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE String actionId = getMetadataValue(context, InFlowExtensionConstants.ACTION_ID_METADATA_KEY); if (actionId == null || actionId.isEmpty()) { - LOG.warn("No action ID configured for In-Flow Extension executor. Cannot execute."); triggerDiagnosticFailure(null, "In-Flow Extension action execution failed: action ID is not configured."); return buildErrorResponse("Extension is not configured.", @@ -92,14 +92,14 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE ActionExecutorService actionExecutorService = getActionExecutorService(); if (actionExecutorService == null) { - LOG.error("ActionExecutorService is not available. In-Flow Extension cannot execute. actionId: " + actionId); triggerDiagnosticFailure(actionId, "In-Flow Extension action execution failed: ActionExecutorService is unavailable."); - throw new FlowEngineException("ActionExecutorService is not available."); + throw FlowExecutionEngineUtils.handleServerException( + Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_ERROR, + "ActionExecutorService is not available. actionId: " + actionId); } if (!actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)) { - LOG.debug("In-Flow Extension action execution is disabled."); triggerDiagnosticFailure(actionId, "In-Flow Extension action execution failed: action type is disabled."); return buildErrorResponse("Extension execution is disabled.", @@ -260,6 +260,7 @@ private void handleFailedStatus(ExecutorResponse response, ActionExecutionStatus failureInfo.put(InFlowExtensionConstants.FAILURE_DESCRIPTION_KEY, failure.getFailureDescription()); } response.setAdditionalInfo(failureInfo); + response.setErrorCode(Constants.ErrorMessages.ERROR_CODE_INFLOW_EXTENSION_FAILURE.getCode()); response.setErrorMessage(buildUserFacingErrorMessage(failure)); } From 1d6517cdcc354ea47d041ff0370b37ff21019df5 Mon Sep 17 00:00:00 2001 From: Kumuditha - KD Date: Wed, 20 May 2026 09:22:10 +0530 Subject: [PATCH 32/41] Address review comments 2 --- .../FlowExecutionEngineDataHolder.java | 2 - .../flow/execution/engine/model/FlowUser.java | 8 -- .../pom.xml | 20 ++-- .../extensions/InFlowExtensionConstants.java | 15 ++- .../executor/InFlowExtensionExecutor.java | 10 +- .../InFlowExtensionRequestBuilder.java | 39 ++++--- .../InFlowExtensionResponseProcessor.java | 15 +-- .../executor/JWEEncryptionUtil.java | 2 +- .../executor/PathTypeAnnotationUtil.java | 2 +- .../internal/InFlowExtensionDataHolder.java | 2 +- .../InFlowExtensionServiceComponent.java | 14 +-- .../InFlowExtensionActionConverter.java | 22 ++-- ...InFlowExtensionActionDTOModelResolver.java | 20 ++-- .../InFlowExtensionContextTreeBuilder.java | 4 +- .../InFlowExtensionContextTreeMetadata.java | 2 +- .../InFlowExtensionContextTreeNode.java | 2 +- .../InFlowExtensionContextTreeService.java | 4 +- .../flow}/extensions/model/AccessConfig.java | 4 +- .../flow}/extensions/model/ContextPath.java | 2 +- .../flow}/extensions/model/Encryption.java | 2 +- .../model/FlowContextHandoverConfig.java | 4 +- .../model/InFlowExtensionAction.java | 2 +- .../model/InFlowExtensionEvent.java | 2 +- .../model/InFlowExtensionRequest.java | 2 +- .../model/OperationExecutionResult.java | 2 +- .../InFlowExtensionContextFilterUtil.java | 6 +- .../util/InFlowExtensionPathUtil.java | 72 ++++++++++++ .../extensions/InFlowExtensionTestUtils.java | 4 +- .../executor/InFlowExtensionExecutorTest.java | 6 +- .../InFlowExtensionRequestBuilderTest.java | 6 +- .../InFlowExtensionResponseProcessorTest.java | 8 +- .../executor/PathTypeAnnotationUtilTest.java | 2 +- .../FlowContextHandoverConfigTestHelper.java | 6 +- ...InFlowExtensionContextTreeBuilderTest.java | 4 +- .../extensions/model/AccessConfigTest.java | 2 +- .../model/InFlowExtensionEventTest.java | 2 +- .../model/OperationExecutionResultTest.java | 2 +- .../util/InFlowExtensionPathUtilTest.java} | 44 ++++--- .../test/resources/repository/conf/carbon.xml | 0 .../src/test/resources/testng.xml | 43 +++++++ .../executor/HierarchicalPrefixMatcher.java | 110 ------------------ .../src/test/resources/testng.xml | 43 ------- .../flow-orchestration-framework/pom.xml | 2 +- .../pom.xml | 8 +- features/flow-orchestration-framework/pom.xml | 2 +- pom.xml | 4 +- 46 files changed, 270 insertions(+), 309 deletions(-) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions => org.wso2.carbon.identity.flow.extensions}/pom.xml (94%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/InFlowExtensionConstants.java (88%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/executor/InFlowExtensionExecutor.java (98%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/executor/InFlowExtensionRequestBuilder.java (95%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/executor/InFlowExtensionResponseProcessor.java (98%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/executor/JWEEncryptionUtil.java (99%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/executor/PathTypeAnnotationUtil.java (99%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/internal/InFlowExtensionDataHolder.java (97%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/internal/InFlowExtensionServiceComponent.java (92%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/management/InFlowExtensionActionConverter.java (88%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/management/InFlowExtensionActionDTOModelResolver.java (95%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/metadata/InFlowExtensionContextTreeBuilder.java (98%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/metadata/InFlowExtensionContextTreeMetadata.java (97%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/metadata/InFlowExtensionContextTreeNode.java (98%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/metadata/InFlowExtensionContextTreeService.java (92%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/model/AccessConfig.java (97%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/model/ContextPath.java (96%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/model/Encryption.java (96%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/model/FlowContextHandoverConfig.java (97%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/model/InFlowExtensionAction.java (99%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/model/InFlowExtensionEvent.java (98%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/model/InFlowExtensionRequest.java (94%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/model/OperationExecutionResult.java (96%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow}/extensions/util/InFlowExtensionContextFilterUtil.java (96%) create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/util/InFlowExtensionPathUtil.java rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow}/extensions/InFlowExtensionTestUtils.java (93%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow}/extensions/executor/InFlowExtensionExecutorTest.java (99%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow}/extensions/executor/InFlowExtensionRequestBuilderTest.java (99%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow}/extensions/executor/InFlowExtensionResponseProcessorTest.java (99%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow}/extensions/executor/PathTypeAnnotationUtilTest.java (99%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow}/extensions/metadata/FlowContextHandoverConfigTestHelper.java (82%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow}/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java (99%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow}/extensions/model/AccessConfigTest.java (99%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow}/extensions/model/InFlowExtensionEventTest.java (98%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow => org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow}/extensions/model/OperationExecutionResultTest.java (97%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcherTest.java => org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/util/InFlowExtensionPathUtilTest.java} (67%) rename components/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions => org.wso2.carbon.identity.flow.extensions}/src/test/resources/repository/conf/carbon.xml (100%) create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/resources/testng.xml delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcher.java delete mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/testng.xml rename features/flow-orchestration-framework/{org.wso2.carbon.identity.flow.inflow.extensions.server.feature => org.wso2.carbon.identity.flow.extensions.server.feature}/pom.xml (89%) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java index f1775598ea45..adf1b6225b53 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/internal/FlowExecutionEngineDataHolder.java @@ -209,7 +209,6 @@ public void setFederatedAssociationManager(FederatedAssociationManager federated this.federatedAssociationManager = federatedAssociationManager; } - public IdentityEventService getIdentityEventService() { return identityEventService; @@ -220,4 +219,3 @@ public void setIdentityEventService(IdentityEventService identityEventService) { this.identityEventService = identityEventService; } } - diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java index feda31a6dfd7..1583423b015d 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java @@ -160,14 +160,6 @@ public void addFederatedAssociation(String idpName, String idpSubject) { this.federatedAssociations.put(idpName, idpSubject); } - public void setFederatedAssociations(Map federatedAssociations) { - - this.federatedAssociations.clear(); - if (federatedAssociations != null) { - this.federatedAssociations.putAll(federatedAssociations); - } - } - /** * Check whether the user credentials are managed locally. * diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/pom.xml similarity index 94% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/pom.xml index 23d0a6c989a0..aedd9a6ed686 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/pom.xml @@ -25,7 +25,7 @@ 4.0.0 - org.wso2.carbon.identity.flow.inflow.extensions + org.wso2.carbon.identity.flow.extensions bundle WSO2 Carbon - Identity Flow In-Flow Extensions WSO2 flow engine in-flow extensions @@ -132,8 +132,8 @@ ${project.artifactId} ${project.artifactId} - org.wso2.carbon.identity.flow.inflow.extensions.internal, - org.wso2.carbon.identity.flow.inflow.extensions.util + org.wso2.carbon.identity.flow.extensions.internal, + org.wso2.carbon.identity.flow.extensions.util javax.xml.parsers; version="${javax.xml.parsers.import.pkg.version}", @@ -196,12 +196,12 @@ org.slf4j; version="${org.slf4j.imp.pkg.version.range}" - !org.wso2.carbon.identity.flow.inflow.extensions.internal, - org.wso2.carbon.identity.flow.inflow.extensions, - org.wso2.carbon.identity.flow.inflow.extensions.executor, - org.wso2.carbon.identity.flow.inflow.extensions.model, - org.wso2.carbon.identity.flow.inflow.extensions.management, - org.wso2.carbon.identity.flow.inflow.extensions.metadata; + !org.wso2.carbon.identity.flow.extensions.internal, + org.wso2.carbon.identity.flow.extensions, + org.wso2.carbon.identity.flow.extensions.executor, + org.wso2.carbon.identity.flow.extensions.model, + org.wso2.carbon.identity.flow.extensions.management, + org.wso2.carbon.identity.flow.extensions.metadata; version="${carbon.identity.package.export.version}" @@ -240,7 +240,7 @@ ${jacoco.version} - org/wso2/carbon/identity/flow/inflow/extensions/internal/** + org/wso2/carbon/identity/flow/extensions/internal/** diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/InFlowExtensionConstants.java similarity index 88% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/InFlowExtensionConstants.java index a620e514dc9e..5f059a5e37be 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionConstants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/InFlowExtensionConstants.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions; +package org.wso2.carbon.identity.flow.extensions; import java.util.Arrays; import java.util.Collections; @@ -52,9 +52,18 @@ private InFlowExtensionConstants() { public static final String FAILURE_DESCRIPTION_KEY = "failureDescription"; // ---- Context path prefixes ---- - public static final String PROPERTIES_PATH_PREFIX = "/properties/"; - public static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; + public static final String USER_PREFIX = "/user/"; + public static final String USER_ID_PATH = "/user/userId"; + public static final String USER_STORE_DOMAIN_PATH = "/user/userStoreDomain"; + public static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; public static final String USER_CREDENTIALS_PATH_PREFIX = "/user/credentials/"; + public static final String PROPERTIES_PATH_PREFIX = "/properties/"; + public static final String FLOW_PREFIX = "/flow/"; + public static final String FLOW_TENANT_PATH = "/flow/tenantDomain"; + public static final String FLOW_APP_ID_PATH = "/flow/applicationId"; + public static final String FLOW_TYPE_PATH = "/flow/flowType"; + public static final String FLOW_CALLBACK_URL_PATH = "/flow/callbackUrl"; + public static final String FLOW_PORTAL_URL_PATH = "/flow/portalUrl"; // ---- Miscellaneous ---- public static final String ACTION_ID_METADATA_KEY = "actionId"; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionExecutor.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionExecutor.java index e8fd287bc029..28d556f337e9 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionExecutor.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.executor; +package org.wso2.carbon.identity.flow.extensions.executor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -33,12 +33,12 @@ import org.wso2.carbon.identity.flow.execution.engine.Constants; import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; import org.wso2.carbon.utils.DiagnosticLog; -import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; +import org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants; import org.wso2.carbon.identity.flow.execution.engine.exception.FlowEngineException; import org.wso2.carbon.identity.flow.execution.engine.util.FlowExecutionEngineUtils; -import org.wso2.carbon.identity.flow.inflow.extensions.internal.InFlowExtensionDataHolder; -import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; -import org.wso2.carbon.identity.flow.inflow.extensions.util.InFlowExtensionContextFilterUtil; +import org.wso2.carbon.identity.flow.extensions.internal.InFlowExtensionDataHolder; +import org.wso2.carbon.identity.flow.extensions.model.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.extensions.util.InFlowExtensionContextFilterUtil; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java similarity index 95% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilder.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java index 584fdd165626..6dda49c5947c 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java @@ -16,14 +16,14 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.executor; +package org.wso2.carbon.identity.flow.extensions.executor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.identity.action.execution.api.constant.ActionExecutionLogConstants; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionRequestBuilderException; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; -import org.wso2.carbon.identity.flow.inflow.extensions.model.*; +import org.wso2.carbon.identity.flow.extensions.model.*; import org.wso2.carbon.utils.DiagnosticLog; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequest; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequestContext; @@ -42,7 +42,8 @@ import org.wso2.carbon.identity.action.management.api.model.Action; import org.wso2.carbon.identity.core.context.IdentityContext; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; -import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; +import org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants; +import org.wso2.carbon.identity.flow.extensions.util.InFlowExtensionPathUtil; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; @@ -517,7 +518,7 @@ private void addRedirectOperation(List allowedOperations) { private void applyTenant(InFlowExtensionEvent.Builder eventBuilder, FlowExecutionContext context, List expose) { - if (!isLeafExposed(HierarchicalPrefixMatcher.FLOW_TENANT_PATH, expose)) { + if (!isLeafExposed(InFlowExtensionConstants.FLOW_TENANT_PATH, expose)) { return; } @@ -547,7 +548,7 @@ private void applyOrganization(InFlowExtensionEvent.Builder eventBuilder) { private void applyApplication(InFlowExtensionEvent.Builder eventBuilder, FlowExecutionContext context, List expose) { - if (!isLeafExposed(HierarchicalPrefixMatcher.FLOW_APP_ID_PATH, expose)) { + if (!isLeafExposed(InFlowExtensionConstants.FLOW_APP_ID_PATH, expose)) { return; } @@ -561,7 +562,7 @@ private void applyUserAndUserStore(InFlowExtensionEvent.Builder eventBuilder, Fl List expose, AccessConfig accessConfig, String certificatePEM) throws ActionExecutionRequestBuilderException { - if (!isAreaExposed(HierarchicalPrefixMatcher.USER_PREFIX, expose)) { + if (!isAreaExposed(InFlowExtensionConstants.USER_PREFIX, expose)) { return; } @@ -571,7 +572,7 @@ private void applyUserAndUserStore(InFlowExtensionEvent.Builder eventBuilder, Fl } eventBuilder.user(buildUser(flowUser, expose, accessConfig, certificatePEM)); - if (isLeafExposed(HierarchicalPrefixMatcher.USER_STORE_DOMAIN_PATH, expose) + if (isLeafExposed(InFlowExtensionConstants.USER_STORE_DOMAIN_PATH, expose) && flowUser.getUserStoreDomain() != null) { eventBuilder.userStore(new UserStore(flowUser.getUserStoreDomain())); } @@ -580,18 +581,18 @@ private void applyUserAndUserStore(InFlowExtensionEvent.Builder eventBuilder, Fl private void applyFlowMetadata(InFlowExtensionEvent.Builder eventBuilder, FlowExecutionContext context, List expose) { - if (isLeafExposed(HierarchicalPrefixMatcher.FLOW_TYPE_PATH, expose)) { + if (isLeafExposed(InFlowExtensionConstants.FLOW_TYPE_PATH, expose)) { eventBuilder.flowType(context.getFlowType()); } eventBuilder.flowId(context.getContextIdentifier()); - if (isLeafExposed(HierarchicalPrefixMatcher.FLOW_CALLBACK_URL_PATH, expose) + if (isLeafExposed(InFlowExtensionConstants.FLOW_CALLBACK_URL_PATH, expose) && context.getCallbackUrl() != null) { eventBuilder.callbackUrl(context.getCallbackUrl()); } - if (isLeafExposed(HierarchicalPrefixMatcher.FLOW_PORTAL_URL_PATH, expose) + if (isLeafExposed(InFlowExtensionConstants.FLOW_PORTAL_URL_PATH, expose) && context.getPortalUrl() != null) { eventBuilder.portalUrl(context.getPortalUrl()); } @@ -601,21 +602,21 @@ private void applyFlowProperties(InFlowExtensionEvent.Builder eventBuilder, Flow List expose, AccessConfig accessConfig, String certificatePEM) throws ActionExecutionRequestBuilderException { - if (!isAreaExposed(HierarchicalPrefixMatcher.PROPERTIES_PREFIX, expose)) { + if (!isAreaExposed(InFlowExtensionConstants.PROPERTIES_PATH_PREFIX, expose)) { return; } Map properties = context.getProperties(); if (properties != null && !properties.isEmpty()) { eventBuilder.flowProperties( - filterMap(properties, HierarchicalPrefixMatcher.PROPERTIES_PREFIX, + filterMap(properties, InFlowExtensionConstants.PROPERTIES_PATH_PREFIX, expose, accessConfig, certificatePEM)); } } private String resolveUserId(FlowUser flowUser, List expose) { - if (isLeafExposed(HierarchicalPrefixMatcher.USER_ID_PATH, expose)) { + if (isLeafExposed(InFlowExtensionConstants.USER_ID_PATH, expose)) { return flowUser.getUserId(); } return null; @@ -625,7 +626,7 @@ private List buildFilteredClaims(FlowUser flowUser, List expo AccessConfig accessConfig, String certificatePEM) throws ActionExecutionRequestBuilderException { - if (!isAreaExposed(HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX, expose)) { + if (!isAreaExposed(InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX, expose)) { return Collections.emptyList(); } @@ -636,7 +637,7 @@ private List buildFilteredClaims(FlowUser flowUser, List expo List userClaims = new ArrayList<>(); for (Map.Entry claim : claims.entrySet()) { - String claimPath = HierarchicalPrefixMatcher.USER_CLAIMS_PREFIX + claim.getKey(); + String claimPath = InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX + claim.getKey(); if (isLeafExposed(claimPath, expose)) { String claimValue = claim.getValue(); if (claimValue != null && shouldEncrypt(claimPath, accessConfig, certificatePEM)) { @@ -652,7 +653,7 @@ private Map buildFilteredCredentials(FlowUser flowUser, List buildFilteredCredentials(FlowUser flowUser, List filteredCredentials = new HashMap<>(); for (Map.Entry entry : credentials.entrySet()) { - String credentialPath = HierarchicalPrefixMatcher.USER_CREDENTIALS_PREFIX + entry.getKey(); + String credentialPath = InFlowExtensionConstants.USER_CREDENTIALS_PATH_PREFIX + entry.getKey(); if (isLeafExposed(credentialPath, expose)) { char[] credentialValue = entry.getValue(); String plaintext = new String(credentialValue); @@ -773,7 +774,7 @@ private List getSkippedPaths() { */ private boolean isAreaExposed(String areaPrefix, List expose) { - return HierarchicalPrefixMatcher.anyExposedUnder(areaPrefix, expose); + return InFlowExtensionPathUtil.anyExposedUnder(areaPrefix, expose); } /** @@ -782,7 +783,7 @@ private boolean isAreaExposed(String areaPrefix, List expose) { */ private boolean isLeafExposed(String leafPath, List expose) { - return HierarchicalPrefixMatcher.isExposedPath(leafPath, expose); + return InFlowExtensionPathUtil.isExposedPath(leafPath, expose); } /** diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessor.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessor.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessor.java index 1df1c1a4f765..f34db7ff9887 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessor.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.executor; +package org.wso2.carbon.identity.flow.extensions.executor; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; @@ -26,7 +26,7 @@ import org.wso2.carbon.identity.action.execution.api.constant.ActionExecutionLogConstants; import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; -import org.wso2.carbon.identity.flow.inflow.extensions.internal.InFlowExtensionDataHolder; +import org.wso2.carbon.identity.flow.extensions.internal.InFlowExtensionDataHolder; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionResponseProcessorException; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionResponseContext; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionStatus; @@ -48,10 +48,11 @@ import org.wso2.carbon.identity.action.execution.api.model.SuccessStatus; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionResponseProcessor; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; -import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; -import org.wso2.carbon.identity.flow.inflow.extensions.model.AccessConfig; -import org.wso2.carbon.identity.flow.inflow.extensions.model.ContextPath; -import org.wso2.carbon.identity.flow.inflow.extensions.model.OperationExecutionResult; +import org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants; +import org.wso2.carbon.identity.flow.extensions.model.AccessConfig; +import org.wso2.carbon.identity.flow.extensions.util.InFlowExtensionPathUtil; +import org.wso2.carbon.identity.flow.extensions.model.ContextPath; +import org.wso2.carbon.identity.flow.extensions.model.OperationExecutionResult; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.utils.DiagnosticLog; @@ -194,7 +195,7 @@ private OperationExecutionResult processOperation(PerformableOperation operation } // Check if operation is on a read-only area. - if (HierarchicalPrefixMatcher.isReadOnly(path)) { + if (InFlowExtensionPathUtil.isReadOnly(path)) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Path is in a read-only area. Modifications not allowed: " + path); } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/JWEEncryptionUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/JWEEncryptionUtil.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/JWEEncryptionUtil.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/JWEEncryptionUtil.java index d456fbfd6861..f3b390d10543 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/JWEEncryptionUtil.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/JWEEncryptionUtil.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.executor; +package org.wso2.carbon.identity.flow.extensions.executor; import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JOSEException; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/PathTypeAnnotationUtil.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtil.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/PathTypeAnnotationUtil.java index 5d0de0248d4e..f3bf8654fada 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtil.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/PathTypeAnnotationUtil.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.executor; +package org.wso2.carbon.identity.flow.extensions.executor; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionDataHolder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/internal/InFlowExtensionDataHolder.java similarity index 97% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionDataHolder.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/internal/InFlowExtensionDataHolder.java index 9c23f6ce71c8..77ce34a3e158 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionDataHolder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/internal/InFlowExtensionDataHolder.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.internal; +package org.wso2.carbon.identity.flow.extensions.internal; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionServiceComponent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/internal/InFlowExtensionServiceComponent.java similarity index 92% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionServiceComponent.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/internal/InFlowExtensionServiceComponent.java index 65cefb682f07..93a9995e678c 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/internal/InFlowExtensionServiceComponent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/internal/InFlowExtensionServiceComponent.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.internal; +package org.wso2.carbon.identity.flow.extensions.internal; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -37,17 +37,17 @@ import org.wso2.carbon.identity.certificate.management.service.CertificateManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.flow.execution.engine.graph.Executor; -import org.wso2.carbon.identity.flow.inflow.extensions.executor.InFlowExtensionExecutor; -import org.wso2.carbon.identity.flow.inflow.extensions.executor.InFlowExtensionRequestBuilder; -import org.wso2.carbon.identity.flow.inflow.extensions.executor.InFlowExtensionResponseProcessor; -import org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionConverter; -import org.wso2.carbon.identity.flow.inflow.extensions.management.InFlowExtensionActionDTOModelResolver; +import org.wso2.carbon.identity.flow.extensions.executor.InFlowExtensionExecutor; +import org.wso2.carbon.identity.flow.extensions.executor.InFlowExtensionRequestBuilder; +import org.wso2.carbon.identity.flow.extensions.executor.InFlowExtensionResponseProcessor; +import org.wso2.carbon.identity.flow.extensions.management.InFlowExtensionActionConverter; +import org.wso2.carbon.identity.flow.extensions.management.InFlowExtensionActionDTOModelResolver; /** * OSGi declarative services component which registers the In-Flow Extension services. */ @Component( - name = "flow.inflow.extensions.component", + name = "flow.extensions.component", immediate = true) public class InFlowExtensionServiceComponent { diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConverter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/management/InFlowExtensionActionConverter.java similarity index 88% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConverter.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/management/InFlowExtensionActionConverter.java index ab23dd441f2f..2a0427560dcd 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionConverter.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/management/InFlowExtensionActionConverter.java @@ -16,28 +16,28 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.management; +package org.wso2.carbon.identity.flow.extensions.management; import org.wso2.carbon.identity.action.management.api.model.Action; import org.wso2.carbon.identity.action.management.api.model.ActionDTO; import org.wso2.carbon.identity.action.management.api.model.ActionProperty; import org.wso2.carbon.identity.action.management.api.service.ActionConverter; import org.wso2.carbon.identity.certificate.management.model.Certificate; -import org.wso2.carbon.identity.flow.inflow.extensions.model.AccessConfig; -import org.wso2.carbon.identity.flow.inflow.extensions.model.Encryption; -import org.wso2.carbon.identity.flow.inflow.extensions.model.ContextPath; -import org.wso2.carbon.identity.flow.inflow.extensions.model.InFlowExtensionAction; +import org.wso2.carbon.identity.flow.extensions.model.AccessConfig; +import org.wso2.carbon.identity.flow.extensions.model.Encryption; +import org.wso2.carbon.identity.flow.extensions.model.ContextPath; +import org.wso2.carbon.identity.flow.extensions.model.InFlowExtensionAction; import java.util.HashMap; import java.util.List; import java.util.Map; -import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_EXPOSE; -import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_EXPOSE_PREFIX; -import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_MODIFY; -import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_MODIFY_PREFIX; -import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.CERTIFICATE; -import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ICON_URL; +import static org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_EXPOSE; +import static org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_EXPOSE_PREFIX; +import static org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_MODIFY; +import static org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_MODIFY_PREFIX; +import static org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.ActionManagement.CERTIFICATE; +import static org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.ActionManagement.ICON_URL; /** * ActionConverter implementation for In-Flow Extension actions. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionDTOModelResolver.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/management/InFlowExtensionActionDTOModelResolver.java similarity index 95% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionDTOModelResolver.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/management/InFlowExtensionActionDTOModelResolver.java index dd8e6a7609f8..7298003e2ff4 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/management/InFlowExtensionActionDTOModelResolver.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/management/InFlowExtensionActionDTOModelResolver.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.management; +package org.wso2.carbon.identity.flow.extensions.management; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -33,7 +33,7 @@ import org.wso2.carbon.identity.certificate.management.exception.CertificateMgtException; import org.wso2.carbon.identity.certificate.management.model.Certificate; import org.wso2.carbon.identity.certificate.management.service.CertificateManagementService; -import org.wso2.carbon.identity.flow.inflow.extensions.model.ContextPath; +import org.wso2.carbon.identity.flow.extensions.model.ContextPath; import java.io.IOException; import java.util.ArrayList; @@ -44,14 +44,14 @@ import java.util.Set; import static java.util.Collections.emptyList; -import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_EXPOSE; -import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_EXPOSE_PREFIX; -import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_MODIFY; -import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_MODIFY_PREFIX; -import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.CERTIFICATE; -import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.CERTIFICATE_NAME_PREFIX; -import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.ICON_URL; -import static org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants.ActionManagement.MAX_EXPOSE_PATHS; +import static org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_EXPOSE; +import static org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_EXPOSE_PREFIX; +import static org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_MODIFY; +import static org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.ActionManagement.ACCESS_CONFIG_MODIFY_PREFIX; +import static org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.ActionManagement.CERTIFICATE; +import static org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.ActionManagement.CERTIFICATE_NAME_PREFIX; +import static org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.ActionManagement.ICON_URL; +import static org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.ActionManagement.MAX_EXPOSE_PATHS; /** * ActionDTOModelResolver implementation for In-Flow Extension actions. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeBuilder.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeBuilder.java index f65e85adeb13..700fd6e8a317 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeBuilder.java @@ -16,9 +16,9 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.metadata; +package org.wso2.carbon.identity.flow.extensions.metadata; -import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.extensions.model.FlowContextHandoverConfig; import java.util.ArrayList; import java.util.Arrays; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeMetadata.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeMetadata.java similarity index 97% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeMetadata.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeMetadata.java index d26d2ca2780b..4ff222c0b3bb 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeMetadata.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeMetadata.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.metadata; +package org.wso2.carbon.identity.flow.extensions.metadata; import java.util.Collections; import java.util.List; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeNode.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeNode.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeNode.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeNode.java index e00a7fef97e7..b657b6ea8370 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeNode.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeNode.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.metadata; +package org.wso2.carbon.identity.flow.extensions.metadata; import java.util.Collections; import java.util.List; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeService.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeService.java similarity index 92% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeService.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeService.java index 8765e89079d8..92d2f46c5653 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeService.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeService.java @@ -16,9 +16,9 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.metadata; +package org.wso2.carbon.identity.flow.extensions.metadata; -import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.extensions.model.FlowContextHandoverConfig; /** * Public-API entry point for retrieving the controlled In-Flow Extension context tree. diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/AccessConfig.java similarity index 97% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfig.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/AccessConfig.java index d970a3245add..0b34091a3d72 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfig.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/AccessConfig.java @@ -16,9 +16,9 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.model; +package org.wso2.carbon.identity.flow.extensions.model; -import org.wso2.carbon.identity.flow.inflow.extensions.executor.PathTypeAnnotationUtil; +import org.wso2.carbon.identity.flow.extensions.executor.PathTypeAnnotationUtil; import java.util.Collections; import java.util.List; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/ContextPath.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/ContextPath.java similarity index 96% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/ContextPath.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/ContextPath.java index 603ea3c159ec..e9ccb9eec7ef 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/ContextPath.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/ContextPath.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.model; +package org.wso2.carbon.identity.flow.extensions.model; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/Encryption.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/Encryption.java similarity index 96% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/Encryption.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/Encryption.java index a9296bcfb4e3..c93057302c8d 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/Encryption.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/Encryption.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.model; +package org.wso2.carbon.identity.flow.extensions.model; import org.wso2.carbon.identity.certificate.management.model.Certificate; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/FlowContextHandoverConfig.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/FlowContextHandoverConfig.java similarity index 97% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/FlowContextHandoverConfig.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/FlowContextHandoverConfig.java index e1ddc736a0ba..ae1828d2d84f 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/FlowContextHandoverConfig.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/FlowContextHandoverConfig.java @@ -16,9 +16,9 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.model; +package org.wso2.carbon.identity.flow.extensions.model; -import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; +import org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants; import java.util.Collections; import java.util.Set; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionAction.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionAction.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionAction.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionAction.java index 30a297000df3..b6369eda1166 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionAction.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionAction.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.model; +package org.wso2.carbon.identity.flow.extensions.model; import org.wso2.carbon.identity.action.management.api.model.Action; import org.wso2.carbon.identity.action.management.api.model.EndpointConfig; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEvent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEvent.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEvent.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEvent.java index c1f3a1ccabee..8f97a036943b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEvent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEvent.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.model; +package org.wso2.carbon.identity.flow.extensions.model; import org.wso2.carbon.identity.action.execution.api.model.Application; import org.wso2.carbon.identity.action.execution.api.model.Event; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionRequest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionRequest.java similarity index 94% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionRequest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionRequest.java index 0f36f68c0a69..6ff67856fc67 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionRequest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionRequest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.model; +package org.wso2.carbon.identity.flow.extensions.model; import org.wso2.carbon.identity.action.execution.api.model.Request; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResult.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/OperationExecutionResult.java similarity index 96% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResult.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/OperationExecutionResult.java index 73eeb00ac073..6c07b714dd8d 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResult.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/OperationExecutionResult.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.model; +package org.wso2.carbon.identity.flow.extensions.model; import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/util/InFlowExtensionContextFilterUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/util/InFlowExtensionContextFilterUtil.java similarity index 96% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/util/InFlowExtensionContextFilterUtil.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/util/InFlowExtensionContextFilterUtil.java index 1a307c79989b..cc18940b10de 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/util/InFlowExtensionContextFilterUtil.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/util/InFlowExtensionContextFilterUtil.java @@ -16,14 +16,14 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.util; +package org.wso2.carbon.identity.flow.extensions.util; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; 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.inflow.extensions.InFlowExtensionConstants.HandoverPolicy; -import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants.HandoverPolicy; +import org.wso2.carbon.identity.flow.extensions.model.FlowContextHandoverConfig; import java.beans.IntrospectionException; import java.beans.Introspector; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/util/InFlowExtensionPathUtil.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/util/InFlowExtensionPathUtil.java new file mode 100644 index 000000000000..2801917a68ca --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/util/InFlowExtensionPathUtil.java @@ -0,0 +1,72 @@ +/* + * 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.extensions.util; + +import org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants; + +import java.util.List; + +/** + * Path-matching utilities for In-Flow Extension access control. + */ +public final class InFlowExtensionPathUtil { + + private InFlowExtensionPathUtil() { + + } + + /** + * Returns {@code true} if the path is in the read-only {@code /flow/} area. + */ + public static boolean isReadOnly(String path) { + + if (path == null) { + return false; + } + return path.startsWith(InFlowExtensionConstants.FLOW_PREFIX); + } + + /** + * Returns {@code true} if at least one leaf path in {@code leafPaths} starts with + * {@code areaPrefix}. Used as an area-gate before iterating a data block. + */ + public static boolean anyExposedUnder(String areaPrefix, List leafPaths) { + + if (areaPrefix == null || leafPaths == null || leafPaths.isEmpty()) { + return false; + } + for (String path : leafPaths) { + if (path != null && path.startsWith(areaPrefix)) { + return true; + } + } + return false; + } + + /** + * Returns {@code true} if {@code leafPath} is present in {@code leafPaths}. + */ + public static boolean isExposedPath(String leafPath, List leafPaths) { + + if (leafPath == null || leafPaths == null || leafPaths.isEmpty()) { + return false; + } + return leafPaths.contains(leafPath); + } +} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionTestUtils.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/InFlowExtensionTestUtils.java similarity index 93% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionTestUtils.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/InFlowExtensionTestUtils.java index df63f77f8cb3..64bf3b6c8931 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/InFlowExtensionTestUtils.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/InFlowExtensionTestUtils.java @@ -16,9 +16,9 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions; +package org.wso2.carbon.identity.flow.extensions; -import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.extensions.model.FlowContextHandoverConfig; import java.util.Arrays; import java.util.HashSet; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionExecutorTest.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionExecutorTest.java index 1264361abf1b..9bda5d444ff9 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionExecutorTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.executor; +package org.wso2.carbon.identity.flow.extensions.executor; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -34,9 +34,9 @@ import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.flow.execution.engine.Constants; -import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; +import org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants; import org.wso2.carbon.identity.flow.execution.engine.Constants.ExecutorStatus; -import org.wso2.carbon.identity.flow.inflow.extensions.internal.InFlowExtensionDataHolder; +import org.wso2.carbon.identity.flow.extensions.internal.InFlowExtensionDataHolder; import org.wso2.carbon.identity.flow.execution.engine.model.ExecutorResponse; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.mgt.model.ExecutorDTO; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilderTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java index 489687ed4783..21bb9285cb07 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.executor; +package org.wso2.carbon.identity.flow.extensions.executor; import org.mockito.MockedStatic; import org.testng.annotations.AfterMethod; @@ -32,10 +32,10 @@ import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.action.management.api.model.Action; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; -import org.wso2.carbon.identity.flow.inflow.extensions.model.*; +import org.wso2.carbon.identity.flow.extensions.model.*; import org.wso2.carbon.identity.certificate.management.model.Certificate; import org.wso2.carbon.identity.flow.execution.engine.Constants; -import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; +import org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants; 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.mgt.model.NodeConfig; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessorTest.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessorTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessorTest.java index 8a698ac866f5..4156cfff8e93 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessorTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.executor; +package org.wso2.carbon.identity.flow.extensions.executor; import org.mockito.MockedStatic; import org.testng.annotations.AfterMethod; @@ -41,9 +41,9 @@ import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; import org.wso2.carbon.identity.flow.execution.engine.Constants; -import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionConstants; -import org.wso2.carbon.identity.flow.inflow.extensions.internal.InFlowExtensionDataHolder; -import org.wso2.carbon.identity.flow.inflow.extensions.model.ContextPath; +import org.wso2.carbon.identity.flow.extensions.InFlowExtensionConstants; +import org.wso2.carbon.identity.flow.extensions.internal.InFlowExtensionDataHolder; +import org.wso2.carbon.identity.flow.extensions.model.ContextPath; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.flow.execution.engine.model.FlowExecutionContext; import org.wso2.carbon.identity.flow.execution.engine.model.FlowUser; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtilTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/PathTypeAnnotationUtilTest.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtilTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/PathTypeAnnotationUtilTest.java index 409dc05acbfd..eb4f3a06588b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/PathTypeAnnotationUtilTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/PathTypeAnnotationUtilTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.executor; +package org.wso2.carbon.identity.flow.extensions.executor; import org.testng.annotations.Test; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/FlowContextHandoverConfigTestHelper.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/metadata/FlowContextHandoverConfigTestHelper.java similarity index 82% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/FlowContextHandoverConfigTestHelper.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/metadata/FlowContextHandoverConfigTestHelper.java index 422bf15a7ef6..b2b48f3c238f 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/FlowContextHandoverConfigTestHelper.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/metadata/FlowContextHandoverConfigTestHelper.java @@ -16,10 +16,10 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.metadata; +package org.wso2.carbon.identity.flow.extensions.metadata; -import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; -import org.wso2.carbon.identity.flow.inflow.extensions.InFlowExtensionTestUtils; +import org.wso2.carbon.identity.flow.extensions.model.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.extensions.InFlowExtensionTestUtils; import java.util.Set; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java index 74c52d9003fd..69117af4a681 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/metadata/InFlowExtensionContextTreeBuilderTest.java @@ -16,10 +16,10 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.metadata; +package org.wso2.carbon.identity.flow.extensions.metadata; import org.testng.annotations.Test; -import org.wso2.carbon.identity.flow.inflow.extensions.model.FlowContextHandoverConfig; +import org.wso2.carbon.identity.flow.extensions.model.FlowContextHandoverConfig; import java.util.Arrays; import java.util.HashSet; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfigTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/model/AccessConfigTest.java similarity index 99% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfigTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/model/AccessConfigTest.java index 47d30a127cf1..c763c07e29dd 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/AccessConfigTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/model/AccessConfigTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.model; +package org.wso2.carbon.identity.flow.extensions.model; import org.testng.annotations.Test; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEventTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEventTest.java similarity index 98% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEventTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEventTest.java index 97ad067bb1f3..2903656d38d2 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/InFlowExtensionEventTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEventTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.model; +package org.wso2.carbon.identity.flow.extensions.model; import org.testng.annotations.Test; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResultTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/model/OperationExecutionResultTest.java similarity index 97% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResultTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/model/OperationExecutionResultTest.java index 8e1b3f1fa8e2..ba4fa2992d3e 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/model/OperationExecutionResultTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/model/OperationExecutionResultTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.model; +package org.wso2.carbon.identity.flow.extensions.model; import org.testng.annotations.Test; import org.wso2.carbon.identity.action.execution.api.model.Operation; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcherTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/util/InFlowExtensionPathUtilTest.java similarity index 67% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcherTest.java rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/util/InFlowExtensionPathUtilTest.java index ea938ea70f5c..e920a6035d1b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcherTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/util/InFlowExtensionPathUtilTest.java @@ -16,7 +16,7 @@ * under the License. */ -package org.wso2.carbon.identity.flow.inflow.extensions.executor; +package org.wso2.carbon.identity.flow.extensions.util; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -30,9 +30,9 @@ import static org.testng.Assert.assertTrue; /** - * Unit tests for {@link HierarchicalPrefixMatcher}. + * Unit tests for {@link InFlowExtensionPathUtil}. */ -public class HierarchicalPrefixMatcherTest { +public class InFlowExtensionPathUtilTest { // ========================= isReadOnly ========================= @@ -52,7 +52,7 @@ public Object[][] readOnlyPaths() { @Test(dataProvider = "readOnlyPaths") public void testIsReadOnly(String path, boolean expected) { - assertEquals(HierarchicalPrefixMatcher.isReadOnly(path), expected); + assertEquals(InFlowExtensionPathUtil.isReadOnly(path), expected); } // ========================= anyExposedUnder ========================= @@ -63,44 +63,43 @@ public void testAnyExposedUnderMatchesLeafUnderPrefix() { List leafPaths = Arrays.asList( "/user/claims/http://wso2.org/claims/email", "/properties/riskScore"); - assertTrue(HierarchicalPrefixMatcher.anyExposedUnder("/user/claims/", leafPaths)); - assertTrue(HierarchicalPrefixMatcher.anyExposedUnder("/properties/", leafPaths)); + assertTrue(InFlowExtensionPathUtil.anyExposedUnder("/user/claims/", leafPaths)); + assertTrue(InFlowExtensionPathUtil.anyExposedUnder("/properties/", leafPaths)); } @Test public void testAnyExposedUnderNoMatch() { List leafPaths = Arrays.asList("/flow/tenantDomain", "/flow/applicationId"); - assertFalse(HierarchicalPrefixMatcher.anyExposedUnder("/user/claims/", leafPaths)); - assertFalse(HierarchicalPrefixMatcher.anyExposedUnder("/properties/", leafPaths)); + assertFalse(InFlowExtensionPathUtil.anyExposedUnder("/user/claims/", leafPaths)); + assertFalse(InFlowExtensionPathUtil.anyExposedUnder("/properties/", leafPaths)); } @Test public void testAnyExposedUnderNullPrefix() { - assertFalse(HierarchicalPrefixMatcher.anyExposedUnder(null, + assertFalse(InFlowExtensionPathUtil.anyExposedUnder(null, Arrays.asList("/user/claims/email"))); } @Test public void testAnyExposedUnderNullList() { - assertFalse(HierarchicalPrefixMatcher.anyExposedUnder("/user/claims/", null)); + assertFalse(InFlowExtensionPathUtil.anyExposedUnder("/user/claims/", null)); } @Test public void testAnyExposedUnderEmptyList() { - assertFalse(HierarchicalPrefixMatcher.anyExposedUnder("/user/claims/", + assertFalse(InFlowExtensionPathUtil.anyExposedUnder("/user/claims/", Collections.emptyList())); } @Test public void testAnyExposedUnderDoesNotMatchShortPath() { - // A leaf path of "/user/userId" should NOT be matched by area prefix "/user/claims/" List leafPaths = Collections.singletonList("/user/userId"); - assertFalse(HierarchicalPrefixMatcher.anyExposedUnder("/user/claims/", leafPaths)); + assertFalse(InFlowExtensionPathUtil.anyExposedUnder("/user/claims/", leafPaths)); } @Test @@ -109,7 +108,7 @@ public void testAnyExposedUnderMultipleLeafsOneMatches() { List leafPaths = Arrays.asList( "/flow/tenantDomain", "/user/credentials/password"); - assertTrue(HierarchicalPrefixMatcher.anyExposedUnder("/user/credentials/", leafPaths)); + assertTrue(InFlowExtensionPathUtil.anyExposedUnder("/user/credentials/", leafPaths)); } // ========================= isExposedPath ========================= @@ -121,45 +120,44 @@ public void testIsExposedPathExactMatch() { "/user/claims/http://wso2.org/claims/email", "/flow/tenantDomain", "/user/userId"); - assertTrue(HierarchicalPrefixMatcher.isExposedPath( + assertTrue(InFlowExtensionPathUtil.isExposedPath( "/user/claims/http://wso2.org/claims/email", leafPaths)); - assertTrue(HierarchicalPrefixMatcher.isExposedPath("/flow/tenantDomain", leafPaths)); - assertTrue(HierarchicalPrefixMatcher.isExposedPath("/user/userId", leafPaths)); + assertTrue(InFlowExtensionPathUtil.isExposedPath("/flow/tenantDomain", leafPaths)); + assertTrue(InFlowExtensionPathUtil.isExposedPath("/user/userId", leafPaths)); } @Test public void testIsExposedPathNoMatch() { List leafPaths = Arrays.asList("/flow/tenantDomain", "/user/userId"); - assertFalse(HierarchicalPrefixMatcher.isExposedPath( + assertFalse(InFlowExtensionPathUtil.isExposedPath( "/user/claims/http://wso2.org/claims/email", leafPaths)); } @Test public void testIsExposedPathNullPath() { - assertFalse(HierarchicalPrefixMatcher.isExposedPath(null, + assertFalse(InFlowExtensionPathUtil.isExposedPath(null, Arrays.asList("/user/userId"))); } @Test public void testIsExposedPathNullList() { - assertFalse(HierarchicalPrefixMatcher.isExposedPath("/user/userId", null)); + assertFalse(InFlowExtensionPathUtil.isExposedPath("/user/userId", null)); } @Test public void testIsExposedPathEmptyList() { - assertFalse(HierarchicalPrefixMatcher.isExposedPath("/user/userId", + assertFalse(InFlowExtensionPathUtil.isExposedPath("/user/userId", Collections.emptyList())); } @Test public void testIsExposedPathPrefixNotSufficient() { - // An area prefix "/user/claims/" must NOT match when the list only has a leaf under it. List leafPaths = Collections.singletonList("/user/claims/http://wso2.org/claims/email"); - assertFalse(HierarchicalPrefixMatcher.isExposedPath("/user/claims/", leafPaths)); + assertFalse(InFlowExtensionPathUtil.isExposedPath("/user/claims/", leafPaths)); } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/repository/conf/carbon.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/resources/repository/conf/carbon.xml similarity index 100% rename from components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/repository/conf/carbon.xml rename to components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/resources/repository/conf/carbon.xml diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/resources/testng.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/resources/testng.xml new file mode 100644 index 000000000000..dcba439c641d --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/resources/testng.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcher.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcher.java deleted file mode 100644 index e61d75868057..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/main/java/org/wso2/carbon/identity/flow/inflow/extensions/executor/HierarchicalPrefixMatcher.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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.inflow.extensions.executor; - -import java.util.List; - -/** - * Utility class for hierarchical prefix-based path matching used in In-Flow Extension - * access control. - * Provides area-gate checks via {@link #anyExposedUnder(String, List)} and exact - * leaf-path checks via {@link #isExposedPath(String, List)}. - */ -public final class HierarchicalPrefixMatcher { - - // Context area prefix constants - public static final String USER_PREFIX = "/user/"; - public static final String USER_CLAIMS_PREFIX = "/user/claims/"; - public static final String USER_CREDENTIALS_PREFIX = "/user/credentials/"; - public static final String USER_ID_PATH = "/user/userId"; - public static final String USER_STORE_DOMAIN_PATH = "/user/userStoreDomain"; - - public static final String PROPERTIES_PREFIX = "/properties/"; - - public static final String FLOW_PREFIX = "/flow/"; - public static final String FLOW_TENANT_PATH = "/flow/tenantDomain"; - public static final String FLOW_APP_ID_PATH = "/flow/applicationId"; - public static final String FLOW_TYPE_PATH = "/flow/flowType"; - public static final String FLOW_CALLBACK_URL_PATH = "/flow/callbackUrl"; - public static final String FLOW_PORTAL_URL_PATH = "/flow/portalUrl"; - - private HierarchicalPrefixMatcher() { - - } - - /** - * Check if a path is read-only (in /flow/ area). - * - * @param path The path to check - * @return true if the path is in a read-only area - */ - public static boolean isReadOnly(String path) { - - if (path == null) { - return false; - } - return path.startsWith(FLOW_PREFIX); - } - - /** - * Check if any leaf path in the list falls under the given area prefix. - * - *

      Used as an area-gate check before iterating over a data block — e.g., to decide - * whether to include any claims, credentials, or properties in the outgoing request. - * The {@code areaPrefix} always ends with {@code /} (e.g. {@code /user/claims/}). - * The {@code leafPaths} list contains only exact leaf paths with no trailing {@code /}.

      - * - * @param areaPrefix The area prefix to check (must end with {@code /}). - * @param leafPaths The list of exposed leaf paths. - * @return {@code true} if at least one leaf path starts with the area prefix. - */ - public static boolean anyExposedUnder(String areaPrefix, List leafPaths) { - - if (areaPrefix == null || leafPaths == null || leafPaths.isEmpty()) { - return false; - } - for (String path : leafPaths) { - if (path != null && path.startsWith(areaPrefix)) { - return true; - } - } - return false; - } - - /** - * Check if an exact leaf path is present in the expose list. - * - *

      Used for leaf-level filtering — e.g., to decide whether a specific claim URI, - * credential key, or scalar field should be included in the outgoing request. - * The {@code leafPath} has no trailing {@code /}. - * The {@code leafPaths} list contains only exact leaf paths.

      - * - * @param leafPath The exact path to look up. - * @param leafPaths The list of exposed leaf paths. - * @return {@code true} if the path is present in the list. - */ - public static boolean isExposedPath(String leafPath, List leafPaths) { - - if (leafPath == null || leafPaths == null || leafPaths.isEmpty()) { - return false; - } - return leafPaths.contains(leafPath); - } - -} diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/testng.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/testng.xml deleted file mode 100644 index 4c8191946b3e..000000000000 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions/src/test/resources/testng.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/components/flow-orchestration-framework/pom.xml b/components/flow-orchestration-framework/pom.xml index c72d439a9a56..59b279b745d6 100644 --- a/components/flow-orchestration-framework/pom.xml +++ b/components/flow-orchestration-framework/pom.xml @@ -37,7 +37,7 @@ org.wso2.carbon.identity.flow.mgt org.wso2.carbon.identity.flow.execution.engine - org.wso2.carbon.identity.flow.inflow.extensions + org.wso2.carbon.identity.flow.extensions diff --git a/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions.server.feature/pom.xml b/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions.server.feature/pom.xml similarity index 89% rename from features/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions.server.feature/pom.xml rename to features/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions.server.feature/pom.xml index 08505499f447..6ae51aa2c857 100644 --- a/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.inflow.extensions.server.feature/pom.xml +++ b/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions.server.feature/pom.xml @@ -25,7 +25,7 @@ 4.0.0 - org.wso2.carbon.identity.flow.inflow.extensions.server.feature + org.wso2.carbon.identity.flow.extensions.server.feature pom Flow InFlow Extensions Feature https://wso2.com @@ -34,7 +34,7 @@ org.wso2.carbon.identity.framework - org.wso2.carbon.identity.flow.inflow.extensions + org.wso2.carbon.identity.flow.extensions @@ -52,7 +52,7 @@ p2-feature-gen - org.wso2.carbon.identity.flow.inflow.extensions.server + org.wso2.carbon.identity.flow.extensions.server ../../etc/feature.properties @@ -61,7 +61,7 @@ - org.wso2.carbon.identity.framework:org.wso2.carbon.identity.flow.inflow.extensions + org.wso2.carbon.identity.framework:org.wso2.carbon.identity.flow.extensions diff --git a/features/flow-orchestration-framework/pom.xml b/features/flow-orchestration-framework/pom.xml index b62f173b3ee0..99d194ab25ce 100644 --- a/features/flow-orchestration-framework/pom.xml +++ b/features/flow-orchestration-framework/pom.xml @@ -35,7 +35,7 @@ org.wso2.carbon.identity.flow.mgt.server.feature org.wso2.carbon.identity.flow.orchestration.framework.feature org.wso2.carbon.identity.flow.execution.engine.server.feature - org.wso2.carbon.identity.flow.inflow.extensions.server.feature + org.wso2.carbon.identity.flow.extensions.server.feature diff --git a/pom.xml b/pom.xml index e5a675a01b57..f2c3d789f203 100644 --- a/pom.xml +++ b/pom.xml @@ -856,7 +856,7 @@ org.wso2.carbon.identity.framework - org.wso2.carbon.identity.flow.inflow.extensions.server.feature + org.wso2.carbon.identity.flow.extensions.server.feature zip ${project.version} @@ -1949,7 +1949,7 @@ org.wso2.carbon.identity.framework - org.wso2.carbon.identity.flow.inflow.extensions + org.wso2.carbon.identity.flow.extensions ${project.version} From b1b784c87d298f35133cb40880ba3433615e7645 Mon Sep 17 00:00:00 2001 From: Kumuditha - KD Date: Thu, 21 May 2026 08:47:33 +0530 Subject: [PATCH 33/41] Add missing dep --- .../pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.orchestration.framework.feature/pom.xml b/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.orchestration.framework.feature/pom.xml index 6088bb5ed761..819c0369dae8 100644 --- a/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.orchestration.framework.feature/pom.xml +++ b/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.orchestration.framework.feature/pom.xml @@ -43,6 +43,11 @@ org.wso2.carbon.identity.flow.execution.engine.server.feature zip + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.flow.extensions.server.feature + zip + @@ -67,6 +72,9 @@ org.wso2.carbon.identity.framework:org.wso2.carbon.identity.flow.execution.engine.server.feature + + org.wso2.carbon.identity.framework:org.wso2.carbon.identity.flow.extensions.server.feature + From 67856196bf00f76a827cfe12ea14df7660498d4a Mon Sep 17 00:00:00 2001 From: Kumuditha - KD Date: Thu, 21 May 2026 16:55:20 +0530 Subject: [PATCH 34/41] Fix pom --- components/entitlement/pom.xml | 2 +- .../org.wso2.carbon.identity.flow.extensions/pom.xml | 2 +- .../pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/entitlement/pom.xml b/components/entitlement/pom.xml index 475a71040ad7..807c82c73870 100644 --- a/components/entitlement/pom.xml +++ b/components/entitlement/pom.xml @@ -21,7 +21,7 @@ org.wso2.carbon.identity.framework identity-framework - 7.11.70-SNAPSHOT + 7.11.88-SNAPSHOT ../../pom.xml diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/pom.xml b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/pom.xml index aedd9a6ed686..2e254cfff114 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/pom.xml +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/pom.xml @@ -20,7 +20,7 @@ org.wso2.carbon.identity.framework identity-framework - 7.11.70-SNAPSHOT + 7.11.88-SNAPSHOT ../../../pom.xml diff --git a/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions.server.feature/pom.xml b/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions.server.feature/pom.xml index 6ae51aa2c857..e4b80a19c2b0 100644 --- a/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions.server.feature/pom.xml +++ b/features/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions.server.feature/pom.xml @@ -20,7 +20,7 @@ org.wso2.carbon.identity.framework flow-orchestration-framework-feature - 7.11.70-SNAPSHOT + 7.11.88-SNAPSHOT ../pom.xml From 93d2047ab65a936955930df7c62f67be8b610058 Mon Sep 17 00:00:00 2001 From: Kumuditha - KD Date: Thu, 21 May 2026 17:18:56 +0530 Subject: [PATCH 35/41] Fix import --- .../component/ActionExecutionServiceComponentHolder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java index 2f819537109e..05f2708d7fc9 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/component/ActionExecutionServiceComponentHolder.java @@ -18,6 +18,7 @@ package org.wso2.carbon.identity.action.execution.internal.component; +import org.wso2.carbon.identity.action.execution.api.service.ActionExecutorService; import org.wso2.carbon.identity.action.management.api.service.ActionManagementService; import org.wso2.carbon.identity.rule.evaluation.api.service.RuleEvaluationService; import org.wso2.carbon.identity.secret.mgt.core.SecretManager; From ee812b92c03f33611b580892ac00ccabad5bbc96 Mon Sep 17 00:00:00 2001 From: Kumuditha - KD Date: Sat, 23 May 2026 10:45:32 +0530 Subject: [PATCH 36/41] Progress 1 --- .gitignore | 5 +- .../action/execution/api/model/User.java | 33 +++ .../flow/execution/engine/model/FlowUser.java | 8 + .../extensions/InFlowExtensionConstants.java | 2 + .../InFlowExtensionRequestBuilder.java | 151 ++++++----- .../InFlowExtensionResponseProcessor.java | 50 +++- .../model/InFlowExtensionEvent.java | 14 +- .../InFlowExtensionRequestBuilderTest.java | 235 ++++++++++++++++++ .../InFlowExtensionResponseProcessorTest.java | 18 +- 9 files changed, 419 insertions(+), 97 deletions(-) diff --git a/.gitignore b/.gitignore index 2f3ae4e27ced..fe5c06fa3508 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,7 @@ hs_err_pid* # Ignore everything in this directory -target \ No newline at end of file +target + +# Maven versions plugin backup files +*.versionsBackup \ No newline at end of file diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/User.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/User.java index fe37ca5d3513..9daaf08e73e4 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/User.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/User.java @@ -18,6 +18,13 @@ package org.wso2.carbon.identity.action.execution.api.model; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -36,6 +43,7 @@ public class User { private final List groups = new ArrayList<>(); private final List roles = new ArrayList<>(); private Organization organization; + private UserStore userStoreDomain; /** * Represents the user id when the user is shared across sub-organizations. * This field differs from the regular user id ({@link #id}) in scenarios where a user is accessed in the context @@ -76,8 +84,10 @@ public User(Builder builder) { this.userType = builder.userType; this.federatedIdP = builder.federatedIdP; this.accessingOrganization = builder.accessingOrganization; + this.userStoreDomain = builder.userStoreDomain; } + @JsonInclude(JsonInclude.Include.NON_NULL) public String getId() { return id; @@ -108,6 +118,13 @@ public Organization getOrganization() { return organization; } + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonSerialize(using = UserStoreNameSerializer.class) + public UserStore getUserStoreDomain() { + + return userStoreDomain; + } + public String getSharedUserId() { return sharedUserId; @@ -139,6 +156,7 @@ public static class Builder { private final List groups = new ArrayList<>(); private final List roles = new ArrayList<>(); private Organization organization; + private UserStore userStoreDomain; private String sharedUserId; private String userType; private String federatedIdP; @@ -173,6 +191,12 @@ public Builder organization(Organization organization) { return this; } + public Builder userStoreDomain(UserStore userStoreDomain) { + + this.userStoreDomain = userStoreDomain; + return this; + } + public Builder sharedUserId(String sharedUserId) { this.sharedUserId = sharedUserId; @@ -208,4 +232,13 @@ public User build() { return new User(this); } } + + private static class UserStoreNameSerializer extends JsonSerializer { + + @Override + public void serialize(UserStore value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + + gen.writeString(value.getName() != null ? value.getName() : ""); + } + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java index 1583423b015d..7241991cd94a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/FlowUser.java @@ -104,6 +104,14 @@ public Map getClaims() { return claims; } + public void setClaims(Map claims) { + + this.claims.clear(); + if (claims != null) { + this.claims.putAll(claims); + } + } + public void addClaims(Map claims) { this.claims.putAll(claims); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/InFlowExtensionConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/InFlowExtensionConstants.java index 5f059a5e37be..f766dea31fb5 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/InFlowExtensionConstants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/InFlowExtensionConstants.java @@ -56,6 +56,8 @@ private InFlowExtensionConstants() { public static final String USER_ID_PATH = "/user/userId"; public static final String USER_STORE_DOMAIN_PATH = "/user/userStoreDomain"; public static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; + public static final String USER_CLAIMS_SELECTOR_PREFIX = "/user/claims[uri="; + public static final String USER_CLAIMS_SELECTOR_SUFFIX = "]"; public static final String USER_CREDENTIALS_PATH_PREFIX = "/user/credentials/"; public static final String PROPERTIES_PATH_PREFIX = "/properties/"; public static final String FLOW_PREFIX = "/flow/"; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java index 6dda49c5947c..547aef605f2d 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java @@ -229,42 +229,12 @@ private User buildUser(FlowUser flowUser, List expose, userBuilder.userCredentials(filteredCredentials); } - return userBuilder.build(); - } - - /** - * Filter a map to only include entries whose paths are exposed. - * Values for expose paths marked as encrypted are JWE-encrypted. - * - * @param map The source map. - * @param areaPrefix The area prefix (e.g. "/properties/"). - * @param expose The expose prefix list. - * @param accessConfig The access config with encryption flags (may be null). - * @param certificatePEM The certificate PEM for JWE encryption (may be null). - * @param The value type. - * @return A new map containing only exposed entries, with encrypted values where configured. - */ - @SuppressWarnings("unchecked") - private Map filterMap(Map map, String areaPrefix, List expose, - AccessConfig accessConfig, String certificatePEM) - throws ActionExecutionRequestBuilderException { - - if (map == null) { - return Collections.emptyMap(); + if (isLeafExposed(InFlowExtensionConstants.USER_STORE_DOMAIN_PATH, expose)) { + String userStoreDomain = flowUser.getUserStoreDomain(); + userBuilder.userStoreDomain(new UserStore(userStoreDomain != null ? userStoreDomain : "")); } - Map filtered = new HashMap<>(); - for (Map.Entry entry : map.entrySet()) { - String fullPath = areaPrefix + entry.getKey(); - if (isLeafExposed(fullPath, expose)) { - T value = entry.getValue(); - if (value != null && shouldEncrypt(fullPath, accessConfig, certificatePEM)) { - value = (T) encryptValue(String.valueOf(value), certificatePEM); - } - filtered.put(entry.getKey(), value); - } - } - return filtered; + return userBuilder.build(); } private FlowExecutionContext getFlowExecutionContextOrThrow(FlowContext flowContext) @@ -477,7 +447,7 @@ private AllowedModifyExtraction extractAllowedModifyPaths(List modi if (annotation != null) { pathTypeAnnotations.put(cleanPath, annotation); } - cleanPaths.add(cleanPath); + cleanPaths.add(toExternalPath(cleanPath)); } else { LOG.warn("Annotation for path " + cleanPath + " exceeds maximum attribute limit. Skipping path."); @@ -526,6 +496,8 @@ private void applyTenant(InFlowExtensionEvent.Builder eventBuilder, FlowExecutio if (tenantDomain != null) { int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); eventBuilder.tenant(new Tenant(String.valueOf(tenantId), tenantDomain)); + } else { + eventBuilder.tenant(new Tenant("", "")); } } @@ -553,9 +525,7 @@ private void applyApplication(InFlowExtensionEvent.Builder eventBuilder, FlowExe } String appId = context.getApplicationId(); - if (appId != null) { - eventBuilder.application(new Application(appId, null)); - } + eventBuilder.application(new Application(appId != null ? appId : "", null)); } private void applyUserAndUserStore(InFlowExtensionEvent.Builder eventBuilder, FlowExecutionContext context, @@ -572,29 +542,23 @@ private void applyUserAndUserStore(InFlowExtensionEvent.Builder eventBuilder, Fl } eventBuilder.user(buildUser(flowUser, expose, accessConfig, certificatePEM)); - if (isLeafExposed(InFlowExtensionConstants.USER_STORE_DOMAIN_PATH, expose) - && flowUser.getUserStoreDomain() != null) { - eventBuilder.userStore(new UserStore(flowUser.getUserStoreDomain())); - } } private void applyFlowMetadata(InFlowExtensionEvent.Builder eventBuilder, FlowExecutionContext context, List expose) { if (isLeafExposed(InFlowExtensionConstants.FLOW_TYPE_PATH, expose)) { - eventBuilder.flowType(context.getFlowType()); + eventBuilder.flowType(context.getFlowType() != null ? context.getFlowType() : ""); } eventBuilder.flowId(context.getContextIdentifier()); - if (isLeafExposed(InFlowExtensionConstants.FLOW_CALLBACK_URL_PATH, expose) - && context.getCallbackUrl() != null) { - eventBuilder.callbackUrl(context.getCallbackUrl()); + if (isLeafExposed(InFlowExtensionConstants.FLOW_CALLBACK_URL_PATH, expose)) { + eventBuilder.callbackUrl(context.getCallbackUrl() != null ? context.getCallbackUrl() : ""); } - if (isLeafExposed(InFlowExtensionConstants.FLOW_PORTAL_URL_PATH, expose) - && context.getPortalUrl() != null) { - eventBuilder.portalUrl(context.getPortalUrl()); + if (isLeafExposed(InFlowExtensionConstants.FLOW_PORTAL_URL_PATH, expose)) { + eventBuilder.portalUrl(context.getPortalUrl() != null ? context.getPortalUrl() : ""); } } @@ -607,17 +571,28 @@ private void applyFlowProperties(InFlowExtensionEvent.Builder eventBuilder, Flow } Map properties = context.getProperties(); - if (properties != null && !properties.isEmpty()) { - eventBuilder.flowProperties( - filterMap(properties, InFlowExtensionConstants.PROPERTIES_PATH_PREFIX, - expose, accessConfig, certificatePEM)); + Map filteredProperties = new HashMap<>(); + + for (String exposePath : expose) { + if (!exposePath.startsWith(InFlowExtensionConstants.PROPERTIES_PATH_PREFIX)) { + continue; + } + String propKey = exposePath.substring(InFlowExtensionConstants.PROPERTIES_PATH_PREFIX.length()); + Object value = properties != null ? properties.get(propKey) : null; + if (value != null && shouldEncrypt(exposePath, accessConfig, certificatePEM)) { + value = encryptValue(String.valueOf(value), certificatePEM); + } + filteredProperties.put(propKey, value != null ? value : ""); } + + eventBuilder.flowProperties(filteredProperties); } private String resolveUserId(FlowUser flowUser, List expose) { if (isLeafExposed(InFlowExtensionConstants.USER_ID_PATH, expose)) { - return flowUser.getUserId(); + String userId = flowUser.getUserId(); + return userId != null ? userId : ""; } return null; } @@ -631,20 +606,19 @@ private List buildFilteredClaims(FlowUser flowUser, List expo } Map claims = flowUser.getClaims(); - if (claims == null || claims.isEmpty()) { - return Collections.emptyList(); - } - List userClaims = new ArrayList<>(); - for (Map.Entry claim : claims.entrySet()) { - String claimPath = InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX + claim.getKey(); - if (isLeafExposed(claimPath, expose)) { - String claimValue = claim.getValue(); - if (claimValue != null && shouldEncrypt(claimPath, accessConfig, certificatePEM)) { - claimValue = encryptValue(claimValue, certificatePEM); - } - userClaims.add(new UserClaim(claim.getKey(), claimValue)); + + for (String exposePath : expose) { + if (!exposePath.startsWith(InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX)) { + continue; + } + String claimKey = exposePath.substring(InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX.length()); + String claimValue = claims != null ? claims.get(claimKey) : null; + claimValue = claimValue != null ? claimValue : ""; + if (!claimValue.isEmpty() && shouldEncrypt(exposePath, accessConfig, certificatePEM)) { + claimValue = encryptValue(claimValue, certificatePEM); } + userClaims.add(new UserClaim(claimKey, claimValue)); } return userClaims; } @@ -658,20 +632,22 @@ private Map buildFilteredCredentials(FlowUser flowUser, List credentials = flowUser.getUserCredentials(); - if (credentials == null || credentials.isEmpty()) { - return Collections.emptyMap(); - } - Map filteredCredentials = new HashMap<>(); - for (Map.Entry entry : credentials.entrySet()) { - String credentialPath = InFlowExtensionConstants.USER_CREDENTIALS_PATH_PREFIX + entry.getKey(); - if (isLeafExposed(credentialPath, expose)) { - char[] credentialValue = entry.getValue(); - String plaintext = new String(credentialValue); - java.util.Arrays.fill(credentialValue, '\0'); - - filteredCredentials.put(entry.getKey(), - toEncryptedOrPlainCredentialChars(plaintext, credentialPath, accessConfig, certificatePEM)); + + for (String exposePath : expose) { + if (!exposePath.startsWith(InFlowExtensionConstants.USER_CREDENTIALS_PATH_PREFIX)) { + continue; + } + String credKey = exposePath.substring(InFlowExtensionConstants.USER_CREDENTIALS_PATH_PREFIX.length()); + char[] credValue = credentials != null ? credentials.get(credKey) : null; + + if (credValue != null) { + String plaintext = new String(credValue); + java.util.Arrays.fill(credValue, '\0'); + filteredCredentials.put(credKey, + toEncryptedOrPlainCredentialChars(plaintext, exposePath, accessConfig, certificatePEM)); + } else { + filteredCredentials.put(credKey, new char[0]); } } return filteredCredentials; @@ -768,6 +744,23 @@ private List getSkippedPaths() { } } + /** + * Convert an internal path to its external (API-facing) form. + * User-claim paths stored internally as {@code /user/claims/} are emitted + * externally as {@code /user/claims[uri=]}. All other paths are unchanged. + */ + private static String toExternalPath(String internalPath) { + + if (internalPath != null + && internalPath.startsWith(InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX)) { + String claimUri = internalPath.substring( + InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX.length()); + return InFlowExtensionConstants.USER_CLAIMS_SELECTOR_PREFIX + claimUri + + InFlowExtensionConstants.USER_CLAIMS_SELECTOR_SUFFIX; + } + return internalPath; + } + /** * Check if any exposed leaf path falls under the given area prefix. * Used as a gate before iterating a data block (claims, credentials, properties). diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessor.java index f34db7ff9887..2cdfdc80aa17 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessor.java @@ -203,7 +203,7 @@ private OperationExecutionResult processOperation(PerformableOperation operation // Route to appropriate handler based on path prefix. if (path.startsWith(InFlowExtensionConstants.PROPERTIES_PATH_PREFIX)) { return handlePropertyOperation(operation, pathTypeAnnotations, pendingProperties); - } else if (path.startsWith(InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX)) { + } else if (path.startsWith(InFlowExtensionConstants.USER_CLAIMS_SELECTOR_PREFIX)) { return handleUserClaimOperation(operation, pendingClaims, tenantDomain); } else if (path.startsWith(InFlowExtensionConstants.USER_CREDENTIALS_PATH_PREFIX)) { return handleUserCredentialOperation(operation, pendingCredentials); @@ -211,7 +211,8 @@ private OperationExecutionResult processOperation(PerformableOperation operation return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Unknown path prefix. Supported: " + InFlowExtensionConstants.PROPERTIES_PATH_PREFIX + - ", " + InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX + + ", " + InFlowExtensionConstants.USER_CLAIMS_SELECTOR_PREFIX + "" + + InFlowExtensionConstants.USER_CLAIMS_SELECTOR_SUFFIX + ", " + InFlowExtensionConstants.USER_CREDENTIALS_PATH_PREFIX); } @@ -286,8 +287,7 @@ private OperationExecutionResult handlePropertyOperation(PerformableOperation op private OperationExecutionResult handleUserClaimOperation(PerformableOperation operation, Map pendingClaims, String tenantDomain) { - String claimUri = extractNameFromPath(operation.getPath(), - InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX); + String claimUri = extractClaimUriFromPath(operation.getPath()); if (claimUri == null || claimUri.isEmpty()) { return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, @@ -403,6 +403,43 @@ private String extractNameFromPath(String path, String prefix) { return remaining; } + /** + * Extract the claim URI from an external-format claim path. + * Accepts the selector form {@code /user/claims[uri=]}. + * Returns {@code null} if the path is null or does not match the expected format. + */ + private String extractClaimUriFromPath(String path) { + + if (path == null) { + return null; + } + if (path.startsWith(InFlowExtensionConstants.USER_CLAIMS_SELECTOR_PREFIX) + && path.endsWith(InFlowExtensionConstants.USER_CLAIMS_SELECTOR_SUFFIX)) { + return path.substring( + InFlowExtensionConstants.USER_CLAIMS_SELECTOR_PREFIX.length(), + path.length() - InFlowExtensionConstants.USER_CLAIMS_SELECTOR_SUFFIX.length()); + } + return null; + } + + /** + * Normalize an external-format claim path to the internal format for encryption checks. + * Converts {@code /user/claims[uri=]} to {@code /user/claims/}. + * All other paths are returned unchanged. + */ + private static String normalizeToInternalPath(String externalPath) { + + if (externalPath != null + && externalPath.startsWith(InFlowExtensionConstants.USER_CLAIMS_SELECTOR_PREFIX) + && externalPath.endsWith(InFlowExtensionConstants.USER_CLAIMS_SELECTOR_SUFFIX)) { + String claimUri = externalPath.substring( + InFlowExtensionConstants.USER_CLAIMS_SELECTOR_PREFIX.length(), + externalPath.length() - InFlowExtensionConstants.USER_CLAIMS_SELECTOR_SUFFIX.length()); + return InFlowExtensionConstants.USER_CLAIMS_PATH_PREFIX + claimUri; + } + return externalPath; + } + @Override public ActionExecutionStatus processIncompleteResponse(FlowContext flowContext, ActionExecutionResponseContext responseContext) @@ -635,8 +672,9 @@ private PerformableOperation decryptOperationValueIfNeeded(PerformableOperation return operation; } - // Check if this operation path has encryption enabled via modify paths in AccessConfig. - if (!accessConfig.isModifyPathEncrypted(operation.getPath())) { + // Normalize external claim path to internal format before checking encryption flags. + String internalPath = normalizeToInternalPath(operation.getPath()); + if (!accessConfig.isModifyPathEncrypted(internalPath)) { return operation; } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEvent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEvent.java index 8f97a036943b..a896a55c31fb 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEvent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEvent.java @@ -18,6 +18,7 @@ package org.wso2.carbon.identity.flow.extensions.model; +import com.fasterxml.jackson.annotation.JsonInclude; import org.wso2.carbon.identity.action.execution.api.model.Application; import org.wso2.carbon.identity.action.execution.api.model.Event; import org.wso2.carbon.identity.action.execution.api.model.Organization; @@ -60,9 +61,12 @@ private InFlowExtensionEvent(Builder builder) { /** * Get the flow type. + * NON_NULL overrides the ObjectMapper-level NON_EMPTY so that an exposed flowType with no + * context value is serialized as {@code ""} rather than omitted. * * @return The flow type (e.g., "REGISTRATION", "PASSWORD_RESET"). */ + @JsonInclude(JsonInclude.Include.NON_NULL) public String getFlowType() { return flowType; @@ -80,9 +84,12 @@ public String getFlowId() { /** * Get the callback URL for the flow, if exposed. + * NON_NULL overrides the ObjectMapper-level NON_EMPTY so that an exposed callbackUrl with no + * context value is serialized as {@code ""} rather than omitted. * - * @return The callback URL, or null if not exposed. + * @return The callback URL, or {@code null} if not exposed. */ + @JsonInclude(JsonInclude.Include.NON_NULL) public String getCallbackUrl() { return callbackUrl; @@ -90,9 +97,12 @@ public String getCallbackUrl() { /** * Get the portal URL for the flow, if exposed. + * NON_NULL overrides the ObjectMapper-level NON_EMPTY so that an exposed portalUrl with no + * context value is serialized as {@code ""} rather than omitted. * - * @return The portal URL, or null if not exposed. + * @return The portal URL, or {@code null} if not exposed. */ + @JsonInclude(JsonInclude.Include.NON_NULL) public String getPortalUrl() { return portalUrl; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java index 21bb9285cb07..8cf37b4c6217 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java @@ -542,6 +542,241 @@ public void testExposeFilteringSpecificClaim() assertEquals(claims.size(), 1); } + // ========================= Null-to-empty-string: consistent contract ========================= + + @Test + public void testExposedCallbackUrlNullYieldsEmptyString() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + // callbackUrl is NOT set — context returns null. + + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/flow/callbackUrl", false)), null); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertEquals(event.getCallbackUrl(), "", + "Exposed callbackUrl must be '' when context value is null"); + } + + @Test + public void testExposedPortalUrlNullYieldsEmptyString() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + // portalUrl is NOT set — context returns null. + + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/flow/portalUrl", false)), null); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertEquals(event.getPortalUrl(), "", + "Exposed portalUrl must be '' when context value is null"); + } + + @Test + public void testExposedFlowTypeNullYieldsEmptyString() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + // flowType is not set in the minimal context. + + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/flow/flowType", false)), null); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertEquals(event.getFlowType(), "", + "Exposed flowType must be '' when context value is null"); + } + + @Test + public void testExposedTenantDomainNullYieldsEmptyStrings() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + execCtx.setTenantDomain(null); + + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/flow/tenantDomain", false)), null); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event.getTenant(), + "Tenant must be present when /flow/tenantDomain is exposed"); + assertEquals(event.getTenant().getId(), "", + "Tenant id must be '' when tenantDomain is null"); + assertEquals(event.getTenant().getName(), "", + "Tenant name must be '' when tenantDomain is null"); + } + + @Test + public void testExposedApplicationIdNullYieldsEmptyString() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createMinimalFlowExecutionContext(); + // applicationId is not set in the minimal context. + + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/flow/applicationId", false)), null); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event.getApplication(), + "Application must be present when /flow/applicationId is exposed"); + assertEquals(event.getApplication().getId(), "", + "Application id must be '' when applicationId is null"); + } + + @Test + public void testExposedUserIdNullYieldsEmptyString() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + execCtx.getFlowUser().setUserId(null); + + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/user/userId", false)), null); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event.getUser()); + assertEquals(event.getUser().getId(), "", + "User id must be '' when userId is null and path is exposed"); + } + + @Test + public void testExposedUserStoreDomainNullYieldsEmptyString() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + execCtx.getFlowUser().setUserStoreDomain(null); + + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/user/userStoreDomain", false)), null); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event.getUser(), "User must be present when /user/userStoreDomain is exposed"); + assertNotNull(event.getUser().getUserStoreDomain(), + "UserStore must be present when /user/userStoreDomain is exposed"); + assertEquals(event.getUser().getUserStoreDomain().getName(), "", + "UserStore name must be '' when userStoreDomain is null"); + } + + @Test + public void testExposedClaimAbsentFromClaimsMapYieldsEmptyString() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + // Expose a claim URI that is NOT present in the flowUser's claims map. + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/user/claims/http://wso2.org/claims/mobile", false), + new ContextPath("/user/userId", false)), null); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event.getUser()); + List claims = event.getUser().getClaims(); + assertEquals(claims.size(), 1); + org.wso2.carbon.identity.action.execution.api.model.UserClaim mobileClaim = + (org.wso2.carbon.identity.action.execution.api.model.UserClaim) claims.get(0); + assertEquals(mobileClaim.getUri(), "http://wso2.org/claims/mobile"); + assertEquals(mobileClaim.getValue(), "", + "Exposed claim absent from claims map must yield ''"); + } + + @Test + public void testExposedPropertyAbsentFromPropertiesMapYieldsEmptyString() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + // Expose a property key that is NOT present in the context properties map. + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/properties/riskScore", false)), null); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + assertNotNull(event.getFlowProperties()); + assertTrue(event.getFlowProperties().containsKey("riskScore"), + "Exposed property absent from properties map must still appear"); + assertEquals(event.getFlowProperties().get("riskScore"), "", + "Exposed property absent from properties map must yield ''"); + } + + @Test + public void testExposedClaimWithNullValueYieldsEmptyString() + throws ActionExecutionRequestBuilderException { + + FlowExecutionContext execCtx = createFullFlowExecutionContext(); + // Overwrite existing claim with null value. + execCtx.getFlowUser().getClaims().put("http://wso2.org/claims/email", null); + + AccessConfig accessConfig = new AccessConfig(Arrays.asList( + new ContextPath("/user/claims/http://wso2.org/claims/email", false)), null); + + FlowContext flowContext = FlowContext.create() + .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, execCtx); + + ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest( + flowContext, mockReqCtx(accessConfig, null)); + + InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); + List claims = event.getUser().getClaims(); + assertEquals(claims.size(), 1); + org.wso2.carbon.identity.action.execution.api.model.UserClaim emailClaim = + (org.wso2.carbon.identity.action.execution.api.model.UserClaim) claims.get(0); + assertEquals(emailClaim.getValue(), "", + "Claim value must be '' when source value is null"); + } + // ========================= Outbound encryption of properties and inputs ========================= @Test diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessorTest.java index 4156cfff8e93..49576528548e 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessorTest.java @@ -342,7 +342,7 @@ public void testUserClaimReplace() throws ActionExecutionResponseProcessorExcept execCtx.getFlowUser().addClaim("http://wso2.org/claims/email", "old@example.com"); PerformableOperation claimOp = createOperation( - Operation.REPLACE, "/user/claims/http://wso2.org/claims/email", "new@example.com"); + Operation.REPLACE, "/user/claims[uri=http://wso2.org/claims/email]", "new@example.com"); executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); Map pendingClaims = @@ -357,7 +357,7 @@ public void testUserClaimReplaceCreatesNewClaim() throws ActionExecutionResponse FlowExecutionContext execCtx = createFlowExecutionContext(); PerformableOperation claimOp = createOperation( - Operation.REPLACE, "/user/claims/http://wso2.org/claims/country", "US"); + Operation.REPLACE, "/user/claims[uri=http://wso2.org/claims/country]", "US"); executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); Map pendingClaims = @@ -373,7 +373,7 @@ public void testUserClaimReplaceStringifiesValue() throws ActionExecutionRespons // Numeric value should be stringified. PerformableOperation claimOp = createOperation( - Operation.REPLACE, "/user/claims/http://wso2.org/claims/country", 42); + Operation.REPLACE, "/user/claims[uri=http://wso2.org/claims/country]", 42); executeSuccessResponse(execCtx, claimOp, Collections.emptyMap()); Map pendingClaims = @@ -390,7 +390,7 @@ public void testUserClaimReplaceIdentityClaimRejected() // Identity claim should be rejected by the identity-claim prefix guard. PerformableOperation claimOp = createOperation( - Operation.REPLACE, "/user/claims/http://wso2.org/claims/identity/accountLocked", "true"); + Operation.REPLACE, "/user/claims[uri=http://wso2.org/claims/identity/accountLocked]", "true"); ActionExecutionStatus status = executeSuccessResponse( execCtx, claimOp, Collections.emptyMap()); @@ -414,7 +414,7 @@ public void testUserClaimReplaceNonExistentClaimRejected() // Claim not registered in the system should be rejected. PerformableOperation claimOp = createOperation( Operation.REPLACE, - "/user/claims/http://wso2.org/claims/nonexistent", "value"); + "/user/claims[uri=http://wso2.org/claims/nonexistent]", "value"); ActionExecutionStatus status = executeSuccessResponse( execCtx, claimOp, Collections.emptyMap()); @@ -432,7 +432,7 @@ public void testUserClaimReplaceNonLocalDialectRejected() // Non-local dialect claim should be rejected by the local-dialect prefix guard. PerformableOperation claimOp = createOperation( Operation.REPLACE, - "/user/claims/urn:ietf:params:scim:schemas:core:2.0:User:name.givenName", "John"); + "/user/claims[uri=urn:ietf:params:scim:schemas:core:2.0:User:name.givenName]", "John"); ActionExecutionStatus status = executeSuccessResponse( execCtx, claimOp, Collections.emptyMap()); @@ -447,7 +447,7 @@ public void testUserClaimReplaceNullValue() throws ActionExecutionResponseProces FlowExecutionContext execCtx = createFlowExecutionContext(); PerformableOperation claimOp = createOperation( - Operation.REPLACE, "/user/claims/http://wso2.org/claims/email", null); + Operation.REPLACE, "/user/claims[uri=http://wso2.org/claims/email]", null); ActionExecutionStatus status = executeSuccessResponse( execCtx, claimOp, Collections.emptyMap()); @@ -461,7 +461,7 @@ public void testUserClaimReplaceEmptyClaimUri() throws ActionExecutionResponsePr FlowExecutionContext execCtx = createFlowExecutionContext(); - PerformableOperation claimOp = createOperation(Operation.REPLACE, "/user/claims/", "value"); + PerformableOperation claimOp = createOperation(Operation.REPLACE, "/user/claims[uri=]", "value"); ActionExecutionStatus status = executeSuccessResponse( execCtx, claimOp, Collections.emptyMap()); @@ -476,7 +476,7 @@ public void testUserClaimReplaceNoFlowUser() throws ActionExecutionResponseProce execCtx.setFlowUser(null); PerformableOperation claimOp = createOperation( - Operation.REPLACE, "/user/claims/http://wso2.org/claims/email", "test@email.com"); + Operation.REPLACE, "/user/claims[uri=http://wso2.org/claims/email]", "test@email.com"); ActionExecutionStatus status = executeSuccessResponse( execCtx, claimOp, Collections.emptyMap()); From 2398344c4d2224a5f5d76f8d64333937873e38d0 Mon Sep 17 00:00:00 2001 From: Kumuditha - KD Date: Sat, 23 May 2026 11:54:13 +0530 Subject: [PATCH 37/41] Rename flow extensions --- .../execution/api/model/ActionType.java | 2 +- .../impl/ActionExecutorServiceImpl.java | 9 +++- .../internal/util/ActionExecutorConfig.java | 10 ++-- .../action/management/api/model/Action.java | 4 +- .../service/impl/DefaultActionValidator.java | 4 +- .../impl/ActionDTOModelResolverFactory.java | 4 +- .../impl/ActionManagementServiceImpl.java | 6 +-- .../internal/util/ActionManagementConfig.java | 6 +-- .../management/model/ActionTypesTest.java | 4 +- .../executor/InFlowExtensionExecutor.java | 8 +-- .../InFlowExtensionRequestBuilder.java | 10 ++-- .../InFlowExtensionResponseProcessor.java | 10 ++-- .../InFlowExtensionActionConverter.java | 2 +- ...InFlowExtensionActionDTOModelResolver.java | 2 +- .../executor/InFlowExtensionExecutorTest.java | 54 +++++++++---------- .../InFlowExtensionRequestBuilderTest.java | 6 +-- .../InFlowExtensionResponseProcessorTest.java | 2 +- 17 files changed, 74 insertions(+), 69 deletions(-) diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/ActionType.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/ActionType.java index 203edb9ae14d..4408c0a227b9 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/ActionType.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/ActionType.java @@ -30,7 +30,7 @@ public enum ActionType { PRE_UPDATE_PROFILE, AUTHENTICATION, PRE_ISSUE_ID_TOKEN, - IN_FLOW_EXTENSION; + FLOW_EXTENSIONS; public String getDisplayName() { diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/service/impl/ActionExecutorServiceImpl.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/service/impl/ActionExecutorServiceImpl.java index defa1749ae76..1936f0d98e33 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/service/impl/ActionExecutorServiceImpl.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/service/impl/ActionExecutorServiceImpl.java @@ -197,8 +197,13 @@ private Action getActionByActionId(ActionType actionType, String actionId, Strin throws ActionExecutionException { try { - return ActionExecutionServiceComponentHolder.getInstance().getActionManagementService().getActionByActionId( - Action.ActionTypes.valueOf(actionType.name()).getPathParam(), actionId, tenantDomain); + Action action = ActionExecutionServiceComponentHolder.getInstance().getActionManagementService() + .getActionByActionId(Action.ActionTypes.valueOf(actionType.name()).getPathParam(), actionId, + tenantDomain); + if (action == null) { + throw new ActionExecutionRuntimeException("No action found for action Id: " + actionId); + } + return action; } catch (ActionMgtException e) { throw new ActionExecutionException("Error occurred while retrieving action by action Id.", e); } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutorConfig.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutorConfig.java index c93967db4de3..2e59c61d0afa 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutorConfig.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/ActionExecutorConfig.java @@ -87,8 +87,8 @@ public boolean isExecutionForActionTypeEnabled(ActionType actionType) { return isActionTypeEnabled(ActionTypeConfig.PRE_UPDATE_PROFILE.getActionTypeEnableProperty()); case PRE_ISSUE_ID_TOKEN: return isActionTypeEnabled(ActionTypeConfig.PRE_ISSUE_ID_TOKEN.getActionTypeEnableProperty()); - case IN_FLOW_EXTENSION: - return isActionTypeEnabled(ActionTypeConfig.IN_FLOW_EXTENSION.getActionTypeEnableProperty()); + case FLOW_EXTENSIONS: + return isActionTypeEnabled(ActionTypeConfig.FLOW_EXTENSIONS.getActionTypeEnableProperty()); default: return false; } @@ -335,8 +335,8 @@ public String getRetiredUpToVersion(ActionType actionType) { return getVersion(ActionTypeConfig.PRE_UPDATE_PROFILE.getRetiredUpToVersionProperty()); case PRE_ISSUE_ID_TOKEN: return getVersion(ActionTypeConfig.PRE_ISSUE_ID_TOKEN.getRetiredUpToVersionProperty()); - case IN_FLOW_EXTENSION: - return getVersion(ActionTypeConfig.IN_FLOW_EXTENSION.getRetiredUpToVersionProperty()); + case FLOW_EXTENSIONS: + return getVersion(ActionTypeConfig.FLOW_EXTENSIONS.getRetiredUpToVersionProperty()); default: return null; } @@ -422,7 +422,7 @@ private enum ActionTypeConfig { "Actions.Types.PreIssueIdToken.ActionRequest.AllowedHeaders.Header", "Actions.Types.PreIssueIdToken.ActionRequest.AllowedParameters.Parameter", "Actions.Types.PreIssueIdToken.Version.RetiredUpTo"), - IN_FLOW_EXTENSION("Actions.Types.InFlowExtension.Enable", + FLOW_EXTENSIONS("Actions.Types.InFlowExtension.Enable", "Actions.Types.InFlowExtension.ActionRequest.ExcludedHeaders.Header", "Actions.Types.InFlowExtension.ActionRequest.ExcludedParameters.Parameter", "Actions.Types.InFlowExtension.ActionRequest.AllowedHeaders.Header", diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java index b35aea2318ab..c4b276dd529c 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/model/Action.java @@ -76,9 +76,9 @@ public enum ActionTypes { "Pre Issue ID Token", "Configure an extension point for modifying ID token via a custom service.", Category.PRE_POST), - IN_FLOW_EXTENSION( + FLOW_EXTENSIONS( "inFlowExtension", - "IN_FLOW_EXTENSION", + "FLOW_EXTENSIONS", "In-Flow Extension", "Configure an extension point within any flow via a custom service.", Category.IN_FLOW_EXTENSION); diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/impl/DefaultActionValidator.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/impl/DefaultActionValidator.java index 72a970409117..a3a66e592466 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/impl/DefaultActionValidator.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/api/service/impl/DefaultActionValidator.java @@ -99,7 +99,7 @@ public void doPreAddActionValidations(Action.ActionTypes actionType, String acti List existingActionsOfType) throws ActionMgtException { doPreAddActionValidations(actionType, actionVersion, action); - if (ActionTypes.IN_FLOW_EXTENSION.equals(actionType)) { + if (ActionTypes.FLOW_EXTENSIONS.equals(actionType)) { validateActionNameUniqueness(action.getName(), null, existingActionsOfType); } } @@ -139,7 +139,7 @@ public void doPreUpdateActionValidations(Action.ActionTypes actionType, String a throws ActionMgtException { doPreUpdateActionValidations(actionType, actionVersion, action); - if (action.getName() != null && ActionTypes.IN_FLOW_EXTENSION.equals(actionType)) { + if (action.getName() != null && ActionTypes.FLOW_EXTENSIONS.equals(actionType)) { validateActionNameUniqueness(action.getName(), excludeId, existingActionsOfType); } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/dao/impl/ActionDTOModelResolverFactory.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/dao/impl/ActionDTOModelResolverFactory.java index 6fdc4186b4af..671bbde7af0f 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/dao/impl/ActionDTOModelResolverFactory.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/dao/impl/ActionDTOModelResolverFactory.java @@ -46,8 +46,8 @@ public static ActionDTOModelResolver getActionDTOModelResolver(Action.ActionType return actionDTOModelResolvers.get(Action.ActionTypes.PRE_UPDATE_PASSWORD); case PRE_ISSUE_ACCESS_TOKEN: return actionDTOModelResolvers.get(Action.ActionTypes.PRE_ISSUE_ACCESS_TOKEN); - case IN_FLOW_EXTENSION: - return actionDTOModelResolvers.get(Action.ActionTypes.IN_FLOW_EXTENSION); + case FLOW_EXTENSIONS: + return actionDTOModelResolvers.get(Action.ActionTypes.FLOW_EXTENSIONS); default: return null; } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java index 42c6d9e9ca43..c09506681bc9 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/service/impl/ActionManagementServiceImpl.java @@ -76,7 +76,7 @@ public Action addAction(String actionType, Action action, String tenantDomain) t int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); String resolvedActionType = getActionTypeFromPath(actionType); Action.ActionTypes castedActionType = Action.ActionTypes.valueOf(resolvedActionType); - List existingActions = ActionTypes.IN_FLOW_EXTENSION.equals(castedActionType) + List existingActions = ActionTypes.FLOW_EXTENSIONS.equals(castedActionType) ? DAO_FACADE.getActionsByActionType(resolvedActionType, tenantId) : Collections.emptyList(); ActionValidatorFactory.getActionValidator(castedActionType).doPreAddActionValidations( @@ -165,7 +165,7 @@ public Action updateAction(String actionType, String actionId, Action action, St String resolvedActionType = getActionTypeFromPath(actionType); ActionDTO existingActionDTO = checkIfActionExists(resolvedActionType, actionId, tenantDomain); Action.ActionTypes castedActionType = Action.ActionTypes.valueOf(resolvedActionType); - List existingActions = ActionTypes.IN_FLOW_EXTENSION.equals(castedActionType) + List existingActions = ActionTypes.FLOW_EXTENSIONS.equals(castedActionType) ? DAO_FACADE.getActionsByActionType(resolvedActionType, tenantId) : Collections.emptyList(); ActionValidatorFactory.getActionValidator(castedActionType).doPreUpdateActionValidations( @@ -378,7 +378,7 @@ private ActionDTO buildActionDTOForCreation(String actionType, String actionId, Action.ActionTypes resolvedActionType = Action.ActionTypes.valueOf(actionType); // PRE_POST actions start INACTIVE (require explicit activation). - // IN_FLOW and IN_FLOW_EXTENSION category actions (e.g., AUTHENTICATION, IN_FLOW_EXTENSION) + // IN_FLOW and IN_FLOW_EXTENSION category actions (e.g., AUTHENTICATION, FLOW_EXTENSIONS) // start ACTIVE and can be used immediately. Action.Status resolvedStatus = resolvedActionType.getCategory() == Action.ActionTypes.Category.PRE_POST ? Action.Status.INACTIVE : Action.Status.ACTIVE; diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/util/ActionManagementConfig.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/util/ActionManagementConfig.java index 3bc9b4e50104..f77edb4759db 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/util/ActionManagementConfig.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/main/java/org/wso2/carbon/identity/action/management/internal/util/ActionManagementConfig.java @@ -96,9 +96,9 @@ public String getLatestVersion(ActionTypes actionType) throws ActionMgtServerExc case PRE_ISSUE_ID_TOKEN: return getVersion( ActionTypeConfig.PRE_ISSUE_ID_TOKEN.getLatestVersionProperty(), actionType); - case IN_FLOW_EXTENSION: + case FLOW_EXTENSIONS: return getVersion( - ActionTypeConfig.IN_FLOW_EXTENSION.getLatestVersionProperty(), actionType); + ActionTypeConfig.FLOW_EXTENSIONS.getLatestVersionProperty(), actionType); default: throw new ActionMgtServerException("Unsupported action type: " + actionType); } @@ -145,7 +145,7 @@ public enum ActionTypeConfig { "Actions.Types.PreIssueIdToken.ActionRequest.ExcludedParameters.Parameter", "Actions.Types.PreIssueIdToken.Version.Latest" ), - IN_FLOW_EXTENSION( + FLOW_EXTENSIONS( "Actions.Types.InFlowExtension.ActionRequest.ExcludedHeaders.Header", "Actions.Types.InFlowExtension.Version.Latest" ); diff --git a/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java b/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java index a8053186cba7..b896ac8796ac 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.management/src/test/java/org/wso2/carbon/identity/action/management/model/ActionTypesTest.java @@ -53,7 +53,7 @@ public Object[][] actionTypesProvider() { "Pre Issue ID Token", "Configure an extension point for modifying ID token via a custom service.", Action.ActionTypes.Category.PRE_POST}, - {Action.ActionTypes.IN_FLOW_EXTENSION, "inFlowExtension", "IN_FLOW_EXTENSION", + {Action.ActionTypes.FLOW_EXTENSIONS, "inFlowExtension", "FLOW_EXTENSIONS", "In-Flow Extension", "Configure an extension point within any flow via a custom service.", Action.ActionTypes.Category.IN_FLOW_EXTENSION} @@ -82,7 +82,7 @@ public Object[][] filterByCategoryProvider() { Action.ActionTypes.PRE_REGISTRATION, Action.ActionTypes.PRE_ISSUE_ID_TOKEN}}, {Action.ActionTypes.Category.IN_FLOW, new Action.ActionTypes[]{Action.ActionTypes.AUTHENTICATION}}, {Action.ActionTypes.Category.IN_FLOW_EXTENSION, - new Action.ActionTypes[]{Action.ActionTypes.IN_FLOW_EXTENSION}} + new Action.ActionTypes[]{Action.ActionTypes.FLOW_EXTENSIONS}} }; } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionExecutor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionExecutor.java index 28d556f337e9..c3b7e80b62b9 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionExecutor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionExecutor.java @@ -99,7 +99,7 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE "ActionExecutorService is not available. actionId: " + actionId); } - if (!actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)) { + if (!actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)) { triggerDiagnosticFailure(actionId, "In-Flow Extension action execution failed: action type is disabled."); return buildErrorResponse("Extension execution is disabled.", @@ -116,7 +116,7 @@ public ExecutorResponse execute(FlowExecutionContext context) throws FlowEngineE .add(InFlowExtensionConstants.FLOW_EXECUTION_CONTEXT_KEY, filteredContext); ActionExecutionStatus executionStatus = actionExecutorService.execute( - ActionType.IN_FLOW_EXTENSION, actionId, flowContext, context.getTenantDomain()); + ActionType.FLOW_EXTENSIONS, actionId, flowContext, context.getTenantDomain()); ExecutorResponse executionResponse = mapExecutionStatus(executionStatus, flowContext, context); @@ -381,7 +381,7 @@ private void triggerDiagnosticFailure(String diagnosticActionId, String actionId DiagnosticLog.DiagnosticLogBuilder builder = new DiagnosticLog.DiagnosticLogBuilder( InFlowExtensionConstants.Log.COMPONENT_ID, diagnosticActionId) .resultMessage(resultMessage) - .configParam(CONFIG_PARAM_ACTION_TYPE, ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .configParam(CONFIG_PARAM_ACTION_TYPE, ActionType.FLOW_EXTENSIONS.getDisplayName()) .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) .resultStatus(DiagnosticLog.ResultStatus.FAILED); @@ -401,7 +401,7 @@ private void triggerDiagnosticSuccess(String diagnosticActionId, String actionId DiagnosticLog.DiagnosticLogBuilder builder = new DiagnosticLog.DiagnosticLogBuilder( InFlowExtensionConstants.Log.COMPONENT_ID, diagnosticActionId) .resultMessage(resultMessage) - .configParam(CONFIG_PARAM_ACTION_TYPE, ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .configParam(CONFIG_PARAM_ACTION_TYPE, ActionType.FLOW_EXTENSIONS.getDisplayName()) .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) .resultStatus(DiagnosticLog.ResultStatus.SUCCESS); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java index 547aef605f2d..a43c0df37f84 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java @@ -74,7 +74,7 @@ public class InFlowExtensionRequestBuilder implements ActionExecutionRequestBuil @Override public ActionType getSupportedActionType() { - return ActionType.IN_FLOW_EXTENSION; + return ActionType.FLOW_EXTENSIONS; } @Override @@ -330,7 +330,7 @@ private ActionExecutionRequest buildRequestPayload(FlowExecutionContext execCtx, List allowedOperations) { return new ActionExecutionRequest.Builder() - .actionType(ActionType.IN_FLOW_EXTENSION) + .actionType(ActionType.FLOW_EXTENSIONS) .flowId(execCtx.getContextIdentifier()) .event(event) .allowedOperations(allowedOperations) @@ -349,7 +349,7 @@ private void triggerRequestBuildDiagnostic(FlowExecutionContext execCtx, List omittedEncryptedExposeP ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, ActionExecutionLogConstants.ActionIDs.PROCESS_ACTION_REQUEST) .resultMessage("Omitted encrypted expose paths because outbound certificate is not configured.") - .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .configParam("actionType", ActionType.FLOW_EXTENSIONS.getDisplayName()) .configParam(DIAGNOSTIC_REASON, REASON_OMITTED_ENCRYPTED_EXPOSE_PATHS) .inputParam("omittedEncryptedExposePaths", limitForDiagnostic(omittedEncryptedExposePaths)) .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessor.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessor.java index 2cdfdc80aa17..6006aa36006d 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessor.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessor.java @@ -91,7 +91,7 @@ public class InFlowExtensionResponseProcessor implements ActionExecutionResponse @Override public ActionType getSupportedActionType() { - return ActionType.IN_FLOW_EXTENSION; + return ActionType.FLOW_EXTENSIONS; } @Override @@ -504,7 +504,7 @@ private void validateRedirectPresent(String redirectUrl) InFlowExtensionConstants.Log.ActionIDs.PROCESS_RESPONSE) .resultMessage( "INCOMPLETE response from In-Flow Extension is missing a REDIRECT operation.") - .configParam(DIAG_PARAM_ACTION_TYPE, ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .configParam(DIAG_PARAM_ACTION_TYPE, ActionType.FLOW_EXTENSIONS.getDisplayName()) .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) .resultStatus(DiagnosticLog.ResultStatus.FAILED)); } @@ -545,7 +545,7 @@ private void logIncompleteSuccess() { InFlowExtensionConstants.Log.ActionIDs.PROCESS_RESPONSE) .resultMessage( "In-Flow Extension INCOMPLETE response processed. Redirect URL stored in flow context.") - .configParam(DIAG_PARAM_ACTION_TYPE, ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .configParam(DIAG_PARAM_ACTION_TYPE, ActionType.FLOW_EXTENSIONS.getDisplayName()) .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) .resultStatus(DiagnosticLog.ResultStatus.SUCCESS)); } @@ -717,7 +717,7 @@ private PerformableOperation decryptOperationValueIfNeeded(PerformableOperation InFlowExtensionConstants.Log.COMPONENT_ID, InFlowExtensionConstants.Log.ActionIDs.PROCESS_RESPONSE) .resultMessage("Failed to decrypt inbound JWE value for modify path.") - .configParam("actionType", ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .configParam("actionType", ActionType.FLOW_EXTENSIONS.getDisplayName()) .inputParam("path", operation.getPath()) .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) .resultStatus(DiagnosticLog.ResultStatus.FAILED)); @@ -740,7 +740,7 @@ private void emitEncryptionContractViolation(String path, String reason) { InFlowExtensionConstants.Log.COMPONENT_ID, InFlowExtensionConstants.Log.ActionIDs.PROCESS_RESPONSE) .resultMessage(reason) - .configParam(DIAG_PARAM_ACTION_TYPE, ActionType.IN_FLOW_EXTENSION.getDisplayName()) + .configParam(DIAG_PARAM_ACTION_TYPE, ActionType.FLOW_EXTENSIONS.getDisplayName()) .inputParam("path", path) .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) .resultStatus(DiagnosticLog.ResultStatus.FAILED)); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/management/InFlowExtensionActionConverter.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/management/InFlowExtensionActionConverter.java index 2a0427560dcd..26e7db244e07 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/management/InFlowExtensionActionConverter.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/management/InFlowExtensionActionConverter.java @@ -52,7 +52,7 @@ public class InFlowExtensionActionConverter implements ActionConverter { @Override public Action.ActionTypes getSupportedActionType() { - return Action.ActionTypes.IN_FLOW_EXTENSION; + return Action.ActionTypes.FLOW_EXTENSIONS; } /** diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/management/InFlowExtensionActionDTOModelResolver.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/management/InFlowExtensionActionDTOModelResolver.java index 7298003e2ff4..2519b1edba66 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/management/InFlowExtensionActionDTOModelResolver.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/management/InFlowExtensionActionDTOModelResolver.java @@ -85,7 +85,7 @@ public InFlowExtensionActionDTOModelResolver(CertificateManagementService certif @Override public Action.ActionTypes getSupportedActionType() { - return Action.ActionTypes.IN_FLOW_EXTENSION; + return Action.ActionTypes.FLOW_EXTENSIONS; } @Override diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionExecutorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionExecutorTest.java index 9bda5d444ff9..bafe798517b3 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionExecutorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionExecutorTest.java @@ -157,7 +157,7 @@ public void testExecuteDisabledExecution() throws Exception { metadata.put("actionId", "test-action-001"); FlowExecutionContext context = createContextWithMetadata(metadata); - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(false); + when(actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)).thenReturn(false); ExecutorResponse response = executor.execute(context); @@ -176,12 +176,12 @@ public void testExecuteSuccess() throws Exception { metadata.put("actionId", "test-action-001"); FlowExecutionContext context = createContextWithMetadata(metadata); - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)).thenReturn(true); ActionExecutionStatus successStatus = mock(ActionExecutionStatus.class); when(successStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.SUCCESS); when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + eq(ActionType.FLOW_EXTENSIONS), eq("test-action-001"), any(FlowContext.class), eq("carbon.super"))) .thenReturn(successStatus); @@ -200,14 +200,14 @@ public void testExecuteFailed() throws Exception { metadata.put("actionId", "test-action-001"); FlowExecutionContext context = createContextWithMetadata(metadata); - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)).thenReturn(true); Failure failure = new Failure("risk_detected", "Risk score exceeds threshold"); ActionExecutionStatus failedStatus = mock(ActionExecutionStatus.class); when(failedStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.FAILED); when(failedStatus.getResponse()).thenReturn(failure); when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + eq(ActionType.FLOW_EXTENSIONS), eq("test-action-001"), any(FlowContext.class), eq("carbon.super"))) .thenReturn(failedStatus); @@ -228,14 +228,14 @@ public void testExecuteFailedNoDescription() throws Exception { metadata.put("actionId", "test-action-001"); FlowExecutionContext context = createContextWithMetadata(metadata); - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)).thenReturn(true); Failure failure = new Failure("risk_detected", null); ActionExecutionStatus failedStatus = mock(ActionExecutionStatus.class); when(failedStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.FAILED); when(failedStatus.getResponse()).thenReturn(failure); when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + eq(ActionType.FLOW_EXTENSIONS), eq("test-action-001"), any(FlowContext.class), eq("carbon.super"))) .thenReturn(failedStatus); @@ -254,14 +254,14 @@ public void testExecuteFailedBothNull() throws Exception { metadata.put("actionId", "test-action-001"); FlowExecutionContext context = createContextWithMetadata(metadata); - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)).thenReturn(true); Failure failure = new Failure(null, null); ActionExecutionStatus failedStatus = mock(ActionExecutionStatus.class); when(failedStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.FAILED); when(failedStatus.getResponse()).thenReturn(failure); when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + eq(ActionType.FLOW_EXTENSIONS), eq("test-action-001"), any(FlowContext.class), eq("carbon.super"))) .thenReturn(failedStatus); @@ -282,14 +282,14 @@ public void testExecuteError() throws Exception { metadata.put("actionId", "test-action-001"); FlowExecutionContext context = createContextWithMetadata(metadata); - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)).thenReturn(true); Error error = new Error("internal_error", "DB connection failed"); ActionExecutionStatus errorStatus = mock(ActionExecutionStatus.class); when(errorStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.ERROR); when(errorStatus.getResponse()).thenReturn(error); when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + eq(ActionType.FLOW_EXTENSIONS), eq("test-action-001"), any(FlowContext.class), eq("carbon.super"))) .thenReturn(errorStatus); @@ -311,14 +311,14 @@ public void testExecuteErrorNoDescription() throws Exception { metadata.put("actionId", "test-action-001"); FlowExecutionContext context = createContextWithMetadata(metadata); - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)).thenReturn(true); Error error = new Error("internal_error", null); ActionExecutionStatus errorStatus = mock(ActionExecutionStatus.class); when(errorStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.ERROR); when(errorStatus.getResponse()).thenReturn(error); when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + eq(ActionType.FLOW_EXTENSIONS), eq("test-action-001"), any(FlowContext.class), eq("carbon.super"))) .thenReturn(errorStatus); @@ -339,14 +339,14 @@ public void testExecuteErrorBothNull() throws Exception { metadata.put("actionId", "test-action-001"); FlowExecutionContext context = createContextWithMetadata(metadata); - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)).thenReturn(true); Error error = new Error(null, null); ActionExecutionStatus errorStatus = mock(ActionExecutionStatus.class); when(errorStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.ERROR); when(errorStatus.getResponse()).thenReturn(error); when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + eq(ActionType.FLOW_EXTENSIONS), eq("test-action-001"), any(FlowContext.class), eq("carbon.super"))) .thenReturn(errorStatus); @@ -373,12 +373,12 @@ public void testExecuteIncompleteWithoutRedirectUrlReturnsError() throws Excepti metadata.put("actionId", "test-action-001"); FlowExecutionContext context = createContextWithMetadata(metadata); - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)).thenReturn(true); ActionExecutionStatus incompleteStatus = mock(ActionExecutionStatus.class); when(incompleteStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.INCOMPLETE); when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + eq(ActionType.FLOW_EXTENSIONS), eq("test-action-001"), any(FlowContext.class), eq("carbon.super"))) .thenReturn(incompleteStatus); @@ -399,7 +399,7 @@ public void testExecuteIncompleteWithRedirectUrlReturnsExternalRedirection() thr // Set a context identifier so the OTFI collision-guard has something to compare against. context.setContextIdentifier("original-flow-id"); - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)).thenReturn(true); ActionExecutionStatus incompleteStatus = mock(ActionExecutionStatus.class); when(incompleteStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.INCOMPLETE); @@ -407,7 +407,7 @@ public void testExecuteIncompleteWithRedirectUrlReturnsExternalRedirection() thr // Simulate the response processor stashing the redirect URL into the FlowContext // during the actionExecutorService.execute call. when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + eq(ActionType.FLOW_EXTENSIONS), eq("test-action-001"), any(FlowContext.class), eq("carbon.super"))) .thenAnswer(invocation -> { FlowContext fc = invocation.getArgument(2); @@ -449,13 +449,13 @@ public void testExecuteIncompleteRedirectAppendsFlowIdWithAmpersandWhenUrlHasQue FlowExecutionContext context = createContextWithMetadata(metadata); context.setContextIdentifier("original-flow-id"); - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)).thenReturn(true); ActionExecutionStatus incompleteStatus = mock(ActionExecutionStatus.class); when(incompleteStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.INCOMPLETE); when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + eq(ActionType.FLOW_EXTENSIONS), eq("test-action-001"), any(FlowContext.class), eq("carbon.super"))) .thenAnswer(invocation -> { FlowContext fc = invocation.getArgument(2); @@ -483,13 +483,13 @@ public void testExecuteIncompleteRedirectEmptyUrlReturnsError() throws Exception metadata.put("actionId", "test-action-001"); FlowExecutionContext context = createContextWithMetadata(metadata); - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)).thenReturn(true); ActionExecutionStatus incompleteStatus = mock(ActionExecutionStatus.class); when(incompleteStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.INCOMPLETE); when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + eq(ActionType.FLOW_EXTENSIONS), eq("test-action-001"), any(FlowContext.class), eq("carbon.super"))) .thenAnswer(invocation -> { FlowContext fc = invocation.getArgument(2); @@ -514,9 +514,9 @@ public void testExecuteNullStatus() throws Exception { metadata.put("actionId", "test-action-001"); FlowExecutionContext context = createContextWithMetadata(metadata); - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)).thenReturn(true); when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + eq(ActionType.FLOW_EXTENSIONS), eq("test-action-001"), any(FlowContext.class), eq("carbon.super"))) .thenReturn(null); @@ -534,9 +534,9 @@ public void testExecuteActionException() throws Exception { metadata.put("actionId", "test-action-001"); FlowExecutionContext context = createContextWithMetadata(metadata); - when(actionExecutorService.isExecutionEnabled(ActionType.IN_FLOW_EXTENSION)).thenReturn(true); + when(actionExecutorService.isExecutionEnabled(ActionType.FLOW_EXTENSIONS)).thenReturn(true); when(actionExecutorService.execute( - eq(ActionType.IN_FLOW_EXTENSION), eq("test-action-001"), + eq(ActionType.FLOW_EXTENSIONS), eq("test-action-001"), any(FlowContext.class), eq("carbon.super"))) .thenThrow(new ActionExecutionException("Connection timeout")); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java index 8cf37b4c6217..572cee5b70eb 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java @@ -87,7 +87,7 @@ public void tearDown() { @Test public void testGetSupportedActionType() { - assertEquals(requestBuilder.getSupportedActionType(), ActionType.IN_FLOW_EXTENSION); + assertEquals(requestBuilder.getSupportedActionType(), ActionType.FLOW_EXTENSIONS); } // ========================= buildActionExecutionRequest — basics ========================= @@ -112,7 +112,7 @@ public void testBuildRequestWithMinimalContext() throws ActionExecutionRequestBu ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); assertNotNull(request); - assertEquals(request.getActionType(), ActionType.IN_FLOW_EXTENSION); + assertEquals(request.getActionType(), ActionType.FLOW_EXTENSIONS); assertNotNull(request.getEvent()); } @@ -194,7 +194,7 @@ public void testBuildRequestWithNonInFlowActionFallsBackToMinimalPayload() ActionExecutionRequest request = requestBuilder.buildActionExecutionRequest(flowContext, reqCtx); assertNotNull(request); - assertEquals(request.getActionType(), ActionType.IN_FLOW_EXTENSION); + assertEquals(request.getActionType(), ActionType.FLOW_EXTENSIONS); assertEquals(request.getAllowedOperations().size(), 1); assertEquals(request.getAllowedOperations().get(0).getOp(), Operation.REDIRECT); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessorTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessorTest.java index 49576528548e..a88bce808e2a 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessorTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionResponseProcessorTest.java @@ -100,7 +100,7 @@ public void tearDown() { @Test public void testGetSupportedActionType() { - assertEquals(responseProcessor.getSupportedActionType(), ActionType.IN_FLOW_EXTENSION); + assertEquals(responseProcessor.getSupportedActionType(), ActionType.FLOW_EXTENSIONS); } // ========================= processSuccessResponse — Property REPLACE ========================= From ad41b3c3dfca56fb3f42a5705d4ed5cf5d8123ec Mon Sep 17 00:00:00 2001 From: Kumuditha - KD Date: Sat, 23 May 2026 13:07:32 +0530 Subject: [PATCH 38/41] Update structure --- .../extensions/model/InFlowExtensionFlow.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionFlow.java diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionFlow.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionFlow.java new file mode 100644 index 000000000000..cf555573a469 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionFlow.java @@ -0,0 +1,91 @@ +/* + * 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.extensions.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import org.wso2.carbon.identity.action.execution.api.model.User; + +/** + * Models the {@code flow} object nested inside the In-Flow Extension Event. + * Groups flow-scoped context: the flow type, flow identifier, and the acting user. + */ +public class InFlowExtensionFlow { + + private final String flowType; + private final String flowId; + private final User user; + + private InFlowExtensionFlow(Builder builder) { + + this.flowType = builder.flowType; + this.flowId = builder.flowId; + this.user = builder.user; + } + + /** + * NON_NULL overrides the ObjectMapper-level NON_EMPTY so that an exposed flowType with no + * context value is serialized as {@code ""} rather than omitted. + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + public String getFlowType() { + + return flowType; + } + + public String getFlowId() { + + return flowId; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public User getUser() { + + return user; + } + + public static class Builder { + + private String flowType; + private String flowId; + private User user; + + public Builder flowType(String flowType) { + + this.flowType = flowType; + return this; + } + + public Builder flowId(String flowId) { + + this.flowId = flowId; + return this; + } + + public Builder user(User user) { + + this.user = user; + return this; + } + + public InFlowExtensionFlow build() { + + return new InFlowExtensionFlow(this); + } + } +} From fd9a311e258643998bf67bf8ff0c4e35f75acc62 Mon Sep 17 00:00:00 2001 From: Kumuditha - KD Date: Sat, 23 May 2026 13:08:29 +0530 Subject: [PATCH 39/41] change structure --- .../engine/graph/TaskExecutionNode.java | 29 +++++++++ .../engine/model/ExecutorResponse.java | 11 ++++ .../InFlowExtensionRequestBuilder.java | 65 +++++-------------- .../model/InFlowExtensionEvent.java | 59 +++-------------- .../InFlowExtensionRequestBuilderTest.java | 43 ++++++------ .../model/InFlowExtensionEventTest.java | 22 +++++-- 6 files changed, 105 insertions(+), 124 deletions(-) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/graph/TaskExecutionNode.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/graph/TaskExecutionNode.java index c57ff63bd380..e08b8e3accf2 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/graph/TaskExecutionNode.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/graph/TaskExecutionNode.java @@ -19,8 +19,10 @@ package org.wso2.carbon.identity.flow.execution.engine.graph; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; 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.ExecutorResponse; @@ -28,6 +30,7 @@ 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.mgt.model.NodeConfig; +import org.wso2.carbon.user.core.common.AbstractUserStoreManager; import static org.wso2.carbon.identity.flow.execution.engine.Constants.ErrorMessages.ERROR_CODE_EXECUTOR_FAILURE; import static org.wso2.carbon.identity.flow.execution.engine.Constants.ErrorMessages.ERROR_CODE_EXECUTOR_NOT_FOUND; @@ -204,16 +207,42 @@ private NodeResponse handleCompleteStatus(FlowExecutionContext context, Executor } FlowUser user = context.getFlowUser(); + if (response.getUserId() != null) { + user.setUserId(response.getUserId()); + } if (response.getUpdatedUserClaims() != null) { response.getUpdatedUserClaims().forEach((key, value) -> user.addClaim(key, String.valueOf(value))); } if (response.getUserCredentials() != null) { user.getUserCredentials().putAll(response.getUserCredentials()); } + if (user.getUserId() == null) { + resolveUserIdFromUserStore(user, context.getTenantDomain()); + } if (CollectionUtils.isNotEmpty(configs.getEdges())) { configs.setNextNodeId(configs.getEdges().get(0).getTargetNodeId()); } return new NodeResponse.Builder().status(STATUS_COMPLETE).build(); } + + private void resolveUserIdFromUserStore(FlowUser user, String tenantDomain) { + + String username = user.getUsername(); + if (StringUtils.isBlank(username)) { + return; + } + try { + int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); + AbstractUserStoreManager userStoreManager = (AbstractUserStoreManager) + FlowExecutionEngineDataHolder.getInstance().getRealmService() + .getTenantUserRealm(tenantId).getUserStoreManager(); + String userId = userStoreManager.getUserIDFromUserName(username); + if (StringUtils.isNotBlank(userId)) { + user.setUserId(userId); + } + } catch (Exception e) { + LOG.warn("Failed to resolve userId for user '" + username + "' from user store.", e); + } + } } diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/ExecutorResponse.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/ExecutorResponse.java index d70ef7b39224..5aa8f0591b81 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/ExecutorResponse.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.execution.engine/src/main/java/org/wso2/carbon/identity/flow/execution/engine/model/ExecutorResponse.java @@ -27,6 +27,7 @@ public class ExecutorResponse { private String result; + private String userId; private List requiredData; private List optionalData; private Map updatedUserClaims; @@ -55,6 +56,16 @@ public void setResult(String result) { this.result = result; } + public String getUserId() { + + return userId; + } + + public void setUserId(String userId) { + + this.userId = userId; + } + public List getRequiredData() { return requiredData; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java index a43c0df37f84..f28a1cace6fe 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java @@ -31,7 +31,6 @@ import org.wso2.carbon.identity.action.execution.api.model.AllowedOperation; import org.wso2.carbon.identity.action.execution.api.model.Application; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; -import org.wso2.carbon.identity.action.execution.api.model.Header; import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.action.execution.api.model.Organization; import org.wso2.carbon.identity.action.execution.api.model.Tenant; @@ -105,7 +104,7 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex InFlowExtensionEvent event = buildEvent(execCtx, exposeResolution.getEffectiveExposePaths(), accessConfig, certificatePEM); - return buildRequestPayload(execCtx, event, allowedOperations); + return buildRequestPayload(event, allowedOperations); } /** @@ -158,51 +157,19 @@ private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List headers = new ArrayList<>(); - for (org.wso2.carbon.identity.core.context.model.Header coreHeader : inboundRequest.getHeaders()) { - if (coreHeader.getName() == null) { - continue; - } - List values = coreHeader.getValue(); - String[] valueArray = values != null - ? values.toArray(new String[0]) : new String[0]; - headers.add(new Header(coreHeader.getName(), valueArray)); - } - request.setAdditionalHeaders(headers); - - return request; - } - /** * Build the {@link User} model from {@link FlowUser}, filtering by expose config. * Encrypts credential and claim values for expose paths marked as encrypted. @@ -321,17 +288,18 @@ private ActionExecutionRequest buildFallbackRequest(FlowContext flowContext, Flo triggerFallbackDiagnostic(execCtx); InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() - .flowId(execCtx.getContextIdentifier()) + .flow(new InFlowExtensionFlow.Builder() + .flowId(execCtx.getContextIdentifier()) + .build()) .build(); - return buildRequestPayload(execCtx, event, allowedOperations); + return buildRequestPayload(event, allowedOperations); } - private ActionExecutionRequest buildRequestPayload(FlowExecutionContext execCtx, InFlowExtensionEvent event, + private ActionExecutionRequest buildRequestPayload(InFlowExtensionEvent event, List allowedOperations) { return new ActionExecutionRequest.Builder() .actionType(ActionType.FLOW_EXTENSIONS) - .flowId(execCtx.getContextIdentifier()) .event(event) .allowedOperations(allowedOperations) .build(); @@ -528,7 +496,7 @@ private void applyApplication(InFlowExtensionEvent.Builder eventBuilder, FlowExe eventBuilder.application(new Application(appId != null ? appId : "", null)); } - private void applyUserAndUserStore(InFlowExtensionEvent.Builder eventBuilder, FlowExecutionContext context, + private void applyUserAndUserStore(InFlowExtensionFlow.Builder flowBuilder, FlowExecutionContext context, List expose, AccessConfig accessConfig, String certificatePEM) throws ActionExecutionRequestBuilderException { @@ -541,17 +509,18 @@ private void applyUserAndUserStore(InFlowExtensionEvent.Builder eventBuilder, Fl return; } - eventBuilder.user(buildUser(flowUser, expose, accessConfig, certificatePEM)); + flowBuilder.user(buildUser(flowUser, expose, accessConfig, certificatePEM)); } - private void applyFlowMetadata(InFlowExtensionEvent.Builder eventBuilder, FlowExecutionContext context, - List expose) { + private void applyFlowMetadata(InFlowExtensionFlow.Builder flowBuilder, + InFlowExtensionEvent.Builder eventBuilder, + FlowExecutionContext context, List expose) { if (isLeafExposed(InFlowExtensionConstants.FLOW_TYPE_PATH, expose)) { - eventBuilder.flowType(context.getFlowType() != null ? context.getFlowType() : ""); + flowBuilder.flowType(context.getFlowType() != null ? context.getFlowType() : ""); } - eventBuilder.flowId(context.getContextIdentifier()); + flowBuilder.flowId(context.getContextIdentifier()); if (isLeafExposed(InFlowExtensionConstants.FLOW_CALLBACK_URL_PATH, expose)) { eventBuilder.callbackUrl(context.getCallbackUrl() != null ? context.getCallbackUrl() : ""); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEvent.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEvent.java index a896a55c31fb..0ead5b5c4b9e 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEvent.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEvent.java @@ -22,9 +22,7 @@ import org.wso2.carbon.identity.action.execution.api.model.Application; import org.wso2.carbon.identity.action.execution.api.model.Event; import org.wso2.carbon.identity.action.execution.api.model.Organization; -import org.wso2.carbon.identity.action.execution.api.model.Request; import org.wso2.carbon.identity.action.execution.api.model.Tenant; -import org.wso2.carbon.identity.action.execution.api.model.User; import org.wso2.carbon.identity.action.execution.api.model.UserStore; import java.util.Collections; @@ -37,49 +35,33 @@ */ public class InFlowExtensionEvent extends Event { - private final String flowType; - private final String flowId; + private final InFlowExtensionFlow flow; private final String callbackUrl; private final String portalUrl; private final Map flowProperties; private InFlowExtensionEvent(Builder builder) { - this.request = builder.request; this.tenant = builder.tenant; this.organization = builder.organization; - this.user = builder.user; this.userStore = builder.userStore; this.application = builder.application; - this.flowType = builder.flowType; - this.flowId = builder.flowId; + this.flow = builder.flow; this.callbackUrl = builder.callbackUrl; this.portalUrl = builder.portalUrl; - this.flowProperties = builder.flowProperties != null ? + this.flowProperties = builder.flowProperties != null ? Collections.unmodifiableMap(new HashMap<>(builder.flowProperties)) : Collections.emptyMap(); } /** - * Get the flow type. - * NON_NULL overrides the ObjectMapper-level NON_EMPTY so that an exposed flowType with no - * context value is serialized as {@code ""} rather than omitted. + * Get the flow context (type, ID, and user). * - * @return The flow type (e.g., "REGISTRATION", "PASSWORD_RESET"). + * @return The flow context object, or {@code null} if not set. */ @JsonInclude(JsonInclude.Include.NON_NULL) - public String getFlowType() { - - return flowType; - } - - /** - * Get the flow identifier (context identifier of the executing flow). - * - * @return The flow identifier. - */ - public String getFlowId() { + public InFlowExtensionFlow getFlow() { - return flowId; + return flow; } /** @@ -123,21 +105,18 @@ public Map getFlowProperties() { */ public static class Builder { - private Request request; + private InFlowExtensionFlow flow; private Tenant tenant; private Organization organization; - private User user; private UserStore userStore; private Application application; - private String flowType; - private String flowId; private String callbackUrl; private String portalUrl; private Map flowProperties; - public Builder request(Request request) { + public Builder flow(InFlowExtensionFlow flow) { - this.request = request; + this.flow = flow; return this; } @@ -153,12 +132,6 @@ public Builder organization(Organization organization) { return this; } - public Builder user(User user) { - - this.user = user; - return this; - } - public Builder userStore(UserStore userStore) { this.userStore = userStore; @@ -171,18 +144,6 @@ public Builder application(Application application) { return this; } - public Builder flowType(String flowType) { - - this.flowType = flowType; - return this; - } - - public Builder flowId(String flowId) { - - this.flowId = flowId; - return this; - } - public Builder callbackUrl(String callbackUrl) { this.callbackUrl = callbackUrl; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java index 572cee5b70eb..4383ffef407f 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilderTest.java @@ -130,7 +130,7 @@ public void testBuildRequestUsesEmptyExposeWhenExposeIsNull() // With empty expose, no context areas should be included in the event. InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); assertNotNull(event); - assertNull(event.getFlowType()); + assertNull(event.getFlow().getFlowType()); // flowProperties defaults to emptyMap() in the builder — verify it is empty. assertTrue(event.getFlowProperties().isEmpty()); } @@ -200,9 +200,10 @@ public void testBuildRequestWithNonInFlowActionFallsBackToMinimalPayload() InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); assertNotNull(event); - assertEquals(event.getFlowId(), execCtx.getContextIdentifier()); - assertNull(event.getUser()); - assertNull(event.getFlowType()); + assertNotNull(event.getFlow()); + assertEquals(event.getFlow().getFlowId(), execCtx.getContextIdentifier()); + assertNull(event.getFlow().getUser()); + assertNull(event.getFlow().getFlowType()); } @Test @@ -450,9 +451,9 @@ public void testExposeFilteringOnlyExposedAreaIncluded() InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); // User should NOT be in the event since /user/ is not exposed. - assertNull(event.getUser()); + assertNull(event.getFlow().getUser()); // Flow type should be present. - assertNotNull(event.getFlowType()); + assertNotNull(event.getFlow().getFlowType()); // Tenant should be present. assertNotNull(event.getTenant()); } @@ -536,9 +537,9 @@ public void testExposeFilteringSpecificClaim() flowContext, mockReqCtx(accessConfig, null)); InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); - assertNotNull(event.getUser()); + assertNotNull(event.getFlow().getUser()); // Only the email claim should be present, not the country claim. - List claims = event.getUser().getClaims(); + List claims = event.getFlow().getUser().getClaims(); assertEquals(claims.size(), 1); } @@ -603,7 +604,7 @@ public void testExposedFlowTypeNullYieldsEmptyString() flowContext, mockReqCtx(accessConfig, null)); InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); - assertEquals(event.getFlowType(), "", + assertEquals(event.getFlow().getFlowType(), "", "Exposed flowType must be '' when context value is null"); } @@ -672,8 +673,8 @@ public void testExposedUserIdNullYieldsEmptyString() flowContext, mockReqCtx(accessConfig, null)); InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); - assertNotNull(event.getUser()); - assertEquals(event.getUser().getId(), "", + assertNotNull(event.getFlow().getUser()); + assertEquals(event.getFlow().getUser().getId(), "", "User id must be '' when userId is null and path is exposed"); } @@ -694,10 +695,10 @@ public void testExposedUserStoreDomainNullYieldsEmptyString() flowContext, mockReqCtx(accessConfig, null)); InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); - assertNotNull(event.getUser(), "User must be present when /user/userStoreDomain is exposed"); - assertNotNull(event.getUser().getUserStoreDomain(), + assertNotNull(event.getFlow().getUser(), "User must be present when /user/userStoreDomain is exposed"); + assertNotNull(event.getFlow().getUser().getUserStoreDomain(), "UserStore must be present when /user/userStoreDomain is exposed"); - assertEquals(event.getUser().getUserStoreDomain().getName(), "", + assertEquals(event.getFlow().getUser().getUserStoreDomain().getName(), "", "UserStore name must be '' when userStoreDomain is null"); } @@ -718,8 +719,8 @@ public void testExposedClaimAbsentFromClaimsMapYieldsEmptyString() flowContext, mockReqCtx(accessConfig, null)); InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); - assertNotNull(event.getUser()); - List claims = event.getUser().getClaims(); + assertNotNull(event.getFlow().getUser()); + List claims = event.getFlow().getUser().getClaims(); assertEquals(claims.size(), 1); org.wso2.carbon.identity.action.execution.api.model.UserClaim mobileClaim = (org.wso2.carbon.identity.action.execution.api.model.UserClaim) claims.get(0); @@ -769,7 +770,7 @@ public void testExposedClaimWithNullValueYieldsEmptyString() flowContext, mockReqCtx(accessConfig, null)); InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); - List claims = event.getUser().getClaims(); + List claims = event.getFlow().getUser().getClaims(); assertEquals(claims.size(), 1); org.wso2.carbon.identity.action.execution.api.model.UserClaim emailClaim = (org.wso2.carbon.identity.action.execution.api.model.UserClaim) claims.get(0); @@ -847,8 +848,8 @@ public void testClaimEncryptedWhenExposePathMarkedEncrypted() flowContext, mockReqCtx(accessConfig, encryption)); InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); - assertNotNull(event.getUser()); - List claims = event.getUser().getClaims(); + assertNotNull(event.getFlow().getUser()); + List claims = event.getFlow().getUser().getClaims(); assertEquals(claims.size(), 2); org.wso2.carbon.identity.action.execution.api.model.UserClaim emailClaim = null; @@ -902,8 +903,8 @@ public void testCredentialEncryptedWhenExposePathMarkedEncrypted() flowContext, mockReqCtx(accessConfig, encryption)); InFlowExtensionEvent event = (InFlowExtensionEvent) request.getEvent(); - assertNotNull(event.getUser()); - Map eventCreds = event.getUser().getUserCredentials(); + assertNotNull(event.getFlow().getUser()); + Map eventCreds = event.getFlow().getUser().getUserCredentials(); assertNotNull(eventCreds); assertTrue(eventCreds.containsKey("password")); String credValue = new String(eventCreds.get("password")); diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEventTest.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEventTest.java index 2903656d38d2..2827cd16feb6 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEventTest.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/test/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionEventTest.java @@ -39,16 +39,21 @@ public void testBuilderWithAllFields() { Map flowProperties = new HashMap<>(); flowProperties.put("riskScore", 85); - InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() + InFlowExtensionFlow flow = new InFlowExtensionFlow.Builder() .flowType("REGISTRATION") .flowId("flow-id-123") + .build(); + + InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() + .flow(flow) .callbackUrl("https://example.com/callback") .portalUrl("https://example.com/portal") .flowProperties(flowProperties) .build(); - assertEquals(event.getFlowType(), "REGISTRATION"); - assertEquals(event.getFlowId(), "flow-id-123"); + assertNotNull(event.getFlow()); + assertEquals(event.getFlow().getFlowType(), "REGISTRATION"); + assertEquals(event.getFlow().getFlowId(), "flow-id-123"); assertEquals(event.getCallbackUrl(), "https://example.com/callback"); assertEquals(event.getPortalUrl(), "https://example.com/portal"); assertEquals(event.getFlowProperties().get("riskScore"), 85); @@ -57,14 +62,19 @@ public void testBuilderWithAllFields() { @Test public void testOptionalFieldsDefaultToNull() { - InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() + InFlowExtensionFlow flow = new InFlowExtensionFlow.Builder() .flowType("LOGIN") .flowId("flow-id-456") + .build(); + + InFlowExtensionEvent event = new InFlowExtensionEvent.Builder() + .flow(flow) .flowProperties(null) .build(); - assertEquals(event.getFlowType(), "LOGIN"); - assertEquals(event.getFlowId(), "flow-id-456"); + assertNotNull(event.getFlow()); + assertEquals(event.getFlow().getFlowType(), "LOGIN"); + assertEquals(event.getFlow().getFlowId(), "flow-id-456"); assertNull(event.getCallbackUrl()); assertNull(event.getPortalUrl()); assertNotNull(event.getFlowProperties()); From d8c3e9e8091416f0fdc40dc40923988358093a69 Mon Sep 17 00:00:00 2001 From: Kumuditha - KD Date: Sat, 23 May 2026 17:07:04 +0530 Subject: [PATCH 40/41] update --- .../InFlowExtensionRequestBuilder.java | 4 +- .../extensions/model/InFlowExtensionFlow.java | 9 ++-- .../flow/extensions/model/InFlowUser.java | 43 +++++++++++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowUser.java diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java index f28a1cace6fe..ada25de4fd71 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java @@ -180,7 +180,7 @@ private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List expose, + private InFlowUser buildUser(FlowUser flowUser, List expose, AccessConfig accessConfig, String certificatePEM) throws ActionExecutionRequestBuilderException { @@ -201,7 +201,7 @@ private User buildUser(FlowUser flowUser, List expose, userBuilder.userStoreDomain(new UserStore(userStoreDomain != null ? userStoreDomain : "")); } - return userBuilder.build(); + return new InFlowUser(userBuilder); } private FlowExecutionContext getFlowExecutionContextOrThrow(FlowContext flowContext) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionFlow.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionFlow.java index cf555573a469..78fa97678e00 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionFlow.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowExtensionFlow.java @@ -19,7 +19,6 @@ package org.wso2.carbon.identity.flow.extensions.model; import com.fasterxml.jackson.annotation.JsonInclude; -import org.wso2.carbon.identity.action.execution.api.model.User; /** * Models the {@code flow} object nested inside the In-Flow Extension Event. @@ -29,7 +28,7 @@ public class InFlowExtensionFlow { private final String flowType; private final String flowId; - private final User user; + private final InFlowUser user; private InFlowExtensionFlow(Builder builder) { @@ -54,7 +53,7 @@ public String getFlowId() { } @JsonInclude(JsonInclude.Include.NON_NULL) - public User getUser() { + public InFlowUser getUser() { return user; } @@ -63,7 +62,7 @@ public static class Builder { private String flowType; private String flowId; - private User user; + private InFlowUser user; public Builder flowType(String flowType) { @@ -77,7 +76,7 @@ public Builder flowId(String flowId) { return this; } - public Builder user(User user) { + public Builder user(InFlowUser user) { this.user = user; return this; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowUser.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowUser.java new file mode 100644 index 000000000000..9889b24292e0 --- /dev/null +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/model/InFlowUser.java @@ -0,0 +1,43 @@ +/* + * 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.extensions.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.wso2.carbon.identity.action.execution.api.model.User; + +/** + * In-Flow Extension specific view of {@link User} that serializes the user identifier as + * {@code "userId"} rather than the shared model's {@code "id"} field name. + */ +public class InFlowUser extends User { + + public InFlowUser(User.Builder builder) { + + super(builder); + } + + @Override + @JsonProperty("userId") + @JsonInclude(JsonInclude.Include.NON_NULL) + public String getId() { + + return super.getId(); + } +} From 1da00f92111ac78d40334020a21ff61371177070 Mon Sep 17 00:00:00 2001 From: Kumuditha - KD Date: Sat, 23 May 2026 20:48:22 +0530 Subject: [PATCH 41/41] Update applyOrganization in the request builder --- .../extensions/InFlowExtensionConstants.java | 5 ++++ .../InFlowExtensionRequestBuilder.java | 30 ++++++++++++++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/InFlowExtensionConstants.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/InFlowExtensionConstants.java index f766dea31fb5..603e9f32405b 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/InFlowExtensionConstants.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/InFlowExtensionConstants.java @@ -66,6 +66,11 @@ private InFlowExtensionConstants() { public static final String FLOW_TYPE_PATH = "/flow/flowType"; public static final String FLOW_CALLBACK_URL_PATH = "/flow/callbackUrl"; public static final String FLOW_PORTAL_URL_PATH = "/flow/portalUrl"; + public static final String ORGANIZATION_PREFIX = "/organization/"; + public static final String ORGANIZATION_ID_PATH = "/organization/id"; + public static final String ORGANIZATION_NAME_PATH = "/organization/name"; + public static final String ORGANIZATION_HANDLE_PATH = "/organization/orgHandle"; + public static final String ORGANIZATION_DEPTH_PATH = "/organization/depth"; // ---- Miscellaneous ---- public static final String ACTION_ID_METADATA_KEY = "actionId"; diff --git a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java index ada25de4fd71..e04c4140b16c 100644 --- a/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java +++ b/components/flow-orchestration-framework/org.wso2.carbon.identity.flow.extensions/src/main/java/org/wso2/carbon/identity/flow/extensions/executor/InFlowExtensionRequestBuilder.java @@ -160,7 +160,7 @@ private InFlowExtensionEvent buildEvent(FlowExecutionContext context, List expose) { + + if (!isAreaExposed(InFlowExtensionConstants.ORGANIZATION_PREFIX, expose)) { + return; + } org.wso2.carbon.identity.core.context.model.Organization coreOrg = IdentityContext.getThreadLocalIdentityContext().getOrganization(); @@ -477,12 +481,22 @@ private void applyOrganization(InFlowExtensionEvent.Builder eventBuilder) { return; } - eventBuilder.organization(new Organization.Builder() - .id(coreOrg.getId()) - .name(coreOrg.getName()) - .orgHandle(coreOrg.getOrganizationHandle()) - .depth(coreOrg.getDepth()) - .build()); + Organization.Builder orgBuilder = new Organization.Builder(); + + if (isLeafExposed(InFlowExtensionConstants.ORGANIZATION_ID_PATH, expose)) { + orgBuilder.id(coreOrg.getId()); + } + if (isLeafExposed(InFlowExtensionConstants.ORGANIZATION_NAME_PATH, expose)) { + orgBuilder.name(coreOrg.getName()); + } + if (isLeafExposed(InFlowExtensionConstants.ORGANIZATION_HANDLE_PATH, expose)) { + orgBuilder.orgHandle(coreOrg.getOrganizationHandle()); + } + if (isLeafExposed(InFlowExtensionConstants.ORGANIZATION_DEPTH_PATH, expose)) { + orgBuilder.depth(coreOrg.getDepth()); + } + + eventBuilder.organization(orgBuilder.build()); } private void applyApplication(InFlowExtensionEvent.Builder eventBuilder, FlowExecutionContext context,