Skip to content

Commit

Permalink
Add "evaluateExpression" bulk action
Browse files Browse the repository at this point in the history
Now we can evaluate arbitrary expressions from within bulk actions.

Work in progress:

1. Only single return values are supported.
2. Expression profile determination is not part of this commit.
Root authorization is therefore required for now.
3. Tests are quite rudimentary yet.
  • Loading branch information
mederly committed Aug 7, 2023
1 parent 300910b commit f27af03
Show file tree
Hide file tree
Showing 20 changed files with 815 additions and 276 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -677,10 +677,10 @@
</xsd:annotation>
</xsd:element>

<xsd:complexType name="ExecuteScriptActionExpressionType">
<xsd:complexType name="AbstractExecuteActionExpressionType">
<xsd:annotation>
<xsd:documentation>
Statically-typed "execute-script" action.
Statically-typed "execute-script" or "evaluate-expression" action.
</xsd:documentation>
<xsd:appinfo>
<a:since>4.2</a:since>
Expand All @@ -690,13 +690,6 @@
<xsd:complexContent>
<xsd:extension base="tns:ActionExpressionType">
<xsd:sequence>
<xsd:element name="script" type="c:ScriptExpressionEvaluatorType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Script to be executed.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="outputItemName" type="xsd:QName" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Expand Down Expand Up @@ -731,6 +724,31 @@
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>

<xsd:complexType name="ExecuteScriptActionExpressionType">
<xsd:annotation>
<xsd:documentation>
Statically-typed "execute-script" action.
</xsd:documentation>
<xsd:appinfo>
<a:since>4.2</a:since>
<a:experimental>true</a:experimental>
</xsd:appinfo>
</xsd:annotation>
<xsd:complexContent>
<xsd:extension base="tns:AbstractExecuteActionExpressionType">
<xsd:sequence>
<xsd:element name="script" type="c:ScriptExpressionEvaluatorType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Script to be executed.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="execute" type="tns:ExecuteScriptActionExpressionType" substitutionGroup="tns:scriptingExpression">
<xsd:annotation>
<xsd:appinfo>
Expand All @@ -739,6 +757,37 @@
</xsd:annotation>
</xsd:element>

<xsd:complexType name="EvaluateExpressionActionExpressionType">
<xsd:annotation>
<xsd:documentation>
Statically-typed "evaluate-expression" action.
</xsd:documentation>
<xsd:appinfo>
<a:since>4.8</a:since>
</xsd:appinfo>
</xsd:annotation>
<xsd:complexContent>
<xsd:extension base="tns:AbstractExecuteActionExpressionType">
<xsd:sequence>
<xsd:element name="expression" type="c:ExpressionType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Expression to be evaluated.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="evaluateExpression" type="tns:EvaluateExpressionActionExpressionType" substitutionGroup="tns:scriptingExpression">
<xsd:annotation>
<xsd:appinfo>
<a:heterogeneousListItem/>
</xsd:appinfo>
</xsd:annotation>
</xsd:element>

