Skip to content

Commit

Permalink
Limit the ability to run Groovy scripts
Browse files Browse the repository at this point in the history
This resolves MID-9063.
  • Loading branch information
mederly committed Sep 12, 2023
1 parent a2e1e48 commit 342f94a
Show file tree
Hide file tree
Showing 13 changed files with 237 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
@Component
public class ConstExpressionEvaluatorFactory extends AbstractAutowiredExpressionEvaluatorFactory {

private static final QName ELEMENT_NAME = SchemaConstantsGenerated.C_CONST;
public static final QName ELEMENT_NAME = SchemaConstantsGenerated.C_CONST;

@Autowired private Protector protector;
@Autowired private ConstantsManager constantsManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
*/
public class PathExpressionEvaluatorFactory extends AbstractObjectResolvableExpressionEvaluatorFactory {

private static final QName ELEMENT_NAME = SchemaConstantsGenerated.C_PATH;
public static final QName ELEMENT_NAME = SchemaConstantsGenerated.C_PATH;

private final Protector protector;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ private ScriptExpressionProfile processScriptExpressionProfile(
if (expressionProfile.getDecision() == AccessDecision.ALLOW) {
return null;
} else {
throw new SecurityViolationException("Access to script expression evaluator " +
throw new SecurityViolationException("Access to script expression evaluator" +
" not allowed (expression profile: " + expressionProfile.getIdentifier() + ") in " + shortDesc);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,28 @@
import com.evolveum.midpoint.model.api.ModelService;
import com.evolveum.midpoint.model.api.PipelineItem;
import com.evolveum.midpoint.model.api.ScriptExecutionResult;
import com.evolveum.midpoint.model.common.expression.evaluator.ConstExpressionEvaluatorFactory;
import com.evolveum.midpoint.model.common.expression.evaluator.path.PathExpressionEvaluatorFactory;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.query.QueryConverter;
import com.evolveum.midpoint.repo.common.expression.evaluator.AsIsExpressionEvaluatorFactory;
import com.evolveum.midpoint.repo.common.expression.evaluator.LiteralExpressionEvaluatorFactory;
import com.evolveum.midpoint.schema.AccessDecision;
import com.evolveum.midpoint.schema.expression.ExpressionEvaluatorProfile;
import com.evolveum.midpoint.schema.expression.ExpressionProfile;
import com.evolveum.midpoint.schema.expression.VariablesMap;
import com.evolveum.midpoint.schema.util.MiscSchemaUtil;
import com.evolveum.midpoint.task.api.RunningTask;
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.util.exception.SystemException;
import com.evolveum.midpoint.util.exception.*;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.model.scripting_3.ScriptingExpressionEvaluationOptionsType;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.xml.namespace.QName;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -39,16 +51,20 @@ public class ExecutionContext {
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)
private final boolean recordProgressAndIterationStatistics;
/** Are we running under root principal? This is used to derive expression profiles in some cases. */
private final boolean root;

public ExecutionContext(ScriptingExpressionEvaluationOptionsType options, Task task,
ScriptingExpressionEvaluator scriptingExpressionEvaluator,
boolean privileged, boolean recordProgressAndIterationStatistics, VariablesMap initialVariables) {
boolean privileged, boolean recordProgressAndIterationStatistics, VariablesMap initialVariables,
boolean root) {
this.options = options;
this.task = task;
this.scriptingExpressionEvaluator = scriptingExpressionEvaluator;
this.privileged = privileged;
this.initialVariables = initialVariables;
this.recordProgressAndIterationStatistics = recordProgressAndIterationStatistics;
this.root = root;
}

public Task getTask() {
Expand Down Expand Up @@ -146,4 +162,29 @@ public boolean isPrivileged() {
public QueryConverter getQueryConverter() {
return getPrismContext().getQueryConverter();
}

@Nullable
public ExpressionProfile determineExpressionProfile()
throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException, CommunicationException,
ConfigurationException, SecurityViolationException {
if (root) {
return MiscSchemaUtil.getExpressionProfile(); // currently null, i.e. allowing everything
} else {
// Profile for unprivileged users. Only "safe" evaluators are allowed.
ExpressionProfile profile = new ExpressionProfile("##builtin-safe##");
profile.setDecision(AccessDecision.DENY);
profile.add(evaluatorAllowed(AsIsExpressionEvaluatorFactory.ELEMENT_NAME));
profile.add(evaluatorAllowed(PathExpressionEvaluatorFactory.ELEMENT_NAME));
profile.add(evaluatorAllowed(LiteralExpressionEvaluatorFactory.ELEMENT_NAME));
profile.add(evaluatorAllowed(ConstExpressionEvaluatorFactory.ELEMENT_NAME));
return profile;
}
}

@NotNull
private static ExpressionEvaluatorProfile evaluatorAllowed(QName elementName) {
ExpressionEvaluatorProfile profile = new ExpressionEvaluatorProfile(elementName);
profile.setDecision(AccessDecision.ALLOW);
return profile;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
package com.evolveum.midpoint.model.impl.scripting;

import com.evolveum.midpoint.model.api.ModelService;
import com.evolveum.midpoint.security.api.AuthorizationConstants;
import com.evolveum.midpoint.security.enforcer.api.AuthorizationParameters;
import com.evolveum.midpoint.security.enforcer.api.SecurityEnforcer;
import com.evolveum.midpoint.util.exception.ScriptExecutionException;
import com.evolveum.midpoint.model.impl.ModelObjectResolver;
import com.evolveum.midpoint.model.impl.scripting.expressions.FilterContentEvaluator;
Expand Down Expand Up @@ -64,6 +67,7 @@ public class ScriptingExpressionEvaluator {
@Autowired private ModelObjectResolver modelObjectResolver;
@Autowired private ExpressionFactory expressionFactory;
@Autowired public ScriptingActionExecutorRegistry actionExecutorRegistry;
@Autowired public SecurityEnforcer securityEnforcer;

/**
* Asynchronously executes any scripting expression.
Expand Down Expand Up @@ -133,8 +137,13 @@ private ExecutionContext evaluateExpression(@NotNull ExecuteScriptType executeSc
try {
VariablesMap frozenVariables = VariablesUtil.initialPreparation(initialVariables, executeScript.getVariables(), expressionFactory, modelObjectResolver, prismContext, expressionProfile, task, result);
PipelineData pipelineData = PipelineData.parseFrom(executeScript.getInput(), frozenVariables, prismContext);
ExecutionContext context = new ExecutionContext(executeScript.getOptions(), task, this,
privileged, recordProgressAndIterationStatistics, frozenVariables);
boolean root = securityEnforcer.isAuthorized(
AuthorizationConstants.AUTZ_ALL_URL, null,
AuthorizationParameters.EMPTY, null, task, result);
ExecutionContext context = new ExecutionContext(
executeScript.getOptions(), task, this,
privileged, recordProgressAndIterationStatistics, frozenVariables,
root);
PipelineData output = evaluateExpression(executeScript.getScriptingExpression().getValue(), pipelineData, context, result);
context.setFinalOutput(output);
result.computeStatusIfUnknown();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import com.evolveum.midpoint.schema.expression.VariablesMap;
import com.evolveum.midpoint.schema.constants.ExpressionConstants;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.util.MiscSchemaUtil;
import com.evolveum.midpoint.util.exception.*;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
Expand Down Expand Up @@ -127,9 +126,14 @@ private ObjectFilter resolveFilter(AssignmentHolderType object, PipelineItem ite
ExpressionEvaluationException, CommunicationException, ConfigurationException, SecurityViolationException {
if (parameters.staticFilter != null) {
return ExpressionUtil.evaluateFilterExpressions(
parameters.staticFilter, createVariables(object, item), MiscSchemaUtil.getExpressionProfile(),
expressionFactory, prismContext,
"expression evaluation in unassign filter for " + object, context.getTask(), result);
parameters.staticFilter,
createVariables(object, item),
context.determineExpressionProfile(),
expressionFactory,
prismContext,
"expression evaluation in unassign filter for " + object,
context.getTask(),
result);
} else {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ public <T extends ObjectType> PipelineData evaluate(SearchExpressionType searchE
throws ScriptExecutionException, SchemaException, ConfigurationException, ObjectNotFoundException, CommunicationException, SecurityViolationException, ExpressionEvaluationException {
Validate.notNull(searchExpression.getType());

ExpressionProfile expressionProfile = MiscSchemaUtil.getExpressionProfile();

List<PipelineItem> data = input.getData();
if (data.isEmpty()) {
// TODO fix this brutal hack (with dummyValue)
Expand Down Expand Up @@ -109,6 +107,7 @@ public <T extends ObjectType> PipelineData evaluate(SearchExpressionType searchE
//noinspection unchecked
item.getVariables().forEach((name, value) -> variables.put(name, cloneIfNecessary(name, value)));
try {
ExpressionProfile expressionProfile = context.determineExpressionProfile();
objectQuery = ExpressionUtil
.evaluateQueryExpressions(unresolvedObjectQuery, variables, expressionProfile, expressionFactory, prismContext,
"bulk action query", context.getTask(), globalResult);
Expand Down Expand Up @@ -167,5 +166,4 @@ public <T extends ObjectType> PipelineData evaluate(SearchExpressionType searchE
}
return outputData;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@

import com.evolveum.midpoint.audit.api.AuditEventRecord;
import com.evolveum.midpoint.audit.api.AuditEventStage;
import com.evolveum.midpoint.model.api.ModelAuthorizationAction;
import com.evolveum.midpoint.model.impl.scripting.ExecutionContext;
import com.evolveum.midpoint.prism.delta.ObjectDelta;
import com.evolveum.midpoint.prism.query.ObjectQuery;
import com.evolveum.midpoint.schema.constants.SchemaConstants;
import com.evolveum.midpoint.schema.expression.VariablesMap;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.result.OperationResultStatus;
import com.evolveum.midpoint.schema.util.ExceptionUtil;
import com.evolveum.midpoint.schema.util.ObjectTypeUtil;
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.test.TestResource;
import com.evolveum.midpoint.util.DebugUtil;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.exception.*;
import com.evolveum.midpoint.xml.ns._public.common.common_3.*;
import com.evolveum.midpoint.xml.ns._public.model.scripting_3.ExecuteScriptType;
import com.evolveum.midpoint.xml.ns._public.model.scripting_3.ScriptingExpressionType;
Expand All @@ -27,6 +31,7 @@
import org.testng.annotations.Test;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

Expand All @@ -48,6 +53,10 @@ public class TestScriptingBasicNew extends AbstractBasicScriptingTest {
private static final File UNASSIGN_ALL_FROM_JACK_FILE = new File(TEST_DIR, "unassign-all-from-jack.xml");
private static final File EXECUTE_CUSTOM_DELTA = new File(TEST_DIR, "execute-custom-delta.xml");

private static final File SEARCH_WITH_SCRIPT_OK = new File(TEST_DIR, "search-with-script-ok.xml");
private static final File SEARCH_WITH_SCRIPT_BAD = new File(TEST_DIR, "search-with-script-bad.xml");
private static final File UNASSIGN_WITH_SCRIPT_BAD = new File(TEST_DIR, "unassign-with-script-bad.xml");

private static final TestResource<TaskType> TASK_DELETE_SHADOWS_MULTINODE = new TestResource<>(TEST_DIR, "task-delete-shadows-multinode.xml", "931e34be-5cf0-46c6-8cc1-90812a66d5cb");

@Override
Expand Down Expand Up @@ -274,4 +283,80 @@ private int countDummyAccountShadows(OperationResult result) throws SchemaExcept
DebugUtil.debugDump(repositoryService.searchObjects(ShadowType.class, query, null, result)));
return repositoryService.countObjects(ShadowType.class, query, null, result);
}

/**
* Check for execution of groovy script as part of search filter at various places.
*/
@Test
public void test920CheckScriptExecution() throws Exception {
Task task = getTestTask();
OperationResult result = task.getResult();

given("a user able to run bulk actions");
RoleType role = new RoleType()
.name(getTestNameShort())
.authorization(new AuthorizationType()
.action(ModelAuthorizationAction.EXECUTE_SCRIPT.getUrl()))
.authorization(new AuthorizationType()
.action(ModelAuthorizationAction.READ.getUrl()));
addObject(role.asPrismObject(), task, result);

String regularUserName = getTestNameShort();
UserType user = new UserType()
.name(regularUserName)
.assignment(ObjectTypeUtil.createAssignmentTo(role, SchemaConstants.ORG_DEFAULT));
addObject(user, task, result);

checkSearchWithScriptExecuted(regularUserName, SEARCH_WITH_SCRIPT_OK);
checkSearchWithScriptNotExecuted(regularUserName, SEARCH_WITH_SCRIPT_BAD);
checkSearchWithScriptExecuted(USER_ADMINISTRATOR_USERNAME, SEARCH_WITH_SCRIPT_OK);
checkSearchWithScriptExecuted(USER_ADMINISTRATOR_USERNAME, SEARCH_WITH_SCRIPT_BAD);

checkUnassignWithScriptNotExecuted(regularUserName, UNASSIGN_WITH_SCRIPT_BAD);
}

private void checkSearchWithScriptExecuted(String userName, File file) throws CommonException, IOException {
ExecutionContext output = execute(userName, file);
int objects = output.getFinalOutput().getData().size();
assertThat(objects).as("Objects found").isEqualTo(1);
}

private ExecutionContext execute(String userName, File file) throws CommonException, IOException {
Task task = getTestTask();
OperationResult result = task.getResult();

when("logged in as " + userName);
login(userName);

and("executing " + file);
ExecuteScriptType executeScriptBean = parseExecuteScript(file);
return evaluator.evaluateExpression(
executeScriptBean, VariablesMap.emptyMap(), false, task, result);
}

@SuppressWarnings("SameParameterValue")
private void checkSearchWithScriptNotExecuted(String userName, File file) throws CommonException, IOException {
try {
execute(userName, file);
fail("unexpected success");
} catch (ScriptExecutionException e) {
assertNoScriptsAllowed(e);
}
}

private void assertNoScriptsAllowed(ScriptExecutionException e) {
SecurityViolationException cause = ExceptionUtil.findCause(e, SecurityViolationException.class);
assertExpectedException(cause)
.hasMessageContaining("Access to script expression evaluator not allowed");
}

@SuppressWarnings("SameParameterValue")
private void checkUnassignWithScriptNotExecuted(String userName, File file) throws CommonException, IOException {
try {
execute(userName, file);
fail("unexpected success");
} catch (ScriptExecutionException e) {
assertNoScriptsAllowed(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
~ Copyright (c) 2010-2017 Evolveum and contributors
~
~ This work is dual-licensed under the Apache License 2.0
~ and European Union Public License. See LICENSE file for details.
-->


<s:executeScript xmlns:s="http://midpoint.evolveum.com/xml/ns/public/model/scripting-3"
xmlns:c="http://midpoint.evolveum.com/xml/ns/public/common/common-3"
xmlns:q="http://prism.evolveum.com/xml/ns/public/query-3">
<s:search>
<s:type>UserType</s:type>
<s:searchFilter>
<q:equal>
<q:path>name</q:path>
<c:expression>
<c:script>
<c:code>'administrator'</c:code>
</c:script>
</c:expression>
</q:equal>
</s:searchFilter>
</s:search>
</s:executeScript>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
~ Copyright (c) 2010-2017 Evolveum and contributors
~
~ This work is dual-licensed under the Apache License 2.0
~ and European Union Public License. See LICENSE file for details.
-->


<s:executeScript xmlns:s="http://midpoint.evolveum.com/xml/ns/public/model/scripting-3"
xmlns:c="http://midpoint.evolveum.com/xml/ns/public/common/common-3"
xmlns:q="http://prism.evolveum.com/xml/ns/public/query-3">
<s:search>
<s:type>UserType</s:type>
<s:searchFilter>
<q:equal>
<q:path>name</q:path>
<c:expression>
<c:value>administrator</c:value>
</c:expression>
</q:equal>
</s:searchFilter>
</s:search>
</s:executeScript>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
~ Copyright (c) 2020 Evolveum and contributors
~
~ This work is dual-licensed under the Apache License 2.0
~ and European Union Public License. See LICENSE file for details.
-->

<s:executeScript xmlns:s="http://midpoint.evolveum.com/xml/ns/public/model/scripting-3"
xmlns:c="http://midpoint.evolveum.com/xml/ns/public/common/common-3"
xmlns:q="http://prism.evolveum.com/xml/ns/public/query-3">
<s:search>
<s:type>UserType</s:type>
<s:searchFilter>
<q:equal>
<path>name</path>
<value>jack</value>
</q:equal>
</s:searchFilter>
<s:unassign>
<s:filter>
<q:equal>
<q:path>subtype</q:path>
<c:expression>
<c:script>
<c:code>null</c:code>
</c:script>
</c:expression>
</q:equal>
</s:filter>
</s:unassign>
</s:search>
</s:executeScript>

0 comments on commit 342f94a

Please sign in to comment.