Skip to content

Validate role templates before saving role mapping #52636

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Mar 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3cce84d
Validate role templates before save
ywangd Feb 21, 2020
07c7819
No double wrap for illegal argument exception
ywangd Feb 21, 2020
a593def
Fix doc tests
ywangd Feb 21, 2020
3e2e4b4
Merge remote-tracking branch 'origin/master' into es-48773-role-mappi…
ywangd Feb 21, 2020
46cc8d3
Merge remote-tracking branch 'origin/master' into es-48773-role-mappi…
ywangd Feb 26, 2020
e07b243
Address feedback
ywangd Feb 26, 2020
51d9c20
Revert change to mustacheTemplateEvaluator
ywangd Feb 26, 2020
18333bb
Add more tests for role template validation
ywangd Feb 28, 2020
e4f8ec2
Merge remote-tracking branch 'origin/master' into es-48773-role-mappi…
ywangd Feb 28, 2020
d58a4c8
Fix tests and add doc
ywangd Feb 28, 2020
4587f14
Update x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc
ywangd Mar 1, 2020
7ed5322
Merge remote-tracking branch 'origin/master' into es-48773-role-mappi…
ywangd Mar 2, 2020
59b8405
Add example of using stored script for role templates
ywangd Mar 2, 2020
2f44946
Update x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc
ywangd Mar 2, 2020
2f84c1e
Update x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc
ywangd Mar 2, 2020
3f2512f
Update x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc
ywangd Mar 2, 2020
f9c0b48
Merge remote-tracking branch 'origin/master' into es-48773-role-mappi…
ywangd Mar 3, 2020
b6f68ba
[DOCS] Moves role templates into API description
lcawl Mar 3, 2020
c86d09b
Update x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc
lcawl Mar 3, 2020
d464d73
Merge remote-tracking branch 'origin/master' into es-48773-role-mappi…
ywangd Mar 24, 2020
fe0da67
Remove doc of using stored script for role template since we do not r…
ywangd Mar 24, 2020
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
54 changes: 27 additions & 27 deletions x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Creates and updates role mappings.
[[security-api-put-role-mapping-desc]]
==== {api-description-title}

Role mappings define which roles are assigned to each user. Each mapping has
Role mappings define which roles are assigned to each user. Each mapping has
_rules_ that identify users and a list of _roles_ that are granted to those users.

The role mapping APIs are generally the preferred way to manage role mappings
Expand All @@ -37,6 +37,31 @@ roles API>> or <<roles-management-file,roles files>>.

For more information, see <<mapping-roles>>.

[[_role_templates]]
===== Role templates

The most common use for role mappings is to create a mapping from a known value
on the user to a fixed role name. For example, all users in the
`cn=admin,dc=example,dc=com` LDAP group should be given the `superuser` role in
{es}. The `roles` field is used for this purpose.

For more complex needs, it is possible to use Mustache templates to dynamically
determine the names of the roles that should be granted to the user. The
`role_templates` field is used for this purpose.

NOTE: To use role templates successfully, the relevant scripting feature must be
enabled. Otherwise, all attempts to create a role mapping with role templates
fail. See <<allowed-script-types-setting>>.

All of the <<role-mapping-resources,user fields>> that are available in the
role mapping `rules` are also available in the role templates. Thus it is possible
to assign a user to a role that reflects their `username`, their `groups`, or the
name of the `realm` to which they authenticated.

By default a template is evaluated to produce a single string that is the name
of the role which should be assigned to the user. If the `format` of the template
is set to `"json"` then the template is expected to produce a JSON string or an
array of JSON strings for the role names.

[[security-api-put-role-mapping-path-params]]
==== {api-path-parms-title}
Expand Down Expand Up @@ -77,31 +102,7 @@ _Exactly one of `roles` or `role_templates` must be specified_.
`rules`::
(Required, object) The rules that determine which users should be matched by the
mapping. A rule is a logical condition that is expressed by using a JSON DSL.
See <<role-mapping-resources>>.

==== Role Templates

The most common use for role mappings is to create a mapping from a known value
on the user to a fixed role name.
For example, all users in the `cn=admin,dc=example,dc=com` LDAP group should be
given the `superuser` role in {es}.
The `roles` field is used for this purpose.

For more complex needs it is possible to use Mustache templates to dynamically
determine the names of the roles that should be granted to the user.
The `role_templates` field is used for this purpose.

All of the <<role-mapping-resources,user fields>> that are available in the
role mapping `rules` are also available in the role templates. Thus it is possible
to assign a user to a role that reflects their `username`, their `groups` or the
name of the `realm` to which they authenticated.

By default a template is evaluated to produce a single string that is the name
of the role which should be assigned to the user. If the `format` of the template
is set to `"json"` then the template is expected to produce a JSON string, or an
array of JSON strings for the role name(s).

