Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
256f3c7
Add In-Flow Extension Action Execution Support
ThejithaR Feb 5, 2026
3abe160
Refactor component responsibilities in in-flow extensions.
ThejithaR Feb 17, 2026
8ac739b
Implemented unit tests
ThejithaR Feb 20, 2026
31c0f2e
Moved the In-flow extension executor and its util classes to flow exe…
ThejithaR Feb 23, 2026
2011976
Executor Package re-arrangements
ThejithaR Feb 27, 2026
c6526ba
Add In-Flow Extension Action Management and Access Configuration
ThejithaR Mar 5, 2026
8dcf6eb
Enhance In-Flow Extension Model with Encryption and Path Management
ThejithaR Mar 11, 2026
ffca806
path type annotation validations and encryption
ThejithaR Mar 27, 2026
72425ce
Adding action executor service to flow execution engine service compo…
ThejithaR Mar 29, 2026
bc2a3cb
Fixed an issue in property and input encryption and modified unit tests.
ThejithaR Mar 30, 2026
7ac1f50
Address code review comments for In-Flow Extension
ThejithaR Apr 6, 2026
14c6366
Removed flow update interceptor - direct action api call for overrides
ThejithaR Apr 17, 2026
f1efaac
Moved action property handling to request builder and response proces…
ThejithaR Apr 23, 2026
1f9e78a
Removed the context modification logic from response processor. to be…
ThejithaR Apr 24, 2026
74d6b83
Adding Dynamic error handling and diagnostic logs.
ThejithaR Apr 27, 2026
a51ef48
Redirect operation support
ThejithaR Apr 28, 2026
a52f600
Flow execution context handover filtering and flow context tree metad…
ThejithaR Apr 30, 2026
c4debd2
Adding handover policy and metadata service. and remove i18n key pref…
ThejithaR Apr 30, 2026
98be83c
adding missing export packages
ThejithaR May 1, 2026
944de18
Removing action name fetching for prefixing and moved the synchroniza…
ThejithaR May 1, 2026
6f3d94b
Improved diagnostic logs and fixed the fail fast behaviour when certi…
ThejithaR May 4, 2026
0eaf61c
Moved all Inflow extension Constants to flow execution Constants
ThejithaR May 4, 2026
2aac9a7
Fix CodeRabbit review findings (1-7) for In-Flow Extension
ThejithaR May 6, 2026
261eff1
Moved inflow extensions to a new module
ThejithaR May 8, 2026
a63b57e
Flow execution context handover filtering Generalized and moved to fl…
ThejithaR May 8, 2026
f054597
Reviewed action framework suggestions are fixed.
ThejithaR May 9, 2026
447a663
Addressed comments and sonarcube warnings
ThejithaR May 11, 2026
6d9d925
fixed a minor import issue java.util.stream.Collectors
ThejithaR May 11, 2026
54bfdf4
corrected copyright headers
ThejithaR May 11, 2026
7239b92
Removed the deployment.toml based context filtering - moved to consta…
ThejithaR May 11, 2026
8238705
Address review comments
KD23243 May 20, 2026
1d6517c
Address review comments 2
KD23243 May 20, 2026
b1b784c
Add missing dep
KD23243 May 21, 2026
6785619
Fix pom
KD23243 May 21, 2026
93d2047
Fix import
KD23243 May 21, 2026
ee812b9
Progress 1
KD23243 May 23, 2026
2398344
Rename flow extensions
KD23243 May 23, 2026
ad41b3c
Update structure
KD23243 May 23, 2026
fd9a311
change structure
KD23243 May 23, 2026
d8c3e9e
update
KD23243 May 23, 2026
1da00f9
Update applyOrganization in the request builder
KD23243 May 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@
hs_err_pid*

# Ignore everything in this directory
target
target