<xsd:complexType name="GenerateValueActionExpressionType">
<xsd:annotation>
<xsd:documentation>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.evolveum.midpoint.model.api.ScriptExecutionResult;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.query.QueryConverter;
import com.evolveum.midpoint.schema.config.ExecuteScriptConfigItem;
import com.evolveum.midpoint.schema.expression.VariablesMap;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.task.api.RunningTask;
Expand All @@ -34,7 +35,8 @@ public class ExecutionContext {

/**
* Are we pre-authorized for dangerous operations like Groovy script execution? See
* {@link ScriptingExpressionEvaluator#evaluateExpressionPrivileged(ExecuteScriptType, VariablesMap, Task, OperationResult)}.
* {@link ScriptingExpressionEvaluator#evaluateExpressionPrivileged(ExecuteScriptConfigItem,
* VariablesMap, Task, OperationResult)}.
*
* TEMPORARY. To be replaced.
*/
Expand All @@ -43,9 +45,12 @@ public class ExecutionContext {
private final Task task;
private final ScriptingExpressionEvaluator scriptingExpressionEvaluator;
private final StringBuilder consoleOutput = new StringBuilder();
private final Map<String, PipelineData> globalVariables = new HashMap<>(); // will probably remain unused
private final VariablesMap initialVariables; // used e.g. when there are no data in a pipeline; these are frozen - i.e. made immutable if possible; to be cloned-on-use
private PipelineData finalOutput; // used only when passing result to external clients (TODO do this more cleanly)
/** will probably remain unused */
private final Map<String, PipelineData> globalVariables = new HashMap<>();
/** used e.g. when there are no data in a pipeline; these are frozen - i.e. made immutable if possible; to be cloned-on-use */
private final VariablesMap initialVariables;
/** used only when passing result to external clients (TODO do this more cleanly) */
private PipelineData finalOutput;
private final boolean recordProgressAndIterationStatistics;

public ExecutionContext(ScriptingExpressionEvaluationOptionsType options, Task task,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/*
* Copyright (C) 2010-2023 Evolveum and contributors
*
* This work is dual-licensed under the Apache License 2.0
* and European Union Public License. See LICENSE file for details.
*/

package com.evolveum.midpoint.model.impl.scripting.actions;

import java.util.Collection;
import java.util.function.Function;
import javax.xml.namespace.QName;

import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;

import com.evolveum.midpoint.model.api.PipelineItem;
import com.evolveum.midpoint.model.impl.lens.LensContext;
import com.evolveum.midpoint.model.impl.scripting.ExecutionContext;
import com.evolveum.midpoint.model.impl.scripting.PipelineData;
import com.evolveum.midpoint.prism.*;
import com.evolveum.midpoint.prism.xml.XmlTypeConverter;
import com.evolveum.midpoint.schema.SchemaConstantsGenerated;
import com.evolveum.midpoint.schema.constants.ExpressionConstants;
import com.evolveum.midpoint.schema.expression.TypedValue;
import com.evolveum.midpoint.schema.expression.VariablesMap;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.util.QNameUtil;
import com.evolveum.midpoint.util.exception.*;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType;
import com.evolveum.midpoint.xml.ns._public.model.scripting_3.AbstractExecuteActionExpressionType;
import com.evolveum.midpoint.xml.ns._public.model.scripting_3.ActionExpressionType;

/**
* Executes either `execute-script` or `evaluate-expression` actions.
*/
abstract class AbstractExecuteExecutor<P extends AbstractExecuteExecutor.Parameters>
extends BaseActionExecutor {

private static final String PARAM_QUIET = "quiet"; // could be useful for other actions as well
private static final String PARAM_OUTPUT_ITEM = "outputItem"; // item name or type (as URI!) -- EXPERIMENTAL
private static final String PARAM_FOR_WHOLE_INPUT = "forWholeInput";

P getParameters(
ActionExpressionType action, PipelineData input, ExecutionContext context,
OperationResult globalResult, Function<Parameters, P> function)
throws CommunicationException, ObjectNotFoundException, SchemaException,
ScriptExecutionException, SecurityViolationException, ConfigurationException, ExpressionEvaluationException {

ItemDefinition<?> outputDefinition;
String outputItemUri = expressionHelper.getSingleArgumentValue(
action.getParameter(), PARAM_OUTPUT_ITEM, false, false,
getName(), input, context, String.class, globalResult);
if (StringUtils.isNotBlank(outputItemUri)) {
outputDefinition = getItemDefinition(outputItemUri);
} else if (action instanceof AbstractExecuteActionExpressionType execute) {
if (execute.getOutputItemName() != null) {
outputDefinition = getItemDefinitionFromItemName(execute.getOutputItemName());
} else if (execute.getOutputTypeName() != null) {
outputDefinition = getItemDefinitionFromTypeName(execute.getOutputTypeName());
} else {
outputDefinition = null;
}
} else {
outputDefinition = null;
}
boolean forWholeInput = expressionHelper.getActionArgument(
Boolean.class, action,
AbstractExecuteActionExpressionType.F_FOR_WHOLE_INPUT, PARAM_FOR_WHOLE_INPUT,
input, context, false, PARAM_FOR_WHOLE_INPUT, globalResult);
boolean quiet = expressionHelper.getActionArgument(
Boolean.class, action,
AbstractExecuteActionExpressionType.F_QUIET, PARAM_QUIET,
input, context, false, PARAM_QUIET, globalResult);

return function.apply(new Parameters(outputDefinition, forWholeInput, quiet));
}

@NotNull abstract String getName();

private @NotNull ItemDefinition<?> getItemDefinition(String uri) throws ScriptExecutionException {
QName name = QNameUtil.uriToQName(uri, true);
ItemDefinition<?> byName = prismContext.getSchemaRegistry().findItemDefinitionByElementName(name);
if (byName != null) {
return byName;
}

ItemDefinition<?> byType = prismContext.getSchemaRegistry().findItemDefinitionByType(name);
if (byType != null) {
return byType;
}

throw new ScriptExecutionException(
"Supplied item identification '" + uri + "' corresponds neither to item name nor type name");
}

private @NotNull ItemDefinition<?> getItemDefinitionFromItemName(QName itemName) throws ScriptExecutionException {
ItemDefinition<?> def = prismContext.getSchemaRegistry().findItemDefinitionByElementName(itemName);
if (def != null) {
return def;
}
throw new ScriptExecutionException("Item with name '" + itemName + "' couldn't be found.");
}

private @NotNull ItemDefinition<?> getItemDefinitionFromTypeName(QName typeName) throws ScriptExecutionException {
ItemDefinition<?> byType = prismContext.getSchemaRegistry().findItemDefinitionByType(typeName);
if (byType != null) {
return byType;
}

if (XmlTypeConverter.canConvert(typeName)) {
return prismContext.definitionFactory().createPropertyDefinition(SchemaConstantsGenerated.C_VALUE, typeName);
}

TypeDefinition typeDef = prismContext.getSchemaRegistry().findTypeDefinitionByType(typeName);
if (typeDef instanceof SimpleTypeDefinition) {
return prismContext.definitionFactory().createPropertyDefinition(SchemaConstantsGenerated.C_VALUE, typeName);
} else if (typeDef instanceof ComplexTypeDefinition ctd) {
if (ctd.isContainerMarker() || ctd.isObjectMarker()) {
return prismContext.definitionFactory().createContainerDefinition(SchemaConstantsGenerated.C_VALUE, ctd);
} else {
return prismContext.definitionFactory().createPropertyDefinition(SchemaConstantsGenerated.C_VALUE, typeName);
}
} else if (typeDef != null) {
throw new ScriptExecutionException("Type with name '" + typeName + "' couldn't be used as output type: " + typeDef);
} else {
throw new ScriptExecutionException("Type with name '" + typeName + "' couldn't be found.");
}
}


@NotNull PipelineData executeInternal(
PipelineData input, P parameters, ExecutionContext context, OperationResult globalResult)
throws ScriptExecutionException {
PipelineData output = PipelineData.createEmpty();
if (parameters.forWholeInput) {
executeForWholeInput(input, output, parameters, context, globalResult);
} else {
iterateOverItems(input, context, globalResult,
(value, item, result) ->
processItem(context, parameters, output, item, value, result),
(value, exception) ->
context.println("Failed to execute script/expression on "
+ getDescription(value) + exceptionSuffix(exception)));
}
return output;
}

private void executeForWholeInput(
PipelineData input, PipelineData output, P parameters, ExecutionContext context,
OperationResult globalResult) throws ScriptExecutionException {
OperationResult result = operationsHelper.createActionResult(null, this, globalResult);
context.checkTaskStop();
try {
TypedValue<PipelineData> inputTypedValue = new TypedValue<>(input, PipelineData.class);
Object outObject = doSingleExecution(parameters, inputTypedValue, context.getInitialVariables(), context, result);
if (outObject != null) {
addToData(outObject, PipelineData.newOperationResult(), output);
} else {
// no definition means we don't plan to provide any output - so let's just copy the input item to the output
// (actually, null definition with non-null outObject should not occur)
if (parameters.outputDefinition == null) {
output.addAllFrom(input);
}
}
if (!parameters.quiet) {
context.println("Executed script/expression on the pipeline");
}

} catch (Throwable ex) {
Throwable exception = processActionException(ex, getName(), null, context); // TODO value for error reporting (3rd parameter)
context.println("Failed to execute script/expression on the pipeline" + exceptionSuffix(exception));
}
operationsHelper.trimAndCloneResult(result, null);
}

private void processItem(
ExecutionContext context, P parameters,
PipelineData output, PipelineItem item, PrismValue value, OperationResult result)
throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException, CommunicationException,
ConfigurationException, SecurityViolationException {

// Hack. TODO: we need to add definitions to Pipeline items.
@SuppressWarnings({ "unchecked", "rawtypes" })
TypedValue<?> typedValue = new TypedValue(value, value == null ? Object.class : value.getClass());
Object outObject = doSingleExecution(parameters, typedValue, item.getVariables(), context, result);
if (outObject != null) {
addToData(outObject, item.getResult(), output);
} else {
// no definition means we don't plan to provide any output - so let's just copy the input item to the output
// (actually, null definition with non-null outObject should not occur)
if (parameters.outputDefinition == null) {
output.add(item);
}
}
if (!parameters.quiet) {
context.println("Executed script/expression on " + getDescription(value));
}
}

abstract <I> Object doSingleExecution(P parameters, TypedValue<I> inputTypedValue,
VariablesMap externalVariables, ExecutionContext context, OperationResult result)
throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException, CommunicationException,
ConfigurationException, SecurityViolationException;

private void addToData(@NotNull Object outObject, @NotNull OperationResult result, PipelineData output) {
if (outObject instanceof Collection) {
for (Object o : (Collection<?>) outObject) {
addToData(o, result, output);
}
} else {
PrismValue value;
if (outObject instanceof PrismValue) {
value = (PrismValue) outObject;
} else if (outObject instanceof Objectable) {
value = prismContext.itemFactory().createObjectValue((Objectable) outObject);
} else if (outObject instanceof Containerable) {
value = prismContext.itemFactory().createContainerValue((Containerable) outObject);
} else {
value = prismContext.itemFactory().createPropertyValue(outObject);
}
output.add(new PipelineItem(value, result));
}
}

// TODO implement seriously! This implementation requires custom modelContext variable that might or might not be present
// (it is set e.g. for policy rule script execution)
<F extends ObjectType> LensContext<F> getLensContext(VariablesMap externalVariables) {
TypedValue<?> modelContextTypedValue = externalVariables.get(ExpressionConstants.VAR_MODEL_CONTEXT);
//noinspection unchecked
return modelContextTypedValue != null ? (LensContext<F>) modelContextTypedValue.getValue() : null;
}

/**
* Parameters for `execute-script` and `evaluate-expression` actions.
*/
static class Parameters {

final ItemDefinition<?> outputDefinition;
final boolean forWholeInput;
final boolean quiet;

Parameters(
ItemDefinition<?> outputDefinition,
boolean forWholeInput,
boolean quiet) {
this.outputDefinition = outputDefinition;
this.forWholeInput = forWholeInput;
this.quiet = quiet;
}
}
}

0 comments on commit f27af03

Please sign in to comment.