The Examples section below demonstrates the use of templated role names.
See <<role-mapping-resources>>.

[[security-api-put-role-mapping-example]]
==== {api-examples-title}
Expand Down Expand Up @@ -339,4 +340,3 @@ POST /_security/role_mapping/mapping9
<1> Because it is not possible to specify both `roles` and `role_templates` in
the same role mapping, we can apply a "fixed name" role by using a template
that has no substitutions.

Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ public List<String> getRoleNames(ScriptService scriptService, ExpressionModel mo
}
}

public void validate(ScriptService scriptService) {
try {
parseTemplate(scriptService, Collections.emptyMap());
} catch (IllegalArgumentException e) {
throw e;
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}

private List<String> convertJsonToList(String evaluation) throws IOException {
final XContentParser parser = XContentFactory.xContent(XContentType.JSON).createParser(NamedXContentRegistry.EMPTY,
LoggingDeprecationHandler.INSTANCE, evaluation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

package org.elasticsearch.xpack.core.security.authc.support.mapper;

import org.elasticsearch.cluster.ClusterChangedEvent;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
Expand All @@ -17,8 +20,11 @@
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.script.ScriptException;
import org.elasticsearch.script.ScriptMetaData;
import org.elasticsearch.script.ScriptModule;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.script.StoredScriptSource;
import org.elasticsearch.script.mustache.MustacheScriptEngine;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils;
Expand All @@ -31,8 +37,11 @@
import java.util.Collections;

import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class TemplateRoleNameTests extends ESTestCase {

Expand Down Expand Up @@ -116,4 +125,112 @@ public void tryEquals(TemplateRoleName original) {
};
EqualsHashCodeTestUtils.checkEqualsAndHashCode(original, copy, mutate);
}

public void testValidate() {
final ScriptService scriptService = new ScriptService(Settings.EMPTY,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()), ScriptModule.CORE_CONTEXTS);

final TemplateRoleName plainString = new TemplateRoleName(new BytesArray("{ \"source\":\"heroes\" }"), Format.STRING);
plainString.validate(scriptService);

final TemplateRoleName user = new TemplateRoleName(new BytesArray("{ \"source\":\"_user_{{username}}\" }"), Format.STRING);
user.validate(scriptService);

final TemplateRoleName groups = new TemplateRoleName(new BytesArray("{ \"source\":\"{{#tojson}}groups{{/tojson}}\" }"),
Format.JSON);
groups.validate(scriptService);

final TemplateRoleName notObject = new TemplateRoleName(new BytesArray("heroes"), Format.STRING);
expectThrows(IllegalArgumentException.class, () -> notObject.validate(scriptService));

final TemplateRoleName invalidField = new TemplateRoleName(new BytesArray("{ \"foo\":\"heroes\" }"), Format.STRING);
expectThrows(IllegalArgumentException.class, () -> invalidField.validate(scriptService));
}

public void testValidateWillPassWithEmptyContext() {
final ScriptService scriptService = new ScriptService(Settings.EMPTY,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()), ScriptModule.CORE_CONTEXTS);

final BytesReference template = new BytesArray("{ \"source\":\"" +
"{{username}}/{{dn}}/{{realm}}/{{metadata}}" +
"{{#realm}}" +
" {{name}}/{{type}}" +
"{{/realm}}" +
"{{#toJson}}groups{{/toJson}}" +
"{{^groups}}{{.}}{{/groups}}" +
"{{#metadata}}" +
" {{#first}}" +
" <li><strong>{{name}}</strong></li>" +
" {{/first}}" +
" {{#link}}" +
" <li><a href=\\\"{{url}}\\\">{{name}}</a></li>" +
" {{/link}}" +
" {{#toJson}}subgroups{{/toJson}}" +
" {{something-else}}" +
"{{/metadata}}\" }");
final TemplateRoleName templateRoleName = new TemplateRoleName(template, Format.STRING);
templateRoleName.validate(scriptService);
}

public void testValidateWillFailForSyntaxError() {
final ScriptService scriptService = new ScriptService(Settings.EMPTY,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()), ScriptModule.CORE_CONTEXTS);

final BytesReference template = new BytesArray("{ \"source\":\" {{#not-closed}} {{other-variable}} \" }");

final IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> new TemplateRoleName(template, Format.STRING).validate(scriptService));
assertTrue(e.getCause() instanceof ScriptException);
}

public void testValidationWillFailWhenInlineScriptIsNotEnabled() {
final Settings settings = Settings.builder().put("script.allowed_types", ScriptService.ALLOW_NONE).build();
final ScriptService scriptService = new ScriptService(settings,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()), ScriptModule.CORE_CONTEXTS);
final BytesReference inlineScript = new BytesArray("{ \"source\":\"\" }");
final IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> new TemplateRoleName(inlineScript, Format.STRING).validate(scriptService));
assertThat(e.getMessage(), containsString("[inline]"));
}