# Maven versions plugin backup files
*.versionsBackup
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public enum ActionType {
PRE_UPDATE_PASSWORD,
PRE_UPDATE_PROFILE,
AUTHENTICATION,
PRE_ISSUE_ID_TOKEN;
PRE_ISSUE_ID_TOKEN,
FLOW_EXTENSIONS;

public String getDisplayName() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,18 @@

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;
import java.util.List;
import java.util.Map;

/**
* This class models the User.
Expand All @@ -30,9 +39,11 @@ public class User {

private final String id;
private final List<UserClaim> claims = new ArrayList<>();
private final Map<String, char[]> userCredentials = new HashMap<>();
private final List<String> groups = new ArrayList<>();
private final List<String> 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
Expand Down Expand Up @@ -65,15 +76,18 @@ public User(Builder builder) {

this.id = builder.id;
this.claims.addAll(builder.claims);
this.userCredentials.putAll(builder.userCredentials);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Defensively copy credential arrays instead of sharing references.

userCredentials is wrapped as unmodifiable, but each char[] value is still mutable and currently shared by reference. That allows external mutation after build/get, which is risky for credential data integrity and secrecy.

Proposed fix
@@
     public User(Builder builder) {
@@
-        this.userCredentials.putAll(builder.userCredentials);
+        builder.userCredentials.forEach((k, v) ->
+                this.userCredentials.put(k, v != null ? v.clone() : null));
@@
     public Map<String, char[]> getUserCredentials() {
-
-        return Collections.unmodifiableMap(userCredentials);
+        Map<String, char[]> safeCopy = new HashMap<>();
+        userCredentials.forEach((k, v) -> safeCopy.put(k, v != null ? v.clone() : null));
+        return Collections.unmodifiableMap(safeCopy);
     }
@@
         public Builder userCredentials(Map<String, char[]> userCredentials) {
-
-            this.userCredentials.putAll(userCredentials);
+            if (userCredentials == null) {
+                return this;
+            }
+            userCredentials.forEach((k, v) -> this.userCredentials.put(k, v != null ? v.clone() : null));
             return this;
         }

Also applies to: 101-104, 224-227

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/api/model/User.java`
at line 79, The code currently copies the map reference via putAll but still
shares mutable char[] values; change all places that populate or expose
userCredentials (the builder assignment that does
this.userCredentials.putAll(builder.userCredentials), the builder's
setter/constructor code at the other indicated spots, and any getUserCredentials
method) to defensively copy each char[] (e.g., Arrays.copyOf(value,
value.length)) when storing into the internal map and likewise copy each char[]
when returning values or building the unmodifiable map so callers never get a
direct reference to the original mutable arrays; update the builder to copy
incoming arrays into its map and ensure the User constructor copies
builder.userCredentials entries into its internal map using per-entry
Arrays.copyOf before wrapping with Collections.unmodifiableMap.

this.groups.addAll(builder.groups);
this.roles.addAll(builder.roles);
this.organization = builder.organization;
this.sharedUserId = builder.sharedUserId;
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;
Expand All @@ -84,6 +98,11 @@ public List<UserClaim> getClaims() {
return Collections.unmodifiableList(claims);
}

public Map<String, char[]> getUserCredentials() {

return Collections.unmodifiableMap(userCredentials);
}

public List<String> getGroups() {

return Collections.unmodifiableList(groups);
Expand All @@ -99,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;
Expand Down Expand Up @@ -126,9 +152,11 @@ public static class Builder {

private final String id;
private final List<UserClaim> claims = new ArrayList<>();
private final Map<String, char[]> userCredentials = new HashMap<>();
private final List<String> groups = new ArrayList<>();
private final List<String> roles = new ArrayList<>();
private Organization organization;
private UserStore userStoreDomain;
private String sharedUserId;
private String userType;
private String federatedIdP;
Expand Down Expand Up @@ -163,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;
Expand All @@ -187,9 +221,24 @@ public Builder accessingOrganization(Organization accessingOrganization) {
return this;
}

public Builder userCredentials(Map<String, char[]> userCredentials) {

this.userCredentials.putAll(userCredentials);
Comment thread
KD23243 marked this conversation as resolved.
return this;
}

public User build() {

return new User(this);
}
}

private static class UserStoreNameSerializer extends JsonSerializer<UserStore> {

@Override
public void serialize(UserStore value, JsonGenerator gen, SerializerProvider serializers) throws IOException {

gen.writeString(value.getName() != null ? value.getName() : "");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,6 +37,7 @@ public class ActionExecutionServiceComponentHolder {
private SecretManager secretManager;
private SecretResolveManager secretResolveManager;
private RealmService realmService;
private ActionExecutorService actionExecutorService;

private ActionExecutionServiceComponentHolder() {

Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Comment on lines +200 to +205

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not convert “action not found” into a silently skipped success path.

Throwing ActionExecutionRuntimeException here is swallowed upstream (Line 154), so an explicit actionId miss is treated as successful execution instead of a failure.

Suggested fix
-            if (action == null) {
-                throw new ActionExecutionRuntimeException("No action found for action Id: " + actionId);
-            }
+            if (action == null) {
+                throw new ActionExecutionException("No action found for action Id: " + actionId);
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
}
Action action = ActionExecutionServiceComponentHolder.getInstance().getActionManagementService()
.getActionByActionId(Action.ActionTypes.valueOf(actionType.name()).getPathParam(), actionId,
tenantDomain);
if (action == null) {
throw new ActionExecutionException("No action found for action Id: " + actionId);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/service/impl/ActionExecutorServiceImpl.java`
around lines 200 - 205, The code currently throws
ActionExecutionRuntimeException when getActionByActionId returns null, but that
runtime exception is swallowed upstream causing missing actionId to be treated
as success; instead create and throw a specific checked exception (e.g.,
ActionNotFoundException) from ActionExecutorServiceImpl when action == null
(replace the ActionExecutionRuntimeException throw), update the method
signature(s) that call getActionByActionId/executeAction to declare the new
checked exception, and propagate the exception up so callers cannot silently
swallow it (adjust callers to catch or rethrow as appropriate) — reference
symbols: ActionExecutorServiceImpl, getActionByActionId,
ActionExecutionRuntimeException, and the new ActionNotFoundException to
implement and propagate.

return action;
} catch (ActionMgtException e) {
throw new ActionExecutionException("Error occurred while retrieving action by action Id.", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 FLOW_EXTENSIONS:
return isActionTypeEnabled(ActionTypeConfig.FLOW_EXTENSIONS.getActionTypeEnableProperty());
default:
return false;
}
Expand Down Expand Up @@ -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 FLOW_EXTENSIONS:
return getVersion(ActionTypeConfig.FLOW_EXTENSIONS.getRetiredUpToVersionProperty());
default:
return null;
}
Expand Down Expand Up @@ -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"),
FLOW_EXTENSIONS("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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
FLOW_EXTENSIONS(
"inFlowExtension",
"FLOW_EXTENSIONS",
"In-Flow Extension",
"Configure an extension point within any flow via a custom service.",
Category.IN_FLOW_EXTENSION);

private final String pathParam;
private final String actionType;
Expand Down Expand Up @@ -131,7 +137,8 @@ public static ActionTypes[] filterByCategory(Category category) {
*/
public enum Category {
PRE_POST,
IN_FLOW
IN_FLOW,
IN_FLOW_EXTENSION

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to flow extensions

}
}

Expand Down

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the name check. use a fe validation + backend error

Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,5 @@ Action updateAction(String actionType, String actionId, Action action, String te
*/
Action updateActionEndpointAuthentication(String actionType, String actionId, Authentication authentication,
String tenantDomain) throws ActionMgtException;

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<ActionDTO> 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.
Expand All @@ -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<ActionDTO> existingActionsOfType)
throws ActionMgtException {

doPreUpdateActionValidations(actionType, actionVersion, action);
}
}
Loading