Skip to content

Commit 068f605

Browse files
authored
Use compilation as validation for painless role template (#62845) (#63010)
* Use compilation as validation for painless role template (#62845) Role template validation now performs only compilation if the script is painless. It no longer attempts to execute the script with empty input which is problematic. The compliation process will catch things like invalid syntax, undefined variables, which still provide certain level of protection against ill-defined role templates. Behaviour for Mustache script is unchanged. * Checkstyle
1 parent 26f98f3 commit 068f605

File tree

3 files changed

+66
-2
lines changed

3 files changed

+66
-2
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/TemplateRoleName.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import org.elasticsearch.common.xcontent.XContentParser;
2525
import org.elasticsearch.common.xcontent.XContentType;
2626
import org.elasticsearch.common.xcontent.json.JsonXContent;
27+
import org.elasticsearch.script.Script;
2728
import org.elasticsearch.script.ScriptService;
29+
import org.elasticsearch.script.TemplateScript;
2830
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.ExpressionModel;
2931
import org.elasticsearch.xpack.core.security.support.MustacheTemplateEvaluator;
3032

@@ -99,7 +101,13 @@ public List<String> getRoleNames(ScriptService scriptService, ExpressionModel mo
99101

100102
public void validate(ScriptService scriptService) {
101103
try {
102-
parseTemplate(scriptService, Collections.emptyMap());
104+
final XContentParser parser = XContentHelper.createParser(
105+
NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, template, XContentType.JSON);
106+
final Script script = MustacheTemplateEvaluator.parseForScript(parser, Collections.emptyMap());
107+
final TemplateScript compiledTemplate = scriptService.compile(script, TemplateScript.CONTEXT).newInstance(script.getParams());
108+
if ("mustache".equals(script.getLang())) {
109+
compiledTemplate.execute();
110+
}
103111
} catch (IllegalArgumentException e) {
104112
throw e;
105113
} catch (IOException e) {

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/MustacheTemplateEvaluator.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ private MustacheTemplateEvaluator() {
2525
throw new UnsupportedOperationException("Cannot construct " + MustacheTemplateEvaluator.class);
2626
}
2727

28-
public static String evaluate(ScriptService scriptService, XContentParser parser, Map<String, Object> extraParams) throws IOException {
28+
public static Script parseForScript(XContentParser parser, Map<String, Object> extraParams) throws IOException {
2929
Script script = Script.parse(parser);
3030
// Add the user details to the params
3131
Map<String, Object> params = new HashMap<>();
@@ -36,6 +36,11 @@ public static String evaluate(ScriptService scriptService, XContentParser parser
3636
// Always enforce mustache script lang:
3737
script = new Script(script.getType(), script.getType() == ScriptType.STORED ? null : "mustache", script.getIdOrCode(),
3838
script.getOptions(), params);
39+
return script;
40+
}
41+
42+
public static String evaluate(ScriptService scriptService, XContentParser parser, Map<String, Object> extraParams) throws IOException {
43+
Script script = parseForScript(parser, extraParams);
3944
TemplateScript compiledTemplate = scriptService.compile(script, TemplateScript.CONTEXT).newInstance(script.getParams());
4045
return compiledTemplate.execute();
4146
}

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/support/mapper/TemplateRoleNameTests.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
2121
import org.elasticsearch.common.xcontent.XContentParser;
2222
import org.elasticsearch.common.xcontent.XContentType;
23+
import org.elasticsearch.script.ScriptEngine;
2324
import org.elasticsearch.script.ScriptException;
2425
import org.elasticsearch.script.ScriptMetadata;
2526
import org.elasticsearch.script.ScriptModule;
2627
import org.elasticsearch.script.ScriptService;
2728
import org.elasticsearch.script.StoredScriptSource;
29+
import org.elasticsearch.script.TemplateScript;
2830
import org.elasticsearch.script.mustache.MustacheScriptEngine;
2931
import org.elasticsearch.test.ESTestCase;
3032
import org.elasticsearch.test.EqualsHashCodeTestUtils;
@@ -40,7 +42,13 @@
4042
import static org.hamcrest.Matchers.containsString;
4143
import static org.hamcrest.Matchers.equalTo;
4244
import static org.hamcrest.Matchers.notNullValue;
45+
import static org.mockito.Matchers.any;
46+
import static org.mockito.Matchers.eq;
47+
import static org.mockito.Mockito.doThrow;
4348
import static org.mockito.Mockito.mock;
49+
import static org.mockito.Mockito.never;
50+
import static org.mockito.Mockito.times;
51+
import static org.mockito.Mockito.verify;
4452
import static org.mockito.Mockito.when;
4553

4654
public class TemplateRoleNameTests extends ESTestCase {
@@ -183,6 +191,49 @@ public void testValidateWillFailForSyntaxError() {
183191
assertTrue(e.getCause() instanceof ScriptException);
184192
}
185193

194+
public void testValidateWillCompileButNotExecutePainlessScript() {
195+
final TemplateScript compiledScript = mock(TemplateScript.class);
196+
doThrow(new IllegalStateException("Validate should not execute painless script")).when(compiledScript).execute();
197+
final TemplateScript.Factory scriptFactory = mock(TemplateScript.Factory.class);
198+
when(scriptFactory.newInstance(any())).thenReturn(compiledScript);
199+
200+
final ScriptEngine scriptEngine = mock(ScriptEngine.class);
201+
when(scriptEngine.getType()).thenReturn("painless");
202+
when(scriptEngine.compile(eq("valid"), eq("params.metedata.group"), any(),
203+
eq(org.elasticsearch.common.collect.Map.of())))
204+
.thenReturn(scriptFactory);
205+
final ScriptException scriptException =
206+
new ScriptException("exception", new IllegalStateException(), org.elasticsearch.common.collect.List.of(),
207+
"bad syntax", "painless");
208+
doThrow(scriptException)
209+
.when(scriptEngine).compile(eq("invalid"), eq("bad syntax"), any(),
210+
eq(org.elasticsearch.common.collect.Map.of()));
211+
212+
final ScriptService scriptService = new ScriptService(Settings.EMPTY,
213+
org.elasticsearch.common.collect.Map.of("painless", scriptEngine), ScriptModule.CORE_CONTEXTS) {
214+
@Override
215+
protected StoredScriptSource getScriptFromClusterState(String id) {
216+
if ("valid".equals(id)) {
217+
return new StoredScriptSource("painless", "params.metedata.group",
218+
org.elasticsearch.common.collect.Map.of());
219+
} else {
220+
return new StoredScriptSource("painless", "bad syntax",
221+
org.elasticsearch.common.collect.Map.of());
222+
}
223+
}
224+
};
225+
// Validation succeeds if compilation is successful
226+
new TemplateRoleName(new BytesArray("{ \"id\":\"valid\" }"), Format.STRING).validate(scriptService);
227+
verify(scriptEngine, times(1))
228+
.compile(eq("valid"), eq("params.metedata.group"), any(), eq(org.elasticsearch.common.collect.Map.of()));
229+
verify(compiledScript, never()).execute();
230+
231+
// Validation fails if compilation fails
232+
final IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
233+
() -> new TemplateRoleName(new BytesArray("{ \"id\":\"invalid\" }"), Format.STRING).validate(scriptService));
234+
assertSame(scriptException, e.getCause());
235+
}
236+
186237
public void testValidationWillFailWhenInlineScriptIsNotEnabled() {
187238
final Settings settings = Settings.builder().put("script.allowed_types", ScriptService.ALLOW_NONE).build();
188239
final ScriptService scriptService = new ScriptService(settings,

0 commit comments

Comments
 (0)