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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
import java.util.List;

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;
Expand Down Expand Up @@ -251,7 +252,8 @@ public void testActionExecuteSuccessWhenRuleConfiguredAndNotSatisfied() throws E
when(action.getActionVersion()).thenReturn("v1");
when(action.getType()).thenReturn(Action.ActionTypes.PRE_ISSUE_ACCESS_TOKEN);
when(action.getActionRule()).thenReturn(ActionRule.create("ruleId", "tenantDomain"));
when(ruleEvaluationService.evaluate(any(), any(), any())).thenReturn(new RuleEvaluationResult("ruleId", false));
when(ruleEvaluationService.evaluate(anyString(), any(), any()))
.thenReturn(new RuleEvaluationResult("ruleId", false));

ActionType actionType = ActionType.PRE_ISSUE_ACCESS_TOKEN;

Expand Down Expand Up @@ -324,7 +326,7 @@ public void testActionExecuteFailureWhenRuleEvaluationFails() throws Exception {
when(action.getActionRule()).thenReturn(ActionRule.create("ruleId", "tenantDomain"));
when(actionManagementService.getActionsByActionType(any(), any())).thenReturn(
Collections.singletonList(action));
when(ruleEvaluationService.evaluate(any(), any(), any())).thenThrow(new RuleEvaluationException("Error"));
when(ruleEvaluationService.evaluate(anyString(), any(), any())).thenThrow(new RuleEvaluationException("Error"));

ActionType actionType = ActionType.PRE_ISSUE_ACCESS_TOKEN;

Expand Down Expand Up @@ -546,7 +548,8 @@ public void testActionExecuteSuccessWhenRuleConfiguredInActionIsSatisfied() thro
when(actionManagementService.getActionsByActionType(any(), any())).thenReturn(
Collections.singletonList(action));

when(ruleEvaluationService.evaluate(any(), any(), any())).thenReturn(new RuleEvaluationResult("ruleId", true));
when(ruleEvaluationService.evaluate(anyString(), any(), any()))
.thenReturn(new RuleEvaluationResult("ruleId", true));

actionExecutionRequestBuilderFactory.when(
() -> ActionExecutionRequestBuilderFactory.getActionExecutionRequestBuilder(any()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,18 @@
public class Field {

private final String name;
private final String qualifier;
private final ValueType valueType;

public Field(String name, ValueType valueType) {

this(name, null, valueType);
}

public Field(String name, String qualifier, ValueType valueType) {

this.name = name;
this.qualifier = qualifier;
this.valueType = valueType;
}

Expand All @@ -38,6 +45,17 @@ public String getName() {
return name;
}

/**
* Qualifier selecting a value within the field. A data provider needs it to know which value to
* resolve -- which claim, for instance -- for fields that name a family rather than one value.
*
* @return The qualifier, or null when the field is not qualified.
*/
public String getQualifier() {

return qualifier;
}

public ValueType getValueType() {

return valueType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,37 +27,62 @@
public class FieldValue {

private final String name;
private final String qualifier;
private final ValueType valueType;
private final Object value;

public FieldValue(String name, String value, ValueType valueType) {

this(name, null, value, valueType);
}

public FieldValue(String name, String qualifier, String value, ValueType valueType) {

if (!(valueType == ValueType.STRING || valueType instanceof ValueType.ReferenceValueType)) {
throw new IllegalArgumentException("Value type should be STRING or REFERENCE to set a string value");
}

this.name = name;
this.qualifier = qualifier;
this.valueType = valueType;
this.value = value;
}

public FieldValue(String name, Boolean value) {

this(name, null, value);
}

public FieldValue(String name, String qualifier, Boolean value) {

this.name = name;
this.qualifier = qualifier;
this.valueType = ValueType.BOOLEAN;
this.value = value;
}

public FieldValue(String name, Number value) {

this(name, null, value);
}

public FieldValue(String name, String qualifier, Number value) {

this.name = name;
this.qualifier = qualifier;
this.valueType = ValueType.NUMBER;
this.value = Double.valueOf(value.toString());
}

public FieldValue(String name, List<String> value) {

this(name, null, value);
}

public FieldValue(String name, String qualifier, List<String> value) {

this.name = name;
this.qualifier = qualifier;
this.valueType = ValueType.LIST;
this.value = value;
}
Expand All @@ -67,6 +92,16 @@ public String getName() {
return name;
}

/**
* Qualifier this value was resolved for, echoing the qualifier of the field it answers.
*
* @return The qualifier, or null when the field is not qualified.
*/
public String getQualifier() {

return qualifier;
}

public ValueType getValueType() {

return valueType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@ public enum FlowType {
PRE_UPDATE_PROFILE,
PRE_ISSUE_ID_TOKEN,
APPROVAL_WORKFLOW,
DEVICE_POLICY
DEVICE_POLICY,
REGISTRATION,
PASSWORD_RECOVERY,
INVITED_USER_REGISTRATION
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.wso2.carbon.identity.rule.evaluation.api.exception.RuleEvaluationException;
import org.wso2.carbon.identity.rule.evaluation.api.model.FlowContext;
import org.wso2.carbon.identity.rule.evaluation.api.model.RuleEvaluationResult;
import org.wso2.carbon.identity.rule.management.api.model.Rule;

/**
* Rule evaluation service interface.
Expand All @@ -40,4 +41,20 @@ public interface RuleEvaluationService {
RuleEvaluationResult evaluate(String ruleId, FlowContext flowContext, String tenantDomain)
throws RuleEvaluationException;

/**
* Evaluate a rule that is held by its caller rather than by rule management.
* <p>
* Some callers own their rules inside their own configuration and never register them, so there
* is no id to look up. The caller is responsible for the rule having been validated when it was
* authored -- the id based method gets that guarantee from rule management, this one cannot.
*
* @param rule Rule to evaluate.
* @param flowContext Flow context.
* @param tenantDomain Tenant domain.
* @return Rule evaluation result.
* @throws RuleEvaluationException If an error occurs while evaluating the rule.
*/
RuleEvaluationResult evaluate(Rule rule, FlowContext flowContext, String tenantDomain)
throws RuleEvaluationException;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.wso2.carbon.identity.rule.evaluation.api.model.Field;
import org.wso2.carbon.identity.rule.evaluation.api.model.ValueType;
import org.wso2.carbon.identity.rule.management.api.model.Expression;
import org.wso2.carbon.identity.rule.management.api.model.FieldReference;
import org.wso2.carbon.identity.rule.management.api.model.Rule;
import org.wso2.carbon.identity.rule.metadata.api.model.FieldDefinition;
import org.wso2.carbon.identity.rule.metadata.api.model.OptionsReferenceValue;
Expand Down Expand Up @@ -59,19 +60,35 @@ public FieldExtractor(List<FieldDefinition> expressionMetadataFields) {
public List<Field> extractFields(Rule rule) throws RuleEvaluationException {

List<Field> fieldList = new ArrayList<>();
Set<String> extractedFieldName = new HashSet<>();
Set<String> extractedFields = new HashSet<>();

for (Expression expression : rule.getExpressions()) {
String fieldName = expression.getField();
if (extractedFieldName.add(fieldName)) {
// The qualifier takes part in identity: two expressions over the same field with different qualifiers
// are different values, and de-duplicating on the name alone would drop the second.
Comment on lines +66 to +67

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.

do we need this comments. Keep the comments to a minimum and make the code self-explanatory as much as possible

if (extractedFields.add(FieldLookup.token(expression.getField(), expression.getFieldQualifier()))) {
// Metadata is held per field, not per qualifier, so the definition is still looked up by name.
FieldDefinition fieldDefinition = expressionMetadataFieldsMap.get(expression.getField());
if (fieldDefinition == null) {
throw new RuleEvaluationException(
"Field definition not found for the field: " + expression.getField());
}

Field field = new Field(expression.getField(), resolveValueType(fieldDefinition.getValue()));
fieldList.add(field);
fieldList.add(new Field(expression.getField(), expression.getFieldQualifier(),
resolveValueType(fieldDefinition.getValue())));
}
// A value read from another field needs that field resolved too.
FieldReference reference = expression.getValue() == null ? null
: expression.getValue().getFieldReference();
if (reference != null && expression.getValue().getType()
== org.wso2.carbon.identity.rule.management.api.model.Value.Type.FIELD
&& extractedFields.add(FieldLookup.token(reference.getName(), reference.getQualifier()))) {
FieldDefinition referencedDefinition = expressionMetadataFieldsMap.get(reference.getName());
if (referencedDefinition == null) {
throw new RuleEvaluationException(
"Field definition not found for the field: " + reference.getName());
}
fieldList.add(new Field(reference.getName(), reference.getQualifier(),
resolveValueType(referencedDefinition.getValue())));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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.rule.evaluation.internal.service.impl;

/**
* Identifies a field within one evaluation.
* <p>
* A field name alone stops being enough once a field is qualified: two expressions over the same field
* with different qualifiers are different values, and collapsing them would silently drop one. A field with
* no qualifier tokenises to its bare name, so every field that existed before qualifiers behaves as it always did.
*/
final class FieldLookup {

/**
* Separator between a field name and its qualifier. A unit separator is used because neither a field
* name nor a qualifier may contain a control character, so the token cannot be ambiguous.
*/
private static final char SEPARATOR = (char) 0x1F;

private FieldLookup() {

}

/**
* Build the token identifying a field within one evaluation.
*
* @param name Field name.
* @param qualifier Qualifier selecting a value within that field, or null when the field is not qualified.
* @return Token that distinguishes this field from every other in the same rule.
*/
static String token(String name, String qualifier) {

return qualifier == null ? name : name + SEPARATOR + qualifier;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.wso2.carbon.identity.rule.evaluation.internal.component.RuleEvaluationComponentServiceHolder;
import org.wso2.carbon.identity.rule.metadata.api.service.RuleMetadataService;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiPredicate;
Expand All @@ -45,6 +46,8 @@ public class OperatorRegistry {
supportedOperators.put("endsWith", stringPredicate(String::endsWith));
supportedOperators.put("greaterThan", comparablePredicate(result -> result > 0));
supportedOperators.put("lessThan", comparablePredicate(result -> result < 0));
supportedOperators.put("in", membershipPredicate(true));
supportedOperators.put("notIn", membershipPredicate(false));
}

private OperatorRegistry() {
Expand Down Expand Up @@ -100,4 +103,9 @@ private static BiPredicate<Object, Object> stringPredicate(BiPredicate<String, S

return (a, b) -> a instanceof String && b instanceof String && predicate.test((String) a, (String) b);
}

private static BiPredicate<Object, Object> membershipPredicate(boolean expected) {

return (a, b) -> a != null && b instanceof Collection && ((Collection<?>) b).contains(a) == expected;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,20 @@ public class RuleEvaluationServiceImpl implements RuleEvaluationService {
public RuleEvaluationResult evaluate(String ruleId, FlowContext flowContext, String tenantDomain)
throws RuleEvaluationException {

Rule rule = getRuleFromRuleManagementService(ruleId, tenantDomain);
return evaluate(getRuleFromRuleManagementService(ruleId, tenantDomain), flowContext, tenantDomain);
}

@Override
public RuleEvaluationResult evaluate(Rule rule, FlowContext flowContext, String tenantDomain)
throws RuleEvaluationException {

if (rule == null) {
throw new RuleEvaluationException("Rule to evaluate cannot be null.");
}

if (!rule.isActive()) {
LOG.debug("Rule: " + rule.getId() + " is inactive. Skip evaluation of rule.");
return new RuleEvaluationResult(ruleId, false, null);
return new RuleEvaluationResult(rule.getId(), false, null);
}

LOG.debug("Starting to evaluate rule: " + rule.getId() + ".");
Expand All @@ -72,7 +81,7 @@ public RuleEvaluationResult evaluate(String ruleId, FlowContext flowContext, Str
List<Field> fieldsInRule = fieldExtractor.extractFields(rule);

Map<String, FieldValue> evaluationData =
getEvaluationData(ruleId, flowContext, tenantDomain, fieldsInRule);
getEvaluationData(rule.getId(), flowContext, tenantDomain, fieldsInRule);

RuleEvaluator ruleEvaluator = new RuleEvaluator(RuleEvaluationComponentServiceHolder.getInstance()
.getOperatorRegistry());
Expand All @@ -95,7 +104,9 @@ private Map<String, FieldValue> getEvaluationData(String ruleId, FlowContext flo

return (evaluationDataList == null || evaluationDataList.isEmpty())
? Collections.emptyMap()
: evaluationDataList.stream().collect(Collectors.toMap(FieldValue::getName, fieldValue -> fieldValue));
: evaluationDataList.stream().collect(Collectors.toMap(
fieldValue -> FieldLookup.token(fieldValue.getName(), fieldValue.getQualifier()),
fieldValue -> fieldValue));
}

private Rule getRuleFromRuleManagementService(String ruleId, String tenantDomain)
Expand Down
Loading
Loading