Skip to content

Commit

Permalink
Implement scripting (bulk action) checks
Browse files Browse the repository at this point in the history
The profiles were there, this commit brings the actual checks.

Also, fixed OperationResult handling in "generate value" processing
and security policy determination in ModelInteractionServiceImpl.

Related to MID-6913.
  • Loading branch information
mederly committed Aug 11, 2023
1 parent 4bb54d9 commit 2eab7f3
Show file tree
Hide file tree
Showing 33 changed files with 652 additions and 174 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.evolveum.midpoint.util.exception.ConfigurationException;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ScriptingActionProfileType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ScriptingProfileType;
import org.jetbrains.annotations.Nullable;

/**
* Specifies limitations on the use of a scripting actions. It is a compiled form of a {@link ScriptingProfileType}.
Expand All @@ -26,7 +27,7 @@
*/
public class ScriptingProfile extends AbstractSecurityProfile {

/** Scripting actions profiles, keyed by action name. Unmodifiable. */
/** Scripting actions profiles, keyed by action name (both legacy and modern ones can be used). Unmodifiable. */
@NotNull private final Map<String, ScriptingActionProfile> actionProfiles;

/** "Allow all" profile. */
Expand Down Expand Up @@ -72,7 +73,18 @@ public static ScriptingProfile of(@NotNull ScriptingProfileType bean) throws Con
Collections.unmodifiableMap(actionProfileMap));
}

public @NotNull Map<String, ScriptingActionProfile> getActionProfiles() {
return actionProfiles;
public @NotNull AccessDecision decideActionAccess(
@NotNull String legacyActionName, @Nullable String configurationElementName) {
var byLegacyName = actionProfiles.get(legacyActionName);
if (byLegacyName != null) {
return byLegacyName.decision();
}
if (configurationElementName != null) {
var byConfigurationElementName = actionProfiles.get(configurationElementName);
if (byConfigurationElementName != null) {
return byConfigurationElementName.decision();
}
}
return getDefaultDecision();
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@
/**
* Executes an action of a given type. Instances of this type must be registered with ScriptingExpressionEvaluator.
*/
@FunctionalInterface
public interface ActionExecutor {

/**
* Checks if the execution is allowed; we may put this inside the {@link #execute(ActionExpressionType, PipelineData,
* ExecutionContext, OperationResult)} method later, if needed.
*/
void checkExecutionAllowed(ExecutionContext context) throws SecurityViolationException;

/**
* Executes given action command.
*
Expand All @@ -34,5 +39,6 @@ public interface ActionExecutor {
* individual pipeline items processing.)
*/
PipelineData execute(ActionExpressionType command, PipelineData input, ExecutionContext context, OperationResult globalResult)
throws ScriptExecutionException, SchemaException, ConfigurationException, ObjectNotFoundException, CommunicationException, SecurityViolationException, ExpressionEvaluationException;
throws ScriptExecutionException, SchemaException, ConfigurationException, ObjectNotFoundException,
CommunicationException, SecurityViolationException, ExpressionEvaluationException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,9 @@ private PipelineData executeAction(
} else if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Executing action {}", getActionType(action));
}
return actionExecutorRegistry.getExecutor(action)
.execute(action, input, context, globalResult);
ActionExecutor executor = actionExecutorRegistry.getExecutor(action);
executor.checkExecutionAllowed(context);
return executor.execute(action, input, context, globalResult);
}

private PipelineData executePipeline(ExpressionPipelineType pipeline, PipelineData data, ExecutionContext context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ abstract class AbstractObjectBasedActionExecutor<T extends ObjectType> extends B

@FunctionalInterface
public interface ObjectProcessor<T extends ObjectType> {
void process(PrismObject<? extends T> object, PipelineItem item, OperationResult result) throws ScriptExecutionException, CommonException;
void process(PrismObject<? extends T> object, PipelineItem item, OperationResult result) throws CommonException;
}

@FunctionalInterface
Expand All @@ -55,38 +55,40 @@ void iterateOverObjects(PipelineData input, ExecutionContext context, OperationR
consumer.process(object, item, result);
operationsHelper.recordEnd(context, op, null, result);
} catch (Throwable e) {
result.recordFatalError(e);
result.recordException(e);
operationsHelper.recordEnd(context, op, e, result);
Throwable exception = processActionException(e, getActionName(), value, context);
Throwable exception = processActionException(e, getLegacyActionName(), value, context);
writer.write(object, exception);
}
}
operationsHelper.trimAndCloneResult(result, item.getResult());
} catch (Throwable t) {
result.recordFatalError(t);
result.recordException(t);
throw t;
} finally {
result.computeStatusIfUnknown(); // just in case (should be already computed)
result.close(); // just in case (should be already computed)
}
}
}