public void testValidateWillFailWhenStoredScriptIsNotEnabled() {
final Settings settings = Settings.builder().put("script.allowed_types", ScriptService.ALLOW_NONE).build();
final ScriptService scriptService = new ScriptService(settings,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()), ScriptModule.CORE_CONTEXTS);
final ClusterChangedEvent clusterChangedEvent = mock(ClusterChangedEvent.class);
final ClusterState clusterState = mock(ClusterState.class);
final MetaData metaData = mock(MetaData.class);
final StoredScriptSource storedScriptSource = mock(StoredScriptSource.class);
final ScriptMetaData scriptMetaData = new ScriptMetaData.Builder(null).storeScript("foo", storedScriptSource).build();
when(clusterChangedEvent.state()).thenReturn(clusterState);
when(clusterState.metaData()).thenReturn(metaData);
when(metaData.custom(ScriptMetaData.TYPE)).thenReturn(scriptMetaData);
when(storedScriptSource.getLang()).thenReturn("mustache");
when(storedScriptSource.getSource()).thenReturn("");
when(storedScriptSource.getOptions()).thenReturn(Collections.emptyMap());
scriptService.applyClusterState(clusterChangedEvent);

final BytesReference storedScript = new BytesArray("{ \"id\":\"foo\" }");
final IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> new TemplateRoleName(storedScript, Format.STRING).validate(scriptService));
assertThat(e.getMessage(), containsString("[stored]"));
}

public void testValidateWillFailWhenStoredScriptIsNotFound() {
final ScriptService scriptService = new ScriptService(Settings.EMPTY,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()), ScriptModule.CORE_CONTEXTS);
final ClusterChangedEvent clusterChangedEvent = mock(ClusterChangedEvent.class);
final ClusterState clusterState = mock(ClusterState.class);
final MetaData metaData = mock(MetaData.class);
final ScriptMetaData scriptMetaData = new ScriptMetaData.Builder(null).build();
when(clusterChangedEvent.state()).thenReturn(clusterState);
when(clusterState.metaData()).thenReturn(metaData);
when(metaData.custom(ScriptMetaData.TYPE)).thenReturn(scriptMetaData);
scriptService.applyClusterState(clusterChangedEvent);

final BytesReference storedScript = new BytesArray("{ \"id\":\"foo\" }");
final IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> new TemplateRoleName(storedScript, Format.STRING).validate(scriptService));
assertThat(e.getMessage(), containsString("unable to find script"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappingRequest;
import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingRequest;
import org.elasticsearch.xpack.core.security.authc.support.mapper.ExpressionRoleMapping;
import org.elasticsearch.xpack.core.security.authc.support.mapper.TemplateRoleName;
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.ExpressionModel;
import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames;
import org.elasticsearch.xpack.core.security.authc.support.CachingRealm;
Expand Down Expand Up @@ -165,6 +166,10 @@ protected ExpressionRoleMapping buildMapping(String id, BytesReference source) {
* Stores (create or update) a single mapping in the index
*/
public void putRoleMapping(PutRoleMappingRequest request, ActionListener<Boolean> listener) {
// Validate all templates before storing the role mapping
for (TemplateRoleName templateRoleName : request.getRoleTemplates()) {
templateRoleName.validate(scriptService);
}
modifyMapping(request.getName(), this::innerPutMapping, request, listener);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheAction;
import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheRequest;
import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheResponse;
import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingRequest;
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
import org.elasticsearch.xpack.core.security.authc.RealmSettings;
Expand Down Expand Up @@ -210,6 +211,20 @@ public void testCacheIsNotClearedIfNoRealmsAreAttached() {
assertEquals(0, numInvalidation.get());
}

public void testPutRoleMappingWillValidateTemplateRoleNamesBeforeSave() {
final PutRoleMappingRequest putRoleMappingRequest = mock(PutRoleMappingRequest.class);
final TemplateRoleName templateRoleName = mock(TemplateRoleName.class);
final ScriptService scriptService = mock(ScriptService.class);
when(putRoleMappingRequest.getRoleTemplates()).thenReturn(Collections.singletonList(templateRoleName));
doAnswer(invocationOnMock -> {
throw new IllegalArgumentException();
}).when(templateRoleName).validate(scriptService);

final NativeRoleMappingStore nativeRoleMappingStore =
new NativeRoleMappingStore(Settings.EMPTY, mock(Client.class), mock(SecurityIndexManager.class), scriptService);
expectThrows(IllegalArgumentException.class, () -> nativeRoleMappingStore.putRoleMapping(putRoleMappingRequest, null));
}

private NativeRoleMappingStore buildRoleMappingStoreForInvalidationTesting(AtomicInteger invalidationCounter, boolean attachRealm) {
final Settings settings = Settings.builder().put("path.home", createTempDir()).build();

Expand Down