@SuppressWarnings("ThrowableNotThrown")
private PrismObject<T> castToObject(PrismValue value, Class<T> expectedType, ExecutionContext context)
throws ScriptExecutionException {
if (value instanceof PrismObjectValue) {
PrismObjectValue objectValue = (PrismObjectValue) value;
if (value instanceof PrismObjectValue<?> objectValue) {
Class<? extends Objectable> realType = objectValue.asObjectable().getClass();
if (expectedType.isAssignableFrom(realType)) {
//noinspection unchecked
return objectValue.asPrismObject();
return (PrismObject<T>) objectValue.asPrismObject();
} else {
processActionException(new ScriptExecutionException("Item is not a PrismObject of " + expectedType.getName()
+ "; it is " + realType.getName() + " instead"), getActionName(), value, context);
processActionException(
new ScriptExecutionException(
"Item is not a PrismObject of %s; it is %s instead".formatted(
expectedType.getName(), realType.getName())),
getLegacyActionName(), value, context);
return null;
}
} else {
processActionException(new ScriptExecutionException("Item is not a PrismObject"), getActionName(), value, context);
processActionException(new ScriptExecutionException("Item is not a PrismObject"), getLegacyActionName(), value, context);
return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package com.evolveum.midpoint.model.impl.scripting.actions;

import com.evolveum.midpoint.model.api.ModelExecuteOptions;
import com.evolveum.midpoint.schema.SchemaConstantsGenerated;
import com.evolveum.midpoint.util.exception.ScriptExecutionException;
import com.evolveum.midpoint.model.impl.scripting.PipelineData;
import com.evolveum.midpoint.model.impl.scripting.ExecutionContext;
Expand All @@ -21,6 +22,7 @@
import com.evolveum.midpoint.xml.ns._public.model.scripting_3.ActionExpressionType;
import com.evolveum.midpoint.xml.ns._public.model.scripting_3.AddActionExpressionType;

import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;

import java.util.Collection;
Expand Down Expand Up @@ -75,7 +77,12 @@ Class<ObjectType> getObjectType() {
}

@Override
String getActionName() {
@NotNull String getLegacyActionName() {
return NAME;
}

@Override
@NotNull String getConfigurationElementName() {
return SchemaConstantsGenerated.SC_ADD.getLocalPart();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

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

import com.evolveum.midpoint.schema.SchemaConstantsGenerated;
import com.evolveum.midpoint.util.exception.ScriptExecutionException;
import com.evolveum.midpoint.model.impl.scripting.ExecutionContext;
import com.evolveum.midpoint.model.impl.scripting.PipelineData;
Expand All @@ -18,6 +19,7 @@
import com.evolveum.midpoint.xml.ns._public.model.scripting_3.ActionExpressionType;
import com.evolveum.midpoint.xml.ns._public.model.scripting_3.ApplyDefinitionActionExpressionType;

import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;

import jakarta.annotation.PostConstruct;
Expand Down Expand Up @@ -65,7 +67,12 @@ protected Class<ObjectType> getObjectType() {
}

@Override
protected String getActionName() {
protected @NotNull String getLegacyActionName() {
return NAME;
}

@Override
@NotNull String getConfigurationElementName() {
return SchemaConstantsGenerated.SC_APPLY_DEFINITION.getLocalPart();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

import com.evolveum.midpoint.schema.SchemaConstantsGenerated;
import jakarta.annotation.PostConstruct;
import javax.xml.namespace.QName;

Expand All @@ -29,6 +31,7 @@

import com.evolveum.midpoint.xml.ns._public.model.scripting_3.AssignActionExpressionType;

import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;

import com.evolveum.midpoint.prism.PrismConstants;
Expand All @@ -55,7 +58,7 @@ public class AssignExecutor extends AssignmentOperationsExecutor<AssignParameter
* They are created by merging dynamically and statically defined parameters, resolving
* filters in references, and so on.
*/
static class AssignParameters extends AssignmentOperationsExecutor.Parameters {
protected static class AssignParameters extends AssignmentOperationsExecutor.Parameters {
private final List<ObjectReferenceType> targetRefs = new ArrayList<>();
private final List<ConstructionType> constructions = new ArrayList<>();
}
Expand Down Expand Up @@ -182,19 +185,19 @@ private QName getEffectiveRelation(ObjectReferenceType reference, QName relation

private Collection<ConstructionType> resourceRefsToConstructions(Collection<ObjectReferenceType> resourceRefs) {
return resourceRefs.stream()
.map(ref -> new ConstructionType(prismContext).resourceRef(ref.clone()))
.map(ref -> new ConstructionType().resourceRef(ref.clone()))
.collect(Collectors.toList());
}

private Collection<AssignmentType> targetsToAssignments(Collection<ObjectReferenceType> targetRefs) {
return targetRefs.stream()
.map(ref -> new AssignmentType(prismContext).targetRef(ref.clone()))
.map(ref -> new AssignmentType().targetRef(ref.clone()))
.collect(Collectors.toList());
}

private Collection<AssignmentType> constructionsToAssignments(Collection<ConstructionType> constructions) {
return constructions.stream()
.map(c -> new AssignmentType(prismContext).construction(c.clone()))
.map(c -> new AssignmentType().construction(c.clone()))
.collect(Collectors.toList());
}

Expand All @@ -213,7 +216,12 @@ protected ObjectDelta<? extends ObjectType> createDelta(AssignmentHolderType obj
}

@Override
protected String getActionName() {
protected @NotNull String getLegacyActionName() {
return NAME;
}

@Override
@NotNull String getConfigurationElementName() {
return SchemaConstantsGenerated.SC_ASSIGN.getLocalPart();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ protected Class<AssignmentHolderType> getObjectType() {
ExecutionContext context, OperationResult globalResult) throws ScriptExecutionException, SchemaException,
ConfigurationException, ObjectNotFoundException, CommunicationException, SecurityViolationException,
ExpressionEvaluationException {
ActionParameterValueType roleParameterValue = expressionHelper.getArgument(action.getParameter(), PARAM_ROLE,
false, false, getActionName());
ActionParameterValueType roleParameterValue = expressionHelper.getArgument(
action.getParameter(), PARAM_ROLE, false, false, getLegacyActionName());
if (roleParameterValue != null) {
PipelineData data = expressionHelper.evaluateParameter(roleParameterValue, null, input, context, globalResult);
// if somebody wants to assign Org, he has to use full reference value (including object type)
Expand All @@ -108,8 +108,8 @@ protected Class<AssignmentHolderType> getObjectType() {
ExecutionContext context, OperationResult globalResult) throws ScriptExecutionException, SchemaException,
ConfigurationException, ObjectNotFoundException, CommunicationException, SecurityViolationException,
ExpressionEvaluationException {
ActionParameterValueType resourceParameterValue = expressionHelper.getArgument(action.getParameter(), PARAM_RESOURCE,
false, false, getActionName());
ActionParameterValueType resourceParameterValue = expressionHelper.getArgument(
action.getParameter(), PARAM_RESOURCE, false, false, getLegacyActionName());
if (resourceParameterValue != null) {
PipelineData data = expressionHelper
.evaluateParameter(resourceParameterValue, null, input, context, globalResult);
Expand All @@ -123,7 +123,7 @@ protected Class<AssignmentHolderType> getObjectType() {
OperationResult globalResult) throws ScriptExecutionException, SchemaException, ConfigurationException,
ObjectNotFoundException, CommunicationException, SecurityViolationException, ExpressionEvaluationException {
Collection<String> relationSpecificationUris = expressionHelper.getArgumentValues(action.getParameter(), PARAM_RELATION,
false, false, getActionName(), input, context, String.class, globalResult);
false, false, getLegacyActionName(), input, context, String.class, globalResult);
return relationSpecificationUris.stream()
.map(uri -> QNameUtil.uriToQName(uri, true))
.collect(Collectors.toSet());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@

import static com.evolveum.midpoint.model.impl.scripting.VariablesUtil.cloneIfNecessary;

import com.evolveum.midpoint.schema.AccessDecision;
import com.evolveum.midpoint.schema.statistics.Operation;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

Expand Down Expand Up @@ -68,6 +70,20 @@ public abstract class BaseActionExecutor implements ActionExecutor {
@Autowired protected MatchingRuleRegistry matchingRuleRegistry;
@Autowired protected SchemaService schemaService;

/**
* Returns the name used to invoke this action in a dynamic way, e.g. `execute-script`, `generate-value`, etc.
*
* TODO should we really call this "legacy"? Not all actions have their "modern" names.
* */
abstract @NotNull String getLegacyActionName();

/**
* Returns the name used to invoke this action in a static way, e.g. `execute`, `generateValue`, etc.
*
* Not all actions have such a name; e.g. `reencrypt` has not.
*/
abstract @Nullable String getConfigurationElementName();

private String optionsSuffix(ModelExecuteOptions options) {
return options.notEmpty() ? " " + options : "";
}
Expand Down Expand Up @@ -128,7 +144,7 @@ void iterateOverItems(PipelineData input, ExecutionContext context, OperationRes
} catch (Throwable ex) {
result.recordFatalError(ex);
operationsHelper.recordEnd(context, op, ex, result);
Throwable exception = processActionException(ex, getActionName(), value, context);
Throwable exception = processActionException(ex, getLegacyActionName(), value, context);
writer.write(value, exception);
}
operationsHelper.trimAndCloneResult(result, item.getResult());
Expand All @@ -147,8 +163,6 @@ String getDescription(PrismValue value) {
}
}

abstract String getActionName();

/**
* Creates variables for script evaluation based on some externally-supplied variables,
* plus some generic ones (prism context, actor).
Expand All @@ -164,4 +178,28 @@ String getDescription(PrismValue value) {

return variables;
}

@Override
public void checkExecutionAllowed(ExecutionContext context) throws SecurityViolationException {

var expressionProfile = context.getExpressionProfile();
var scriptingProfile = expressionProfile.getScriptingProfile();

String legacyName = getLegacyActionName();
String modernName = getConfigurationElementName();
var decision = scriptingProfile.decideActionAccess(legacyName, modernName);
var names = modernName != null ?
"'%s' ('%s')".formatted(legacyName, modernName) :
"'%s'".formatted(legacyName);

if (decision != AccessDecision.ALLOW) {
throw new SecurityViolationException(
"Access to action %s %s (applied expression profile '%s', actions profile '%s')"
.formatted(
names,
decision == AccessDecision.DENY ? "denied" : "not allowed",
expressionProfile.getIdentifier(),
scriptingProfile.getIdentifier()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.evolveum.midpoint.model.api.ModelExecuteOptions;
import com.evolveum.midpoint.model.impl.scripting.PipelineData;
import com.evolveum.midpoint.model.impl.scripting.ExecutionContext;
import com.evolveum.midpoint.schema.SchemaConstantsGenerated;
import com.evolveum.midpoint.util.exception.ScriptExecutionException;
import com.evolveum.midpoint.prism.delta.ObjectDelta;
import com.evolveum.midpoint.schema.result.OperationResult;
Expand All @@ -19,6 +20,7 @@

import com.evolveum.midpoint.xml.ns._public.model.scripting_3.DeleteActionExpressionType;

import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;

import jakarta.annotation.PostConstruct;
Expand Down Expand Up @@ -68,7 +70,12 @@ Class<ObjectType> getObjectType() {
}

@Override
String getActionName() {
@NotNull String getLegacyActionName() {
return NAME;
}

@Override
@NotNull String getConfigurationElementName() {
return SchemaConstantsGenerated.SC_DELETE.getLocalPart();
}
}

0 comments on commit 2eab7f3

Please sign in to comment.