From 2c770ba3cb66b345e5029ae146c7c5336d902fee Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Tue, 2 Apr 2019 20:55:10 +1100 Subject: [PATCH] Support mustache templates in role mappings (#40571) This adds a new `role_templates` field to role mappings that is an alternative to the existing roles field. These templates are evaluated at runtime to determine which roles should be granted to a user. For example, it is possible to specify: "role_templates": [ { "template":{ "source": "_user_{{username}}" } } ] which would mean that every user is assigned to their own role based on their username. You may not specify both roles and role_templates in the same role mapping. This commit adds support for templates to the role mapping API, the role mapping engine, the Java high level rest client, and Elasticsearch documentation. Due to the lack of caching in our role mapping store, it is currently inefficient to use a large number of templated role mappings. This will be addressed in a future change. Backport of: #39984, #40504 --- .../security/ExpressionRoleMapping.java | 72 ++--- .../security/PutRoleMappingRequest.java | 39 ++- .../client/security/TemplateRoleName.java | 117 ++++++++ .../SecurityRequestConvertersTests.java | 4 +- .../SecurityDocumentationIT.java | 55 ++-- .../security/ExpressionRoleMappingTests.java | 54 ++-- .../GetRoleMappingsResponseTests.java | 18 +- .../security/PutRoleMappingRequestTests.java | 134 ++++++--- .../security/create-role-mappings.asciidoc | 127 ++++++++- .../xpack/core/XPackClientPlugin.java | 2 +- .../rolemapping/PutRoleMappingRequest.java | 36 ++- .../PutRoleMappingRequestBuilder.java | 14 +- .../support/mapper/ExpressionRoleMapping.java | 113 ++++++-- .../support/mapper/TemplateRoleName.java | 211 ++++++++++++++ .../mapper/expressiondsl/ExpressionModel.java | 28 +- .../mapper/expressiondsl/FieldExpression.java | 17 ++ .../SecurityQueryTemplateEvaluator.java | 20 +- .../support/MustacheTemplateEvaluator.java | 42 +++ .../resources/security-index-template.json | 10 + .../support/mapper/TemplateRoleNameTests.java | 119 ++++++++ .../xpack/security/Security.java | 7 +- .../mapper/NativeRoleMappingStore.java | 31 ++- .../xpack/security/SecurityTests.java | 3 +- .../TransportPutRoleMappingActionTests.java | 6 +- .../authc/kerberos/KerberosRealmTestCase.java | 4 +- .../security/authc/ldap/LdapRealmTests.java | 80 ++++++ .../mapper/ExpressionRoleMappingTests.java | 261 +++++++++++++++--- .../mapper/NativeRoleMappingStoreTests.java | 29 +- .../api/security.put_role_mapping.json | 2 +- 29 files changed, 1364 insertions(+), 291 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/TemplateRoleName.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/TemplateRoleName.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/MustacheTemplateEvaluator.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/support/mapper/TemplateRoleNameTests.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/ExpressionRoleMapping.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/ExpressionRoleMapping.java index 9cb78dd9c83..447c67abe32 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/ExpressionRoleMapping.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/ExpressionRoleMapping.java @@ -29,8 +29,10 @@ import org.elasticsearch.common.xcontent.XContentParser; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; /** * A representation of a single role-mapping. @@ -42,13 +44,14 @@ public final class ExpressionRoleMapping { @SuppressWarnings("unchecked") static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("role-mapping", true, - (args, name) -> new ExpressionRoleMapping(name, (RoleMapperExpression) args[0], (List) args[1], - (Map) args[2], (boolean) args[3])); + (args, name) -> new ExpressionRoleMapping(name, (RoleMapperExpression) args[0], (List) args[1], + (List) args[2], (Map) args[3], (boolean) args[4])); static { PARSER.declareField(constructorArg(), (parser, context) -> RoleMapperExpressionParser.fromXContent(parser), Fields.RULES, ObjectParser.ValueType.OBJECT); - PARSER.declareStringArray(constructorArg(), Fields.ROLES); + PARSER.declareStringArray(optionalConstructorArg(), Fields.ROLES); + PARSER.declareObjectArray(optionalConstructorArg(), (parser, ctx) -> TemplateRoleName.fromXContent(parser), Fields.ROLE_TEMPLATES); PARSER.declareField(constructorArg(), XContentParser::map, Fields.METADATA, ObjectParser.ValueType.OBJECT); PARSER.declareBoolean(constructorArg(), Fields.ENABLED); } @@ -56,6 +59,7 @@ public final class ExpressionRoleMapping { private final String name; private final RoleMapperExpression expression; private final List roles; + private final List roleTemplates; private final Map metadata; private final boolean enabled; @@ -70,10 +74,11 @@ public final class ExpressionRoleMapping { * @param enabled a flag when {@code true} signifies the role mapping is active */ public ExpressionRoleMapping(final String name, final RoleMapperExpression expr, final List roles, - final Map metadata, boolean enabled) { + final List templates, final Map metadata, boolean enabled) { this.name = name; this.expression = expr; - this.roles = Collections.unmodifiableList(roles); + this.roles = roles == null ? Collections.emptyList() : Collections.unmodifiableList(roles); + this.roleTemplates = templates == null ? Collections.emptyList() : Collections.unmodifiableList(templates); this.metadata = (metadata == null) ? Collections.emptyMap() : Collections.unmodifiableMap(metadata); this.enabled = enabled; } @@ -90,6 +95,10 @@ public final class ExpressionRoleMapping { return roles; } + public List getRoleTemplates() { + return roleTemplates; + } + public Map getMetadata() { return metadata; } @@ -99,53 +108,26 @@ public final class ExpressionRoleMapping { } @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + (enabled ? 1231 : 1237); - result = prime * result + ((expression == null) ? 0 : expression.hashCode()); - result = prime * result + ((metadata == null) ? 0 : metadata.hashCode()); - result = prime * result + ((name == null) ? 0 : name.hashCode()); - result = prime * result + ((roles == null) ? 0 : roles.hashCode()); - return result; + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final ExpressionRoleMapping that = (ExpressionRoleMapping) o; + return this.enabled == that.enabled && + Objects.equals(this.name, that.name) && + Objects.equals(this.expression, that.expression) && + Objects.equals(this.roles, that.roles) && + Objects.equals(this.roleTemplates, that.roleTemplates) && + Objects.equals(this.metadata, that.metadata); } @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - final ExpressionRoleMapping other = (ExpressionRoleMapping) obj; - if (enabled != other.enabled) - return false; - if (expression == null) { - if (other.expression != null) - return false; - } else if (!expression.equals(other.expression)) - return false; - if (metadata == null) { - if (other.metadata != null) - return false; - } else if (!metadata.equals(other.metadata)) - return false; - if (name == null) { - if (other.name != null) - return false; - } else if (!name.equals(other.name)) - return false; - if (roles == null) { - if (other.roles != null) - return false; - } else if (!roles.equals(other.roles)) - return false; - return true; + public int hashCode() { + return Objects.hash(name, expression, roles, roleTemplates, metadata, enabled); } public interface Fields { ParseField ROLES = new ParseField("roles"); + ParseField ROLE_TEMPLATES = new ParseField("role_templates"); ParseField ENABLED = new ParseField("enabled"); ParseField RULES = new ParseField("rules"); ParseField METADATA = new ParseField("metadata"); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutRoleMappingRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutRoleMappingRequest.java index b8da17da72d..9a9e0fa62f9 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutRoleMappingRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutRoleMappingRequest.java @@ -40,22 +40,34 @@ public final class PutRoleMappingRequest implements Validatable, ToXContentObjec private final String name; private final boolean enabled; private final List roles; + private final List roleTemplates; private final RoleMapperExpression rules; private final Map metadata; private final RefreshPolicy refreshPolicy; + @Deprecated public PutRoleMappingRequest(final String name, final boolean enabled, final List roles, final RoleMapperExpression rules, - @Nullable final Map metadata, @Nullable final RefreshPolicy refreshPolicy) { + @Nullable final Map metadata, @Nullable final RefreshPolicy refreshPolicy) { + this(name, enabled, roles, Collections.emptyList(), rules, metadata, refreshPolicy); + } + + public PutRoleMappingRequest(final String name, final boolean enabled, final List roles, final List templates, + final RoleMapperExpression rules, @Nullable final Map metadata, + @Nullable final RefreshPolicy refreshPolicy) { if (Strings.hasText(name) == false) { throw new IllegalArgumentException("role-mapping name is missing"); } this.name = name; this.enabled = enabled; - if (roles == null || roles.isEmpty()) { - throw new IllegalArgumentException("role-mapping roles are missing"); + this.roles = Collections.unmodifiableList(Objects.requireNonNull(roles, "role-mapping roles cannot be null")); + this.roleTemplates = Collections.unmodifiableList(Objects.requireNonNull(templates, "role-mapping role_templates cannot be null")); + if (this.roles.isEmpty() && this.roleTemplates.isEmpty()) { + throw new IllegalArgumentException("in a role-mapping, one of roles or role_templates is required"); + } + if (this.roles.isEmpty() == false && this.roleTemplates.isEmpty() == false) { + throw new IllegalArgumentException("in a role-mapping, cannot specify both roles and role_templates"); } - this.roles = Collections.unmodifiableList(roles); this.rules = Objects.requireNonNull(rules, "role-mapping rules are missing"); this.metadata = (metadata == null) ? Collections.emptyMap() : metadata; this.refreshPolicy = (refreshPolicy == null) ? RefreshPolicy.getDefault() : refreshPolicy; @@ -73,6 +85,10 @@ public final class PutRoleMappingRequest implements Validatable, ToXContentObjec return roles; } + public List getRoleTemplates() { + return roleTemplates; + } + public RoleMapperExpression getRules() { return rules; } @@ -87,7 +103,7 @@ public final class PutRoleMappingRequest implements Validatable, ToXContentObjec @Override public int hashCode() { - return Objects.hash(name, enabled, refreshPolicy, roles, rules, metadata); + return Objects.hash(name, enabled, refreshPolicy, roles, roleTemplates, rules, metadata); } @Override @@ -104,11 +120,12 @@ public final class PutRoleMappingRequest implements Validatable, ToXContentObjec final PutRoleMappingRequest other = (PutRoleMappingRequest) obj; return (enabled == other.enabled) && - (refreshPolicy == other.refreshPolicy) && - Objects.equals(name, other.name) && - Objects.equals(roles, other.roles) && - Objects.equals(rules, other.rules) && - Objects.equals(metadata, other.metadata); + (refreshPolicy == other.refreshPolicy) && + Objects.equals(name, other.name) && + Objects.equals(roles, other.roles) && + Objects.equals(roleTemplates, other.roleTemplates) && + Objects.equals(rules, other.rules) && + Objects.equals(metadata, other.metadata); } @Override @@ -116,9 +133,9 @@ public final class PutRoleMappingRequest implements Validatable, ToXContentObjec builder.startObject(); builder.field("enabled", enabled); builder.field("roles", roles); + builder.field("role_templates", roleTemplates); builder.field("rules", rules); builder.field("metadata", metadata); return builder.endObject(); } - } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/TemplateRoleName.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/TemplateRoleName.java new file mode 100644 index 00000000000..a6263cee69d --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/TemplateRoleName.java @@ -0,0 +1,117 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParserUtils; +import org.elasticsearch.common.xcontent.XContentType; + +import java.io.IOException; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * A role name that uses a dynamic template. + */ +public class TemplateRoleName implements ToXContentObject { + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("template-role-name", + true, args -> new TemplateRoleName((String) args[0], (Format) args[1])); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), Fields.TEMPLATE); + PARSER.declareField(optionalConstructorArg(), Format::fromXContent, Fields.FORMAT, ObjectParser.ValueType.STRING); + } + private final String template; + private final Format format; + + public TemplateRoleName(String template, Format format) { + this.template = Objects.requireNonNull(template); + this.format = Objects.requireNonNull(format); + } + + public TemplateRoleName(Map template, Format format) throws IOException { + this(Strings.toString(XContentBuilder.builder(XContentType.JSON.xContent()).map(template)), format); + } + + public String getTemplate() { + return template; + } + + public Format getFormat() { + return format; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final TemplateRoleName that = (TemplateRoleName) o; + return Objects.equals(this.template, that.template) && + this.format == that.format; + } + + @Override + public int hashCode() { + return Objects.hash(template, format); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject() + .field(Fields.TEMPLATE.getPreferredName(), template) + .field(Fields.FORMAT.getPreferredName(), format.name().toLowerCase(Locale.ROOT)) + .endObject(); + } + + static TemplateRoleName fromXContent(XContentParser parser) throws IOException { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser::getTokenLocation); + return PARSER.parse(parser, null); + } + + + public enum Format { + STRING, JSON; + + private static Format fromXContent(XContentParser parser) throws IOException { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.VALUE_STRING, parser.currentToken(), parser::getTokenLocation); + return Format.valueOf(parser.text().toUpperCase(Locale.ROOT)); + } + } + + public interface Fields { + ParseField TEMPLATE = new ParseField("template"); + ParseField FORMAT = new ParseField("format"); + } + +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java index b2c2028d0fb..1176cabcc3d 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java @@ -141,8 +141,8 @@ public class SecurityRequestConvertersTests extends ESTestCase { .addExpression(FieldRoleMapperExpression.ofUsername(username)) .addExpression(FieldRoleMapperExpression.ofGroups(groupname)) .build(); - final PutRoleMappingRequest putRoleMappingRequest = new PutRoleMappingRequest(roleMappingName, true, Collections.singletonList( - rolename), rules, null, refreshPolicy); + final PutRoleMappingRequest putRoleMappingRequest = new PutRoleMappingRequest(roleMappingName, true, + Collections.singletonList(rolename), Collections.emptyList(), rules, null, refreshPolicy); final Request request = SecurityRequestConverters.putRoleMapping(putRoleMappingRequest); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java index 1afe6382fa5..b095ca5a9a0 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java @@ -75,6 +75,7 @@ import org.elasticsearch.client.security.PutRoleResponse; import org.elasticsearch.client.security.PutUserRequest; import org.elasticsearch.client.security.PutUserResponse; import org.elasticsearch.client.security.RefreshPolicy; +import org.elasticsearch.client.security.TemplateRoleName; import org.elasticsearch.client.security.support.ApiKey; import org.elasticsearch.client.security.support.CertificateInfo; import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression; @@ -94,6 +95,8 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.set.Sets; import org.hamcrest.Matchers; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; import java.io.IOException; import java.time.Instant; import java.util.ArrayList; @@ -108,9 +111,6 @@ import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; - import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; @@ -120,6 +120,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isIn; +import static org.hamcrest.Matchers.iterableWithSize; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; @@ -366,8 +367,8 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase { .addExpression(FieldRoleMapperExpression.ofUsername("*")) .addExpression(FieldRoleMapperExpression.ofGroups("cn=admins,dc=example,dc=com")) .build(); - final PutRoleMappingRequest request = new PutRoleMappingRequest("mapping-example", true, Collections.singletonList("superuser"), - rules, null, RefreshPolicy.NONE); + final PutRoleMappingRequest request = new PutRoleMappingRequest("mapping-example", true, + Collections.singletonList("superuser"), Collections.emptyList(), rules, null, RefreshPolicy.NONE); final PutRoleMappingResponse response = client.security().putRoleMapping(request, RequestOptions.DEFAULT); // end::put-role-mapping-execute // tag::put-role-mapping-response @@ -381,7 +382,8 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase { .addExpression(FieldRoleMapperExpression.ofUsername("*")) .addExpression(FieldRoleMapperExpression.ofGroups("cn=admins,dc=example,dc=com")) .build(); - final PutRoleMappingRequest request = new PutRoleMappingRequest("mapping-example", true, Collections.singletonList("superuser"), + final PutRoleMappingRequest request = new PutRoleMappingRequest("mapping-example", true, Collections.emptyList(), + Collections.singletonList(new TemplateRoleName("{\"source\":\"{{username}}\"}", TemplateRoleName.Format.STRING)), rules, null, RefreshPolicy.NONE); // tag::put-role-mapping-execute-listener ActionListener listener = new ActionListener() { @@ -397,25 +399,32 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase { }; // end::put-role-mapping-execute-listener + // avoid unused local warning + assertNotNull(listener); + // Replace the empty listener by a blocking listener in test - final CountDownLatch latch = new CountDownLatch(1); - listener = new LatchedActionListener<>(listener, latch); + final PlainActionFuture future = new PlainActionFuture<>(); + listener = future; // tag::put-role-mapping-execute-async client.security().putRoleMappingAsync(request, RequestOptions.DEFAULT, listener); // <1> // end::put-role-mapping-execute-async - assertTrue(latch.await(30L, TimeUnit.SECONDS)); + assertThat(future.get(), notNullValue()); + assertThat(future.get().isCreated(), is(false)); } } public void testGetRoleMappings() throws Exception { final RestHighLevelClient client = highLevelClient(); + final TemplateRoleName monitoring = new TemplateRoleName("{\"source\":\"monitoring\"}", TemplateRoleName.Format.STRING); + final TemplateRoleName template = new TemplateRoleName("{\"source\":\"{{username}}\"}", TemplateRoleName.Format.STRING); + final RoleMapperExpression rules1 = AnyRoleMapperExpression.builder().addExpression(FieldRoleMapperExpression.ofUsername("*")) .addExpression(FieldRoleMapperExpression.ofGroups("cn=admins,dc=example,dc=com")).build(); - final PutRoleMappingRequest putRoleMappingRequest1 = new PutRoleMappingRequest("mapping-example-1", true, Collections.singletonList( - "superuser"), rules1, null, RefreshPolicy.NONE); + final PutRoleMappingRequest putRoleMappingRequest1 = new PutRoleMappingRequest("mapping-example-1", true, Collections.emptyList(), + Arrays.asList(monitoring, template), rules1, null, RefreshPolicy.NONE); final PutRoleMappingResponse putRoleMappingResponse1 = client.security().putRoleMapping(putRoleMappingRequest1, RequestOptions.DEFAULT); boolean isCreated1 = putRoleMappingResponse1.isCreated(); @@ -424,8 +433,8 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase { "cn=admins,dc=example,dc=com")).build(); final Map metadata2 = new HashMap<>(); metadata2.put("k1", "v1"); - final PutRoleMappingRequest putRoleMappingRequest2 = new PutRoleMappingRequest("mapping-example-2", true, Collections.singletonList( - "monitoring"), rules2, metadata2, RefreshPolicy.NONE); + final PutRoleMappingRequest putRoleMappingRequest2 = new PutRoleMappingRequest("mapping-example-2", true, + Arrays.asList("superuser"), Collections.emptyList(), rules2, metadata2, RefreshPolicy.NONE); final PutRoleMappingResponse putRoleMappingResponse2 = client.security().putRoleMapping(putRoleMappingRequest2, RequestOptions.DEFAULT); boolean isCreated2 = putRoleMappingResponse2.isCreated(); @@ -445,7 +454,9 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase { assertThat(mappings.get(0).getName(), is("mapping-example-1")); assertThat(mappings.get(0).getExpression(), equalTo(rules1)); assertThat(mappings.get(0).getMetadata(), equalTo(Collections.emptyMap())); - assertThat(mappings.get(0).getRoles(), contains("superuser")); + assertThat(mappings.get(0).getRoles(), iterableWithSize(0)); + assertThat(mappings.get(0).getRoleTemplates(), iterableWithSize(2)); + assertThat(mappings.get(0).getRoleTemplates(), containsInAnyOrder(monitoring, template)); } { @@ -462,11 +473,13 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase { if (roleMapping.getName().equals("mapping-example-1")) { assertThat(roleMapping.getMetadata(), equalTo(Collections.emptyMap())); assertThat(roleMapping.getExpression(), equalTo(rules1)); - assertThat(roleMapping.getRoles(), contains("superuser")); + assertThat(roleMapping.getRoles(), emptyIterable()); + assertThat(roleMapping.getRoleTemplates(), contains(monitoring, template)); } else { assertThat(roleMapping.getMetadata(), equalTo(metadata2)); assertThat(roleMapping.getExpression(), equalTo(rules2)); - assertThat(roleMapping.getRoles(), contains("monitoring")); + assertThat(roleMapping.getRoles(), contains("superuser")); + assertThat(roleMapping.getRoleTemplates(), emptyIterable()); } } } @@ -485,11 +498,13 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase { if (roleMapping.getName().equals("mapping-example-1")) { assertThat(roleMapping.getMetadata(), equalTo(Collections.emptyMap())); assertThat(roleMapping.getExpression(), equalTo(rules1)); - assertThat(roleMapping.getRoles(), contains("superuser")); + assertThat(roleMapping.getRoles(), emptyIterable()); + assertThat(roleMapping.getRoleTemplates(), containsInAnyOrder(monitoring, template)); } else { assertThat(roleMapping.getMetadata(), equalTo(metadata2)); assertThat(roleMapping.getExpression(), equalTo(rules2)); - assertThat(roleMapping.getRoles(), contains("monitoring")); + assertThat(roleMapping.getRoles(), contains("superuser")); + assertThat(roleMapping.getRoleTemplates(), emptyIterable()); } } } @@ -1093,8 +1108,8 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase { { // Create role mappings final RoleMapperExpression rules = FieldRoleMapperExpression.ofUsername("*"); - final PutRoleMappingRequest request = new PutRoleMappingRequest("mapping-example", true, Collections.singletonList("superuser"), - rules, null, RefreshPolicy.NONE); + final PutRoleMappingRequest request = new PutRoleMappingRequest("mapping-example", true, + Collections.singletonList("superuser"), Collections.emptyList(), rules, null, RefreshPolicy.NONE); final PutRoleMappingResponse response = client.security().putRoleMapping(request, RequestOptions.DEFAULT); boolean isCreated = response.isCreated(); assertTrue(isCreated); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/ExpressionRoleMappingTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/ExpressionRoleMappingTests.java index 29bc7812f5b..f30307ebde5 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/ExpressionRoleMappingTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/ExpressionRoleMappingTests.java @@ -31,6 +31,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import static java.util.Collections.singletonList; import static org.hamcrest.Matchers.equalTo; public class ExpressionRoleMappingTests extends ESTestCase { @@ -59,48 +60,53 @@ public class ExpressionRoleMappingTests extends ESTestCase { public void usedDeprecatedField(String usedName, String replacedWith) { } }, json), "example-role-mapping"); - final ExpressionRoleMapping expectedRoleMapping = new ExpressionRoleMapping("example-role-mapping", FieldRoleMapperExpression - .ofKeyValues("realm.name", "kerb1"), Collections.singletonList("superuser"), null, true); + final ExpressionRoleMapping expectedRoleMapping = new ExpressionRoleMapping("example-role-mapping", + FieldRoleMapperExpression.ofKeyValues("realm.name", "kerb1"), + singletonList("superuser"), Collections.emptyList(), + null, true); assertThat(expressionRoleMapping, equalTo(expectedRoleMapping)); } public void testEqualsHashCode() { - final ExpressionRoleMapping expressionRoleMapping = new ExpressionRoleMapping("kerberosmapping", FieldRoleMapperExpression - .ofKeyValues("realm.name", "kerb1"), Collections.singletonList("superuser"), null, true); - EqualsHashCodeTestUtils.checkEqualsAndHashCode(expressionRoleMapping, (original) -> { - return new ExpressionRoleMapping(original.getName(), original.getExpression(), original.getRoles(), original.getMetadata(), - original.isEnabled()); - }); - EqualsHashCodeTestUtils.checkEqualsAndHashCode(expressionRoleMapping, (original) -> { - return new ExpressionRoleMapping(original.getName(), original.getExpression(), original.getRoles(), original.getMetadata(), - original.isEnabled()); - }, ExpressionRoleMappingTests::mutateTestItem); + final ExpressionRoleMapping expressionRoleMapping = new ExpressionRoleMapping("kerberosmapping", + FieldRoleMapperExpression.ofKeyValues("realm.name", "kerb1"), + singletonList("superuser"), Collections.emptyList(), + null, true); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(expressionRoleMapping, original -> + new ExpressionRoleMapping(original.getName(), original.getExpression(), original.getRoles(), original.getRoleTemplates(), + original.getMetadata(), original.isEnabled()), ExpressionRoleMappingTests::mutateTestItem); } - private static ExpressionRoleMapping mutateTestItem(ExpressionRoleMapping original) { + private static ExpressionRoleMapping mutateTestItem(ExpressionRoleMapping original) throws IOException { ExpressionRoleMapping mutated = null; - switch (randomIntBetween(0, 4)) { + switch (randomIntBetween(0, 5)) { case 0: - mutated = new ExpressionRoleMapping("namechanged", FieldRoleMapperExpression.ofKeyValues("realm.name", "kerb1"), Collections - .singletonList("superuser"), null, true); + mutated = new ExpressionRoleMapping("namechanged", FieldRoleMapperExpression.ofKeyValues("realm.name", "kerb1"), + singletonList("superuser"), Collections.emptyList(), null, true); break; case 1: - mutated = new ExpressionRoleMapping("kerberosmapping", FieldRoleMapperExpression.ofKeyValues("changed", "changed"), Collections - .singletonList("superuser"), null, true); + mutated = new ExpressionRoleMapping("kerberosmapping", FieldRoleMapperExpression.ofKeyValues("changed", "changed"), + singletonList("superuser"), Collections.emptyList(), null, true); break; case 2: - mutated = new ExpressionRoleMapping("kerberosmapping", FieldRoleMapperExpression.ofKeyValues("realm.name", "kerb1"), Collections - .singletonList("changed"), null, true); + mutated = new ExpressionRoleMapping("kerberosmapping", FieldRoleMapperExpression.ofKeyValues("realm.name", "kerb1"), + singletonList("changed"), Collections.emptyList(), null, true); break; case 3: Map metadata = new HashMap<>(); metadata.put("a", "b"); - mutated = new ExpressionRoleMapping("kerberosmapping", FieldRoleMapperExpression.ofKeyValues("realm.name", "kerb1"), Collections - .singletonList("superuser"), metadata, true); + mutated = new ExpressionRoleMapping("kerberosmapping", FieldRoleMapperExpression.ofKeyValues("realm.name", "kerb1"), + singletonList("superuser"), Collections.emptyList(), metadata, true); break; case 4: - mutated = new ExpressionRoleMapping("kerberosmapping", FieldRoleMapperExpression.ofKeyValues("realm.name", "kerb1"), Collections - .singletonList("superuser"), null, false); + mutated = new ExpressionRoleMapping("kerberosmapping", FieldRoleMapperExpression.ofKeyValues("realm.name", "kerb1"), + Collections.emptyList(), + singletonList(new TemplateRoleName(Collections.singletonMap("source", "superuser"), TemplateRoleName.Format.STRING)), + null, true); + break; + case 5: + mutated = new ExpressionRoleMapping("kerberosmapping", FieldRoleMapperExpression.ofKeyValues("realm.name", "kerb1"), + singletonList("superuser"), Collections.emptyList(), null, false); break; } return mutated; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetRoleMappingsResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetRoleMappingsResponseTests.java index b612c9ead28..20883b859f9 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetRoleMappingsResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetRoleMappingsResponseTests.java @@ -74,9 +74,10 @@ public class GetRoleMappingsResponseTests extends ESTestCase { }, json)); final List expectedRoleMappingsList = new ArrayList<>(); expectedRoleMappingsList.add(new ExpressionRoleMapping("kerberosmapping", FieldRoleMapperExpression.ofKeyValues("realm.name", - "kerb1"), Collections.singletonList("superuser"), null, true)); + "kerb1"), Collections.singletonList("superuser"), Collections.emptyList(), null, true)); expectedRoleMappingsList.add(new ExpressionRoleMapping("ldapmapping", FieldRoleMapperExpression.ofGroups( - "cn=ipausers,cn=groups,cn=accounts,dc=ipademo,dc=local"), Collections.singletonList("monitoring"), null, false)); + "cn=ipausers,cn=groups,cn=accounts,dc=ipademo,dc=local"), Collections.singletonList("monitoring"), Collections.emptyList(), + null, false)); final GetRoleMappingsResponse expectedResponse = new GetRoleMappingsResponse(expectedRoleMappingsList); assertThat(response, equalTo(expectedResponse)); } @@ -84,7 +85,7 @@ public class GetRoleMappingsResponseTests extends ESTestCase { public void testEqualsHashCode() { final List roleMappingsList = new ArrayList<>(); roleMappingsList.add(new ExpressionRoleMapping("kerberosmapping", FieldRoleMapperExpression.ofKeyValues("realm.name", - "kerb1"), Collections.singletonList("superuser"), null, true)); + "kerb1"), Collections.singletonList("superuser"), Collections.emptyList(), null, true)); final GetRoleMappingsResponse response = new GetRoleMappingsResponse(roleMappingsList); assertNotNull(response); EqualsHashCodeTestUtils.checkEqualsAndHashCode(response, (original) -> { @@ -101,15 +102,16 @@ public class GetRoleMappingsResponseTests extends ESTestCase { case 0: final List roleMappingsList1 = new ArrayList<>(); roleMappingsList1.add(new ExpressionRoleMapping("ldapmapping", FieldRoleMapperExpression.ofGroups( - "cn=ipausers,cn=groups,cn=accounts,dc=ipademo,dc=local"), Collections.singletonList("monitoring"), null, false)); + "cn=ipausers,cn=groups,cn=accounts,dc=ipademo,dc=local"), Collections.singletonList("monitoring"), Collections.emptyList(), + null, false)); mutated = new GetRoleMappingsResponse(roleMappingsList1); break; case 1: final List roleMappingsList2 = new ArrayList<>(); - ExpressionRoleMapping orginialRoleMapping = original.getMappings().get(0); - roleMappingsList2.add(new ExpressionRoleMapping(orginialRoleMapping.getName(), FieldRoleMapperExpression.ofGroups( - "cn=ipausers,cn=groups,cn=accounts,dc=ipademo,dc=local"), - orginialRoleMapping.getRoles(), orginialRoleMapping.getMetadata(), !orginialRoleMapping.isEnabled())); + ExpressionRoleMapping originalRoleMapping = original.getMappings().get(0); + roleMappingsList2.add(new ExpressionRoleMapping(originalRoleMapping.getName(), + FieldRoleMapperExpression.ofGroups("cn=ipausers,cn=groups,cn=accounts,dc=ipademo,dc=local"), originalRoleMapping.getRoles(), + Collections.emptyList(), originalRoleMapping.getMetadata(), !originalRoleMapping.isEnabled())); mutated = new GetRoleMappingsResponse(roleMappingsList2); break; } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/PutRoleMappingRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/PutRoleMappingRequestTests.java index f0a3f7572ef..bf5ba34bffc 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/PutRoleMappingRequestTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/PutRoleMappingRequestTests.java @@ -29,12 +29,12 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.EqualsHashCodeTestUtils; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import static org.hamcrest.Matchers.equalTo; @@ -49,7 +49,8 @@ public class PutRoleMappingRequestTests extends ESTestCase { metadata.put("k1", "v1"); final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); - PutRoleMappingRequest putRoleMappingRequest = new PutRoleMappingRequest(name, enabled, roles, rules, metadata, refreshPolicy); + PutRoleMappingRequest putRoleMappingRequest = new PutRoleMappingRequest(name, enabled, roles, Collections.emptyList(), rules, + metadata, refreshPolicy); assertNotNull(putRoleMappingRequest); assertThat(putRoleMappingRequest.getName(), equalTo(name)); assertThat(putRoleMappingRequest.isEnabled(), equalTo(enabled)); @@ -68,23 +69,39 @@ public class PutRoleMappingRequestTests extends ESTestCase { metadata.put("k1", "v1"); final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); - final IllegalArgumentException ile = expectThrows(IllegalArgumentException.class, () -> new PutRoleMappingRequest(name, enabled, - roles, rules, metadata, refreshPolicy)); + final IllegalArgumentException ile = expectThrows(IllegalArgumentException.class, + () -> new PutRoleMappingRequest(name, enabled, roles, Collections.emptyList(), rules, metadata, refreshPolicy)); assertThat(ile.getMessage(), equalTo("role-mapping name is missing")); } - public void testPutRoleMappingRequestThrowsExceptionForNullOrEmptyRoles() { + public void testPutRoleMappingRequestThrowsExceptionForNullRoles() { final String name = randomAlphaOfLength(5); final boolean enabled = randomBoolean(); - final List roles = randomBoolean() ? null : Collections.emptyList(); + final List roles = null ; + final List roleTemplates = Collections.emptyList(); final RoleMapperExpression rules = FieldRoleMapperExpression.ofUsername("user"); final Map metadata = new HashMap<>(); metadata.put("k1", "v1"); final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); - final IllegalArgumentException ile = expectThrows(IllegalArgumentException.class, () -> new PutRoleMappingRequest(name, enabled, - roles, rules, metadata, refreshPolicy)); - assertThat(ile.getMessage(), equalTo("role-mapping roles are missing")); + final RuntimeException ex = expectThrows(RuntimeException.class, + () -> new PutRoleMappingRequest(name, enabled, roles, roleTemplates, rules, metadata, refreshPolicy)); + assertThat(ex.getMessage(), equalTo("role-mapping roles cannot be null")); + } + + public void testPutRoleMappingRequestThrowsExceptionForEmptyRoles() { + final String name = randomAlphaOfLength(5); + final boolean enabled = randomBoolean(); + final List roles = Collections.emptyList(); + final List roleTemplates = Collections.emptyList(); + final RoleMapperExpression rules = FieldRoleMapperExpression.ofUsername("user"); + final Map metadata = new HashMap<>(); + metadata.put("k1", "v1"); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + + final RuntimeException ex = expectThrows(RuntimeException.class, + () -> new PutRoleMappingRequest(name, enabled, roles, roleTemplates, rules, metadata, refreshPolicy)); + assertThat(ex.getMessage(), equalTo("in a role-mapping, one of roles or role_templates is required")); } public void testPutRoleMappingRequestThrowsExceptionForNullRules() { @@ -96,7 +113,8 @@ public class PutRoleMappingRequestTests extends ESTestCase { metadata.put("k1", "v1"); final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); - expectThrows(NullPointerException.class, () -> new PutRoleMappingRequest(name, enabled, roles, rules, metadata, refreshPolicy)); + expectThrows(NullPointerException.class, () -> new PutRoleMappingRequest(name, enabled, roles, Collections.emptyList(), rules, + metadata, refreshPolicy)); } public void testPutRoleMappingRequestToXContent() throws IOException { @@ -108,7 +126,8 @@ public class PutRoleMappingRequestTests extends ESTestCase { metadata.put("k1", "v1"); final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); - final PutRoleMappingRequest putRoleMappingRequest = new PutRoleMappingRequest(name, enabled, roles, rules, metadata, refreshPolicy); + final PutRoleMappingRequest putRoleMappingRequest = new PutRoleMappingRequest(name, enabled, roles, Collections.emptyList(), rules, + metadata, refreshPolicy); final XContentBuilder builder = XContentFactory.jsonBuilder(); putRoleMappingRequest.toXContent(builder, ToXContent.EMPTY_PARAMS); @@ -117,6 +136,42 @@ public class PutRoleMappingRequestTests extends ESTestCase { "{"+ "\"enabled\":" + enabled + "," + "\"roles\":[\"superuser\"]," + + "\"role_templates\":[]," + + "\"rules\":{" + + "\"field\":{\"username\":[\"user\"]}" + + "}," + + "\"metadata\":{\"k1\":\"v1\"}" + + "}"; + + assertThat(output, equalTo(expected)); + } + + public void testPutRoleMappingRequestWithTemplateToXContent() throws IOException { + final String name = randomAlphaOfLength(5); + final boolean enabled = randomBoolean(); + final List templates = Arrays.asList( + new TemplateRoleName(Collections.singletonMap("source" , "_realm_{{realm.name}}"), TemplateRoleName.Format.STRING), + new TemplateRoleName(Collections.singletonMap("source" , "some_role"), TemplateRoleName.Format.STRING) + ); + final RoleMapperExpression rules = FieldRoleMapperExpression.ofUsername("user"); + final Map metadata = new HashMap<>(); + metadata.put("k1", "v1"); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + + final PutRoleMappingRequest putRoleMappingRequest = new PutRoleMappingRequest(name, enabled, Collections.emptyList(), templates, + rules, metadata, refreshPolicy); + + final XContentBuilder builder = XContentFactory.jsonBuilder(); + putRoleMappingRequest.toXContent(builder, ToXContent.EMPTY_PARAMS); + final String output = Strings.toString(builder); + final String expected = + "{"+ + "\"enabled\":" + enabled + "," + + "\"roles\":[]," + + "\"role_templates\":[" + + "{\"template\":\"{\\\"source\\\":\\\"_realm_{{realm.name}}\\\"}\",\"format\":\"string\"}," + + "{\"template\":\"{\\\"source\\\":\\\"some_role\\\"}\",\"format\":\"string\"}" + + "]," + "\"rules\":{" + "\"field\":{\"username\":[\"user\"]}" + "}," + @@ -129,48 +184,59 @@ public class PutRoleMappingRequestTests extends ESTestCase { public void testEqualsHashCode() { final String name = randomAlphaOfLength(5); final boolean enabled = randomBoolean(); - final List roles = Collections.singletonList("superuser"); + final List roles; + final List templates; + if (randomBoolean()) { + roles = Arrays.asList(randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(6, 12))); + templates = Collections.emptyList(); + } else { + roles = Collections.emptyList(); + templates = Arrays.asList( + randomArray(1, 3, TemplateRoleName[]::new, + () -> new TemplateRoleName(randomAlphaOfLengthBetween(12, 60), randomFrom(TemplateRoleName.Format.values())) + )); + } final RoleMapperExpression rules = FieldRoleMapperExpression.ofUsername("user"); final Map metadata = new HashMap<>(); metadata.put("k1", "v1"); final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); - PutRoleMappingRequest putRoleMappingRequest = new PutRoleMappingRequest(name, enabled, roles, rules, metadata, refreshPolicy); + PutRoleMappingRequest putRoleMappingRequest = new PutRoleMappingRequest(name, enabled, roles, templates, rules, metadata, + refreshPolicy); assertNotNull(putRoleMappingRequest); EqualsHashCodeTestUtils.checkEqualsAndHashCode(putRoleMappingRequest, (original) -> { - return new PutRoleMappingRequest(original.getName(), original.isEnabled(), original.getRoles(), original.getRules(), original - .getMetadata(), original.getRefreshPolicy()); - }); - EqualsHashCodeTestUtils.checkEqualsAndHashCode(putRoleMappingRequest, (original) -> { - return new PutRoleMappingRequest(original.getName(), original.isEnabled(), original.getRoles(), original.getRules(), original - .getMetadata(), original.getRefreshPolicy()); + return new PutRoleMappingRequest(original.getName(), original.isEnabled(), original.getRoles(), original.getRoleTemplates(), + original.getRules(), original.getMetadata(), original.getRefreshPolicy()); }, PutRoleMappingRequestTests::mutateTestItem); } private static PutRoleMappingRequest mutateTestItem(PutRoleMappingRequest original) { - switch (randomIntBetween(0, 4)) { + switch (randomIntBetween(0, 5)) { case 0: - return new PutRoleMappingRequest(randomAlphaOfLength(5), original.isEnabled(), original.getRoles(), original.getRules(), - original.getMetadata(), original.getRefreshPolicy()); + return new PutRoleMappingRequest(randomAlphaOfLength(5), original.isEnabled(), original.getRoles(), + original.getRoleTemplates(), original.getRules(), original.getMetadata(), original.getRefreshPolicy()); case 1: - return new PutRoleMappingRequest(original.getName(), !original.isEnabled(), original.getRoles(), original.getRules(), - original.getMetadata(), original.getRefreshPolicy()); + return new PutRoleMappingRequest(original.getName(), !original.isEnabled(), original.getRoles(), original.getRoleTemplates(), + original.getRules(), original.getMetadata(), original.getRefreshPolicy()); case 2: - return new PutRoleMappingRequest(original.getName(), original.isEnabled(), original.getRoles(), + return new PutRoleMappingRequest(original.getName(), original.isEnabled(), original.getRoles(), original.getRoleTemplates(), FieldRoleMapperExpression.ofGroups("group"), original.getMetadata(), original.getRefreshPolicy()); case 3: - return new PutRoleMappingRequest(original.getName(), original.isEnabled(), original.getRoles(), original.getRules(), - Collections.emptyMap(), original.getRefreshPolicy()); + return new PutRoleMappingRequest(original.getName(), original.isEnabled(), original.getRoles(), original.getRoleTemplates(), + original.getRules(), Collections.emptyMap(), original.getRefreshPolicy()); case 4: - List values = Arrays.stream(RefreshPolicy.values()) - .filter(rp -> rp != original.getRefreshPolicy()) - .collect(Collectors.toList()); - return new PutRoleMappingRequest(original.getName(), original.isEnabled(), original.getRoles(), original.getRules(), original - .getMetadata(), randomFrom(values)); + return new PutRoleMappingRequest(original.getName(), original.isEnabled(), original.getRoles(), original.getRoleTemplates(), + original.getRules(), original.getMetadata(), + randomValueOtherThan(original.getRefreshPolicy(), () -> randomFrom(RefreshPolicy.values()))); + case 5: + List roles = new ArrayList<>(original.getRoles()); + roles.add(randomAlphaOfLengthBetween(3, 5)); + return new PutRoleMappingRequest(original.getName(), original.isEnabled(), roles, Collections.emptyList(), + original.getRules(), original.getMetadata(), original.getRefreshPolicy()); + default: - return new PutRoleMappingRequest(randomAlphaOfLength(5), original.isEnabled(), original.getRoles(), original.getRules(), - original.getMetadata(), original.getRefreshPolicy()); + throw new IllegalStateException("Bad random value"); } } diff --git a/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc b/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc index de2ad5af308..bfd6a14d3ed 100644 --- a/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc +++ b/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc @@ -50,15 +50,46 @@ mapping is performed. user. Within the `metadata` object, keys beginning with `_` are reserved for system usage. -`roles` (required):: -(list) A list of roles that are granted to the users that match the role mapping -rules. +`roles`:: +(list of strings) A list of role names that are granted to the users that match +the role mapping rules. +_Exactly one of `roles` or `role_templates` must be specified_. + +`role_templates`:: +(list of objects) A list of mustache templates that will be evaluated to +determine the roles names that should granted to the users that match the role +mapping rules. +The format of these objects is defined below. +_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 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 <> 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. ==== Authorization @@ -117,12 +148,26 @@ POST /_security/role_mapping/mapping2 -------------------------------------------------- // CONSOLE +The following example matches users who authenticated against a specific realm: +[source, js] +------------------------------------------------------------ +POST /_security/role_mapping/mapping3 +{ + "roles": [ "ldap-user" ], + "enabled": true, + "rules": { + "field" : { "realm.name" : "ldap1" } + } +} +------------------------------------------------------------ +// CONSOLE + The following example matches any user where either the username is `esadmin` or the user is in the `cn=admin,dc=example,dc=com` group: [source, js] ------------------------------------------------------------ -POST /_security/role_mapping/mapping3 +POST /_security/role_mapping/mapping4 { "roles": [ "superuser" ], "enabled": true, @@ -144,25 +189,52 @@ POST /_security/role_mapping/mapping3 ------------------------------------------------------------ // CONSOLE -The following example matches users who authenticated against a specific realm: +The example above is useful when the group names in your identity management +system (such as Active Directory, or a SAML Identity Provider) do not have a +1-to-1 correspondence with the names of roles in {es}. The role mapping is the +means by which you link a _group name_ with a _role name_. + +However, in rare cases the names of your groups may be an exact match for the +names of your {es} roles. This can be the case when your SAML Identity Provider +includes its own "group mapping" feature and can be configured to release {es} +role names in the user's SAML attributes. + +In these cases it is possible to use a template that treats the group names as +role names. + +*Note*: This should only be done if you intend to define roles for all of the +provided groups. Mapping a user to a large number of unnecessary or undefined +roles is inefficient and can have a negative effect on system performance. +If you only need to map a subset of the groups, then you should do this +using explicit mappings. + [source, js] ------------------------------------------------------------ -POST /_security/role_mapping/mapping4 +POST /_security/role_mapping/mapping5 { - "roles": [ "ldap-user" ], - "enabled": true, + "role_templates": [ + { + "template": { "source": "{{#tojson}}groups{{/tojson}}" }, <1> + "format" : "json" <2> + } + ], "rules": { - "field" : { "realm.name" : "ldap1" } - } + "field" : { "realm.name" : "saml1" } + }, + "enabled": true } ------------------------------------------------------------ // CONSOLE +<1> The `tojson` mustache function is used to convert the list of + group names into a valid JSON array. +<2> Because the template produces a JSON array, the format must be + set to `json`. The following example matches users within a specific LDAP sub-tree: [source, js] ------------------------------------------------------------ -POST /_security/role_mapping/mapping5 +POST /_security/role_mapping/mapping6 { "roles": [ "example-user" ], "enabled": true, @@ -178,7 +250,7 @@ specific realm: [source, js] ------------------------------------------------------------ -POST /_security/role_mapping/mapping6 +POST /_security/role_mapping/mapping7 { "roles": [ "ldap-example-user" ], "enabled": true, @@ -203,7 +275,7 @@ following mapping matches any user where *all* of these conditions are met: [source, js] ------------------------------------------------------------ -POST /_security/role_mapping/mapping7 +POST /_security/role_mapping/mapping8 { "roles": [ "superuser" ], "enabled": true, @@ -240,3 +312,32 @@ POST /_security/role_mapping/mapping7 } ------------------------------------------------------------ // CONSOLE + +A templated role can be used to automatically map every user to their own +custom role. The role itself can be defined through the +<> or using a +{stack-ov}/custom-roles-authorization.html#implementing-custom-roles-provider[custom roles provider]. + +In this example every user who authenticates using the "cloud-saml" realm +will be automatically mapped to two roles - the `"saml_user"` role and a +role that is their username prefixed with `_user_`. +As an example, the user `nwong` would be assigned the `saml_user` and +`_user_nwong` roles. + +[source, js] +------------------------------------------------------------ +POST /_security/role_mapping/mapping9 +{ + "rules": { "field": { "realm.name": "cloud-saml" } }, + "role_templates": [ + { "template": { "source" : "saml_user" } }, <1> + { "template": { "source" : "_user_{{username}}" } } + ], + "enabled": true +} +------------------------------------------------------------ +// CONSOLE +<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. + diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java index a745215fa55..dc8403b7bd5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java @@ -57,9 +57,9 @@ import org.elasticsearch.xpack.core.indexlifecycle.IndexLifecycleFeatureSetUsage import org.elasticsearch.xpack.core.indexlifecycle.IndexLifecycleMetadata; import org.elasticsearch.xpack.core.indexlifecycle.LifecycleAction; import org.elasticsearch.xpack.core.indexlifecycle.LifecycleType; -import org.elasticsearch.xpack.core.indexlifecycle.SetPriorityAction; import org.elasticsearch.xpack.core.indexlifecycle.ReadOnlyAction; import org.elasticsearch.xpack.core.indexlifecycle.RolloverAction; +import org.elasticsearch.xpack.core.indexlifecycle.SetPriorityAction; import org.elasticsearch.xpack.core.indexlifecycle.ShrinkAction; import org.elasticsearch.xpack.core.indexlifecycle.TimeseriesLifecycleType; import org.elasticsearch.xpack.core.indexlifecycle.UnfollowAction; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/rolemapping/PutRoleMappingRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/rolemapping/PutRoleMappingRequest.java index 087e29ec8b5..ae036b63162 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/rolemapping/PutRoleMappingRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/rolemapping/PutRoleMappingRequest.java @@ -5,12 +5,14 @@ */ package org.elasticsearch.xpack.core.security.action.rolemapping; +import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; 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.ExpressionParser; import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.RoleMapperExpression; import org.elasticsearch.xpack.core.security.support.MetadataUtils; @@ -35,6 +37,7 @@ public class PutRoleMappingRequest extends ActionRequest private String name = null; private boolean enabled = true; private List roles = Collections.emptyList(); + private List roleTemplates = Collections.emptyList(); private RoleMapperExpression rules = null; private Map metadata = Collections.emptyMap(); private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE; @@ -46,20 +49,20 @@ public class PutRoleMappingRequest extends ActionRequest public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; if (name == null) { - validationException = addValidationError("role-mapping name is missing", - validationException); + validationException = addValidationError("role-mapping name is missing", validationException); } - if (roles.isEmpty()) { - validationException = addValidationError("role-mapping roles are missing", - validationException); + if (roles.isEmpty() && roleTemplates.isEmpty()) { + validationException = addValidationError("role-mapping roles or role-templates are missing", validationException); + } + if (roles.size() > 0 && roleTemplates.size() > 0) { + validationException = addValidationError("role-mapping cannot have both roles and role-templates", validationException); } if (rules == null) { - validationException = addValidationError("role-mapping rules are missing", - validationException); + validationException = addValidationError("role-mapping rules are missing", validationException); } if (MetadataUtils.containsReservedMetadata(metadata)) { - validationException = addValidationError("metadata keys may not start with [" + - MetadataUtils.RESERVED_PREFIX + "]", validationException); + validationException = addValidationError("metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX + "]", + validationException); } return validationException; } @@ -84,10 +87,18 @@ public class PutRoleMappingRequest extends ActionRequest return Collections.unmodifiableList(roles); } + public List getRoleTemplates() { + return Collections.unmodifiableList(roleTemplates); + } + public void setRoles(List roles) { this.roles = new ArrayList<>(roles); } + public void setRoleTemplates(List templates) { + this.roleTemplates = new ArrayList<>(templates); + } + public RoleMapperExpression getRules() { return rules; } @@ -126,6 +137,9 @@ public class PutRoleMappingRequest extends ActionRequest this.name = in.readString(); this.enabled = in.readBoolean(); this.roles = in.readStringList(); + if (in.getVersion().onOrAfter(Version.V_7_1_0)) { + this.roleTemplates = in.readList(TemplateRoleName::new); + } this.rules = ExpressionParser.readExpression(in); this.metadata = in.readMap(); this.refreshPolicy = RefreshPolicy.readFrom(in); @@ -137,6 +151,9 @@ public class PutRoleMappingRequest extends ActionRequest out.writeString(name); out.writeBoolean(enabled); out.writeStringCollection(roles); + if (out.getVersion().onOrAfter(Version.V_7_1_0)) { + out.writeList(roleTemplates); + } ExpressionParser.writeExpression(rules, out); out.writeMap(metadata); refreshPolicy.writeTo(out); @@ -147,6 +164,7 @@ public class PutRoleMappingRequest extends ActionRequest name, rules, roles, + roleTemplates, metadata, enabled ); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/rolemapping/PutRoleMappingRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/rolemapping/PutRoleMappingRequestBuilder.java index c74952e9dfd..14f722d1694 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/rolemapping/PutRoleMappingRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/rolemapping/PutRoleMappingRequestBuilder.java @@ -5,18 +5,19 @@ */ package org.elasticsearch.xpack.core.security.action.rolemapping; -import java.io.IOException; -import java.util.Arrays; -import java.util.Map; - import org.elasticsearch.action.ActionRequestBuilder; import org.elasticsearch.action.support.WriteRequestBuilder; import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.XContentType; 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.RoleMapperExpression; +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; + /** * Builder for requests to add/update a role-mapping to the native store * @@ -38,6 +39,7 @@ public class PutRoleMappingRequestBuilder extends ActionRequestBuilder TemplateRoleName.parse(parser), Fields.ROLE_TEMPLATES); + PARSER.declareField(Builder::rules, ExpressionParser::parseObject, Fields.RULES, ValueType.OBJECT); + PARSER.declareField(Builder::metadata, XContentParser::map, Fields.METADATA, ValueType.OBJECT); PARSER.declareBoolean(Builder::enabled, Fields.ENABLED); BiConsumer ignored = (b, v) -> { }; // skip the doc_type and type fields in case we're parsing directly from the index PARSER.declareString(ignored, new ParseField(NativeRoleMappingStoreField.DOC_TYPE_FIELD)); PARSER.declareString(ignored, new ParseField(UPGRADE_API_TYPE_FIELD)); - } + } private final String name; private final RoleMapperExpression expression; private final List roles; + private final List roleTemplates ; private final Map metadata; private final boolean enabled; - public ExpressionRoleMapping(String name, RoleMapperExpression expr, List roles, Map metadata, - boolean enabled) { + public ExpressionRoleMapping(String name, RoleMapperExpression expr, List roles, List templates, + Map metadata, boolean enabled) { this.name = name; this.expression = expr; - this.roles = roles; + this.roles = roles == null ? Collections.emptyList() : roles; + this.roleTemplates = templates == null ? Collections.emptyList() : templates; this.metadata = metadata; this.enabled = enabled; } @@ -79,6 +91,11 @@ public class ExpressionRoleMapping implements ToXContentObject, Writeable { this.name = in.readString(); this.enabled = in.readBoolean(); this.roles = in.readStringList(); + if (in.getVersion().onOrAfter(Version.V_7_1_0)) { + this.roleTemplates = in.readList(TemplateRoleName::new); + } else { + this.roleTemplates = Collections.emptyList(); + } this.expression = ExpressionParser.readExpression(in); this.metadata = in.readMap(); } @@ -88,6 +105,9 @@ public class ExpressionRoleMapping implements ToXContentObject, Writeable { out.writeString(name); out.writeBoolean(enabled); out.writeStringCollection(roles); + if (out.getVersion().onOrAfter(Version.V_7_1_0)) { + out.writeList(roleTemplates); + } ExpressionParser.writeExpression(expression, out); out.writeMap(metadata); } @@ -103,7 +123,7 @@ public class ExpressionRoleMapping implements ToXContentObject, Writeable { /** * The expression that determines whether the roles in this mapping should be applied to any given user. * If the expression - * {@link RoleMapperExpression#match(org.elasticsearch.xpack.security.authc.support.mapper.expressiondsl.ExpressionModel) matches} a + * {@link RoleMapperExpression#match(ExpressionModel) matches} a * org.elasticsearch.xpack.security.authc.support.UserRoleMapper.UserData user, then the user should be assigned this mapping's * {@link #getRoles() roles} */ @@ -119,6 +139,14 @@ public class ExpressionRoleMapping implements ToXContentObject, Writeable { return Collections.unmodifiableList(roles); } + /** + * The list of {@link RoleDescriptor roles} (specified by a {@link TemplateRoleName template} that evaluates to one or more names) + * that should be assigned to users that match the {@link #getExpression() expression} in this mapping. + */ + public List getRoleTemplates() { + return Collections.unmodifiableList(roleTemplates); + } + /** * Meta-data for this mapping. This exists for external systems of user to track information about this mapping such as where it was * sourced from, when it was loaded, etc. @@ -137,7 +165,30 @@ public class ExpressionRoleMapping implements ToXContentObject, Writeable { @Override public String toString() { - return getClass().getSimpleName() + "<" + name + " ; " + roles + " = " + Strings.toString(expression) + ">"; + return getClass().getSimpleName() + "<" + name + " ; " + roles + "/" + roleTemplates + " = " + Strings.toString(expression) + ">"; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ExpressionRoleMapping that = (ExpressionRoleMapping) o; + return this.enabled == that.enabled && + Objects.equals(this.name, that.name) && + Objects.equals(this.expression, that.expression) && + Objects.equals(this.roles, that.roles) && + Objects.equals(this.roleTemplates, that.roleTemplates) && + Objects.equals(this.metadata, that.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(name, expression, roles, roleTemplates, metadata, enabled); } /** @@ -157,7 +208,7 @@ public class ExpressionRoleMapping implements ToXContentObject, Writeable { */ public static ExpressionRoleMapping parse(String name, XContentParser parser) throws IOException { try { - final Builder builder = PARSER.parse(parser, null); + final Builder builder = PARSER.parse(parser, name); return builder.build(name); } catch (IllegalArgumentException | IllegalStateException e) { throw new ParsingException(parser.getTokenLocation(), e.getMessage(), e); @@ -166,38 +217,55 @@ public class ExpressionRoleMapping implements ToXContentObject, Writeable { /** * Converts this {@link ExpressionRoleMapping} into XContent that is compatible with - * the format handled by {@link #parse(String, XContentParser)}. + * the format handled by {@link #parse(String, BytesReference, XContentType)}. */ @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { return toXContent(builder, params, false); } - public XContentBuilder toXContent(XContentBuilder builder, Params params, boolean includeDocType) throws IOException { + public XContentBuilder toXContent(XContentBuilder builder, Params params, boolean indexFormat) throws IOException { builder.startObject(); builder.field(Fields.ENABLED.getPreferredName(), enabled); - builder.startArray(Fields.ROLES.getPreferredName()); - for (String r : roles) { - builder.value(r); + if (roles.isEmpty() == false) { + builder.startArray(Fields.ROLES.getPreferredName()); + for (String r : roles) { + builder.value(r); + } + builder.endArray(); + } + if (roleTemplates.isEmpty() == false) { + builder.startArray(Fields.ROLE_TEMPLATES.getPreferredName()); + for (TemplateRoleName r : roleTemplates) { + builder.value(r); + } + builder.endArray(); } - builder.endArray(); builder.field(Fields.RULES.getPreferredName()); expression.toXContent(builder, params); builder.field(Fields.METADATA.getPreferredName(), metadata); - if (includeDocType) { + if (indexFormat) { builder.field(NativeRoleMappingStoreField.DOC_TYPE_FIELD, NativeRoleMappingStoreField.DOC_TYPE_ROLE_MAPPING); } return builder.endObject(); } + public Set getRoleNames(ScriptService scriptService, ExpressionModel model) { + return Stream.concat(this.roles.stream(), + this.roleTemplates.stream() + .flatMap(r -> r.getRoleNames(scriptService, model).stream()) + ).collect(Collectors.toSet()); + } + /** * Used to facilitate the use of {@link ObjectParser} (via {@link #PARSER}). */ private static class Builder { private RoleMapperExpression rules; private List roles; + private List roleTemplates; private Map metadata = Collections.emptyMap(); private Boolean enabled; @@ -207,7 +275,12 @@ public class ExpressionRoleMapping implements ToXContentObject, Writeable { } Builder roles(List roles) { - this.roles = roles; + this.roles = new ArrayList<>(roles); + return this; + } + + Builder roleTemplates(List templates) { + this.roleTemplates = new ArrayList<>(templates); return this; } @@ -222,7 +295,7 @@ public class ExpressionRoleMapping implements ToXContentObject, Writeable { } private ExpressionRoleMapping build(String name) { - if (roles == null) { + if (roles == null && roleTemplates == null) { throw missingField(name, Fields.ROLES); } if (rules == null) { @@ -231,17 +304,17 @@ public class ExpressionRoleMapping implements ToXContentObject, Writeable { if (enabled == null) { throw missingField(name, Fields.ENABLED); } - return new ExpressionRoleMapping(name, rules, roles, metadata, enabled); + return new ExpressionRoleMapping(name, rules, roles, roleTemplates, metadata, enabled); } private IllegalStateException missingField(String id, ParseField field) { return new IllegalStateException("failed to parse role-mapping [" + id + "]. missing field [" + field + "]"); } - } public interface Fields { ParseField ROLES = new ParseField("roles"); + ParseField ROLE_TEMPLATES = new ParseField("role_templates"); ParseField ENABLED = new ParseField("enabled"); ParseField RULES = new ParseField("rules"); ParseField METADATA = new ParseField("metadata"); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/TemplateRoleName.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/TemplateRoleName.java new file mode 100644 index 00000000000..d77882d6454 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/TemplateRoleName.java @@ -0,0 +1,211 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.authc.support.mapper; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParseException; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.ExpressionModel; +import org.elasticsearch.xpack.core.security.support.MustacheTemplateEvaluator; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * Representation of a Mustache template for expressing one or more roles names in a {@link ExpressionRoleMapping}. + */ +public class TemplateRoleName implements ToXContent, Writeable { + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "role-mapping-template", false, arr -> new TemplateRoleName((BytesReference) arr[0], (Format) arr[1])); + + static { + PARSER.declareField(constructorArg(), TemplateRoleName::extractTemplate, Fields.TEMPLATE, ObjectParser.ValueType.OBJECT_OR_STRING); + PARSER.declareField(optionalConstructorArg(), Format::fromXContent, Fields.FORMAT, ObjectParser.ValueType.STRING); + } + + private final BytesReference template; + private final Format format; + + public TemplateRoleName(BytesReference template, Format format) { + this.template = template; + this.format = format == null ? Format.STRING : format; + } + + public TemplateRoleName(StreamInput in) throws IOException { + this.template = in.readBytesReference(); + this.format = in.readEnum(Format.class); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeBytesReference(template); + out.writeEnum(format); + } + + public BytesReference getTemplate() { + return template; + } + + public Format getFormat() { + return format; + } + + public List getRoleNames(ScriptService scriptService, ExpressionModel model) { + try { + final String evaluation = parseTemplate(scriptService, model.asMap()); + switch (format) { + case STRING: + return Collections.singletonList(evaluation); + case JSON: + return convertJsonToList(evaluation); + default: + throw new IllegalStateException("Unsupported format [" + format + "]"); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private List convertJsonToList(String evaluation) throws IOException { + final XContentParser parser = XContentFactory.xContent(XContentType.JSON).createParser(NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, evaluation); + XContentParser.Token token = parser.currentToken(); + if (token == null) { + token = parser.nextToken(); + } + if (token == XContentParser.Token.VALUE_STRING) { + return Collections.singletonList(parser.text()); + } else if (token == XContentParser.Token.START_ARRAY) { + return parser.list().stream() + .filter(Objects::nonNull) + .map(o -> { + if (o instanceof String) { + return (String) o; + } else { + throw new XContentParseException( + "Roles array may only contain strings but found [" + o.getClass().getName() + "] [" + o + "]"); + } + }).collect(Collectors.toList()); + } else { + throw new XContentParseException( + "Roles template must generate a string or an array of strings, but found [" + token + "]"); + } + } + + private String parseTemplate(ScriptService scriptService, Map parameters) throws IOException { + final XContentParser parser = XContentHelper.createParser( + NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, template, XContentType.JSON); + return MustacheTemplateEvaluator.evaluate(scriptService, parser, parameters); + } + + private static BytesReference extractTemplate(XContentParser parser, Void ignore) throws IOException { + if (parser.currentToken() == XContentParser.Token.VALUE_STRING) { + return new BytesArray(parser.text()); + } else { + XContentBuilder builder = JsonXContent.contentBuilder(); + builder.generator().copyCurrentStructure(parser); + return BytesReference.bytes(builder); + } + } + + static TemplateRoleName parse(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public String toString() { + return "template-" + format + "{" + template.utf8ToString() + "}"; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject() + .field(Fields.TEMPLATE.getPreferredName(), template.utf8ToString()) + .field(Fields.FORMAT.getPreferredName(), format.formatName()) + .endObject(); + } + + @Override + public boolean isFragment() { + return false; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final TemplateRoleName that = (TemplateRoleName) o; + return Objects.equals(this.template, that.template) && + this.format == that.format; + } + + @Override + public int hashCode() { + return Objects.hash(template, format); + } + + private interface Fields { + ParseField TEMPLATE = new ParseField("template"); + ParseField FORMAT = new ParseField("format"); + } + + public enum Format { + JSON, STRING; + + private static Format fromXContent(XContentParser parser) throws IOException { + final XContentParser.Token token = parser.currentToken(); + if (token != XContentParser.Token.VALUE_STRING) { + throw new XContentParseException(parser.getTokenLocation(), + "Expected [" + XContentParser.Token.VALUE_STRING + "] but found [" + token + "]"); + } + final String text = parser.text(); + try { + return Format.valueOf(text.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + String valueNames = Stream.of(values()).map(Format::formatName).collect(Collectors.joining(",")); + throw new XContentParseException(parser.getTokenLocation(), + "Invalid format [" + text + "] expected one of [" + valueNames + "]"); + } + + } + + public String formatName() { + return name().toLowerCase(Locale.ROOT); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/expressiondsl/ExpressionModel.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/expressiondsl/ExpressionModel.java index 8d43f864878..d12cc67dcca 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/expressiondsl/ExpressionModel.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/expressiondsl/ExpressionModel.java @@ -6,9 +6,9 @@ package org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl; import org.elasticsearch.common.Numbers; -import org.elasticsearch.common.collect.Tuple; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -22,10 +22,13 @@ import java.util.function.Predicate; public class ExpressionModel { public static final Predicate NULL_PREDICATE = field -> field.getValue() == null; - private Map>> fields; + + private final Map fieldValues; + private final Map> fieldPredicates; public ExpressionModel() { - this.fields = new HashMap<>(); + this.fieldValues = new HashMap<>(); + this.fieldPredicates = new HashMap<>(); } /** @@ -41,7 +44,8 @@ public class ExpressionModel { * Defines a field using a supplied predicate. */ public ExpressionModel defineField(String name, Object value, Predicate predicate) { - this.fields.put(name, new Tuple<>(value, predicate)); + this.fieldValues.put(name, value); + this.fieldPredicates.put(name, predicate); return this; } @@ -49,13 +53,7 @@ public class ExpressionModel { * Returns {@code true} if the named field, matches any of the provided values. */ public boolean test(String field, List values) { - final Tuple> tuple = this.fields.get(field); - final Predicate predicate; - if (tuple == null) { - predicate = NULL_PREDICATE; - } else { - predicate = tuple.v2(); - } + final Predicate predicate = this.fieldPredicates.getOrDefault(field, NULL_PREDICATE); return values.stream().anyMatch(predicate); } @@ -103,4 +101,12 @@ public class ExpressionModel { return Numbers.toLongExact(left) == Numbers.toLongExact(right); } + public Map asMap() { + return Collections.unmodifiableMap(fieldValues); + } + + @Override + public String toString() { + return fieldValues.toString(); + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/expressiondsl/FieldExpression.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/expressiondsl/FieldExpression.java index 0e681b110ef..bea4bbb1cc8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/expressiondsl/FieldExpression.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/expressiondsl/FieldExpression.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.core.security.support.Automatons; import java.io.IOException; import java.util.Collections; import java.util.List; +import java.util.Objects; /** * An expression that evaluates to true if a field (map element) matches @@ -151,6 +152,22 @@ public final class FieldExpression implements RoleMapperExpression { return builder.value(value); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final FieldValue that = (FieldValue) o; + return Objects.equals(this.value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/SecurityQueryTemplateEvaluator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/SecurityQueryTemplateEvaluator.java index 951c4acf10d..73a1d7fcde5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/SecurityQueryTemplateEvaluator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/SecurityQueryTemplateEvaluator.java @@ -11,10 +11,8 @@ import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptService; -import org.elasticsearch.script.ScriptType; -import org.elasticsearch.script.TemplateScript; +import org.elasticsearch.xpack.core.security.support.MustacheTemplateEvaluator; import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; @@ -66,27 +64,19 @@ public final class SecurityQueryTemplateEvaluator { if (token != XContentParser.Token.START_OBJECT) { throw new ElasticsearchParseException("Unexpected token [" + token + "]"); } - Script script = Script.parse(parser); - // Add the user details to the params - Map params = new HashMap<>(); - if (script.getParams() != null) { - params.putAll(script.getParams()); - } Map userModel = new HashMap<>(); userModel.put("username", user.principal()); userModel.put("full_name", user.fullName()); userModel.put("email", user.email()); userModel.put("roles", Arrays.asList(user.roles())); userModel.put("metadata", Collections.unmodifiableMap(user.metadata())); - params.put("_user", userModel); - // Always enforce mustache script lang: - script = new Script(script.getType(), script.getType() == ScriptType.STORED ? null : "mustache", script.getIdOrCode(), - script.getOptions(), params); - TemplateScript compiledTemplate = scriptService.compile(script, TemplateScript.CONTEXT).newInstance(script.getParams()); - return compiledTemplate.execute(); + Map extraParams = Collections.singletonMap("_user", userModel); + + return MustacheTemplateEvaluator.evaluate(scriptService, parser, extraParams); } else { return querySource; } } } + } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/MustacheTemplateEvaluator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/MustacheTemplateEvaluator.java new file mode 100644 index 00000000000..02f730333de --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/MustacheTemplateEvaluator.java @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.support; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.script.TemplateScript; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Utility class for evaluating Mustache templates at runtime. + */ +public final class MustacheTemplateEvaluator { + + private MustacheTemplateEvaluator() { + throw new UnsupportedOperationException("Cannot construct " + MustacheTemplateEvaluator.class); + } + + public static String evaluate(ScriptService scriptService, XContentParser parser, Map extraParams) throws IOException { + Script script = Script.parse(parser); + // Add the user details to the params + Map params = new HashMap<>(); + if (script.getParams() != null) { + params.putAll(script.getParams()); + } + extraParams.forEach(params::put); + // Always enforce mustache script lang: + script = new Script(script.getType(), script.getType() == ScriptType.STORED ? null : "mustache", script.getIdOrCode(), + script.getOptions(), params); + TemplateScript compiledTemplate = scriptService.compile(script, TemplateScript.CONTEXT).newInstance(script.getParams()); + return compiledTemplate.execute(); + } +} diff --git a/x-pack/plugin/core/src/main/resources/security-index-template.json b/x-pack/plugin/core/src/main/resources/security-index-template.json index 94bb2b03ee0..f4e3cd6db02 100644 --- a/x-pack/plugin/core/src/main/resources/security-index-template.json +++ b/x-pack/plugin/core/src/main/resources/security-index-template.json @@ -45,6 +45,16 @@ "roles" : { "type" : "keyword" }, + "role_templates" : { + "properties": { + "template" : { + "type": "text" + }, + "format" : { + "type": "keyword" + } + } + }, "password" : { "type" : "keyword", "index" : false, diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/support/mapper/TemplateRoleNameTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/support/mapper/TemplateRoleNameTests.java new file mode 100644 index 00000000000..cab10ca7283 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/support/mapper/TemplateRoleNameTests.java @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.authc.support.mapper; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.ByteBufferStreamInput; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.DeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.script.ScriptModule; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.mustache.MustacheScriptEngine; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; +import org.elasticsearch.xpack.core.security.authc.support.mapper.TemplateRoleName.Format; +import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.ExpressionModel; +import org.hamcrest.Matchers; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +public class TemplateRoleNameTests extends ESTestCase { + + public void testParseRoles() throws Exception { + final TemplateRoleName role1 = parse("{ \"template\": { \"source\": \"_user_{{username}}\" } }"); + assertThat(role1, Matchers.instanceOf(TemplateRoleName.class)); + assertThat(role1.getTemplate().utf8ToString(), equalTo("{\"source\":\"_user_{{username}}\"}")); + assertThat(role1.getFormat(), equalTo(Format.STRING)); + + final TemplateRoleName role2 = parse( + "{ \"template\": \"{\\\"source\\\":\\\"{{#tojson}}groups{{/tojson}}\\\"}\", \"format\":\"json\" }"); + assertThat(role2, Matchers.instanceOf(TemplateRoleName.class)); + assertThat(role2.getTemplate().utf8ToString(), + equalTo("{\"source\":\"{{#tojson}}groups{{/tojson}}\"}")); + assertThat(role2.getFormat(), equalTo(Format.JSON)); + } + + public void testToXContent() throws Exception { + final String json = "{" + + "\"template\":\"{\\\"source\\\":\\\"" + randomAlphaOfLengthBetween(8, 24) + "\\\"}\"," + + "\"format\":\"" + randomFrom(Format.values()).formatName() + "\"" + + "}"; + assertThat(Strings.toString(parse(json)), equalTo(json)); + } + + public void testSerializeTemplate() throws Exception { + trySerialize(new TemplateRoleName(new BytesArray(randomAlphaOfLengthBetween(12, 60)), randomFrom(Format.values()))); + } + + public void testEqualsAndHashCode() throws Exception { + tryEquals(new TemplateRoleName(new BytesArray(randomAlphaOfLengthBetween(12, 60)), randomFrom(Format.values()))); + } + + public void testEvaluateRoles() throws Exception { + final ScriptService scriptService = new ScriptService(Settings.EMPTY, + Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()), ScriptModule.CORE_CONTEXTS); + final ExpressionModel model = new ExpressionModel(); + model.defineField("username", "hulk"); + model.defineField("groups", Arrays.asList("avengers", "defenders", "panthenon")); + + final TemplateRoleName plainString = new TemplateRoleName(new BytesArray("{ \"source\":\"heroes\" }"), Format.STRING); + assertThat(plainString.getRoleNames(scriptService, model), contains("heroes")); + + final TemplateRoleName user = new TemplateRoleName(new BytesArray("{ \"source\":\"_user_{{username}}\" }"), Format.STRING); + assertThat(user.getRoleNames(scriptService, model), contains("_user_hulk")); + + final TemplateRoleName groups = new TemplateRoleName(new BytesArray("{ \"source\":\"{{#tojson}}groups{{/tojson}}\" }"), + Format.JSON); + assertThat(groups.getRoleNames(scriptService, model), contains("avengers", "defenders", "panthenon")); + } + + private TemplateRoleName parse(String json) throws IOException { + final XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json); + final TemplateRoleName role = TemplateRoleName.parse(parser); + assertThat(role, notNullValue()); + return role; + } + + public void trySerialize(TemplateRoleName original) throws Exception { + BytesStreamOutput output = new BytesStreamOutput(); + original.writeTo(output); + + final StreamInput rawInput = ByteBufferStreamInput.wrap(BytesReference.toBytes(output.bytes())); + final TemplateRoleName serialized = new TemplateRoleName(rawInput); + assertEquals(original, serialized); + } + + public void tryEquals(TemplateRoleName original) { + final EqualsHashCodeTestUtils.CopyFunction copy = + rmt -> new TemplateRoleName(rmt.getTemplate(), rmt.getFormat()); + final EqualsHashCodeTestUtils.MutateFunction mutate = rmt -> { + if (randomBoolean()) { + return new TemplateRoleName(rmt.getTemplate(), + randomValueOtherThan(rmt.getFormat(), () -> randomFrom(Format.values()))); + } else { + final String templateStr = rmt.getTemplate().utf8ToString(); + return new TemplateRoleName(new BytesArray(templateStr.substring(randomIntBetween(1, templateStr.length() / 2))), + rmt.getFormat()); + } + }; + EqualsHashCodeTestUtils.checkEqualsAndHashCode(original, copy, mutate); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 2b642d93e9c..7b7e72fdd6b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -370,7 +370,7 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw NamedXContentRegistry xContentRegistry, Environment environment, NodeEnvironment nodeEnvironment, NamedWriteableRegistry namedWriteableRegistry) { try { - return createComponents(client, threadPool, clusterService, resourceWatcherService); + return createComponents(client, threadPool, clusterService, resourceWatcherService, scriptService); } catch (final Exception e) { throw new IllegalStateException("security initialization failed", e); } @@ -378,7 +378,7 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw // pkg private for testing - tests want to pass in their set of extensions hence we are not using the extension service directly Collection createComponents(Client client, ThreadPool threadPool, ClusterService clusterService, - ResourceWatcherService resourceWatcherService) throws Exception { + ResourceWatcherService resourceWatcherService, ScriptService scriptService) throws Exception { if (enabled == false) { return Collections.emptyList(); } @@ -404,7 +404,8 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw // realms construction final NativeUsersStore nativeUsersStore = new NativeUsersStore(settings, client, securityIndex.get()); - final NativeRoleMappingStore nativeRoleMappingStore = new NativeRoleMappingStore(settings, client, securityIndex.get()); + final NativeRoleMappingStore nativeRoleMappingStore = new NativeRoleMappingStore(settings, client, securityIndex.get(), + scriptService); final AnonymousUser anonymousUser = new AnonymousUser(settings); final ReservedRealm reservedRealm = new ReservedRealm(env, settings, nativeUsersStore, anonymousUser, securityIndex.get(), threadPool); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStore.java index cbb352e67ab..e8d874bc9d4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStore.java @@ -26,6 +26,7 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.script.ScriptService; import org.elasticsearch.xpack.core.security.ScrollHelper; import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheAction; import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheResponse; @@ -51,7 +52,6 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.Stream; import static org.elasticsearch.action.DocWriteResponse.Result.CREATED; import static org.elasticsearch.action.DocWriteResponse.Result.DELETED; @@ -99,12 +99,14 @@ public class NativeRoleMappingStore implements UserRoleMapper { private final Settings settings; private final Client client; private final SecurityIndexManager securityIndex; + private final ScriptService scriptService; private final List realmsToRefresh = new CopyOnWriteArrayList<>(); - public NativeRoleMappingStore(Settings settings, Client client, SecurityIndexManager securityIndex) { + public NativeRoleMappingStore(Settings settings, Client client, SecurityIndexManager securityIndex, ScriptService scriptService) { this.settings = settings; this.client = client; this.securityIndex = securityIndex; + this.scriptService = scriptService; } private String getNameFromId(String id) { @@ -120,7 +122,7 @@ public class NativeRoleMappingStore implements UserRoleMapper { * Loads all mappings from the index. * package private for unit testing */ - void loadMappings(ActionListener> listener) { + protected void loadMappings(ActionListener> listener) { if (securityIndex.isIndexUpToDate() == false) { listener.onFailure(new IllegalStateException( "Security index is not on the current version - the native realm will not be operational until " + @@ -149,7 +151,7 @@ public class NativeRoleMappingStore implements UserRoleMapper { } } - private ExpressionRoleMapping buildMapping(String id, BytesReference source) { + protected ExpressionRoleMapping buildMapping(String id, BytesReference source) { try (InputStream stream = source.streamInput(); XContentParser parser = XContentType.JSON.xContent() .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) { @@ -349,17 +351,16 @@ public class NativeRoleMappingStore implements UserRoleMapper { getRoleMappings(null, ActionListener.wrap( mappings -> { final ExpressionModel model = user.asModel(); - Stream stream = mappings.stream() - .filter(ExpressionRoleMapping::isEnabled) - .filter(m -> m.getExpression().match(model)); - if (logger.isTraceEnabled()) { - stream = stream.map(m -> { - logger.trace("User [{}] matches role-mapping [{}] with roles [{}]", user.getUsername(), m.getName(), - m.getRoles()); - return m; - }); - } - final Set roles = stream.flatMap(m -> m.getRoles().stream()).collect(Collectors.toSet()); + final Set roles = mappings.stream() + .filter(ExpressionRoleMapping::isEnabled) + .filter(m -> m.getExpression().match(model)) + .flatMap(m -> { + final Set roleNames = m.getRoleNames(scriptService, model); + logger.trace("Applying role-mapping [{}] to user-model [{}] produced role-names [{}]", + m.getName(), model, roleNames); + return roleNames.stream(); + }) + .collect(Collectors.toSet()); logger.debug("Mapping user [{}] to roles [{}]", user, roles); listener.onResponse(roles); }, listener::onFailure diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java index 5b7ce8b1d03..87e1c73b978 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java @@ -27,6 +27,7 @@ import org.elasticsearch.license.License; import org.elasticsearch.license.TestUtils; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.plugins.MapperPlugin; +import org.elasticsearch.script.ScriptService; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.threadpool.ThreadPool; @@ -130,7 +131,7 @@ public class SecurityTests extends ESTestCase { Client client = mock(Client.class); when(client.threadPool()).thenReturn(threadPool); when(client.settings()).thenReturn(settings); - return security.createComponents(client, threadPool, clusterService, mock(ResourceWatcherService.class)); + return security.createComponents(client, threadPool, clusterService, mock(ResourceWatcherService.class), mock(ScriptService.class)); } private static T findComponent(Class type, Collection components) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/rolemapping/TransportPutRoleMappingActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/rolemapping/TransportPutRoleMappingActionTests.java index 91222a5af58..ee5f935fcc5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/rolemapping/TransportPutRoleMappingActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/rolemapping/TransportPutRoleMappingActionTests.java @@ -25,9 +25,10 @@ import java.util.Collections; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.iterableWithSize; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -72,7 +73,8 @@ public class TransportPutRoleMappingActionTests extends ESTestCase { assertThat(mapping.getExpression(), is(expression)); assertThat(mapping.isEnabled(), equalTo(true)); assertThat(mapping.getName(), equalTo("anarchy")); - assertThat(mapping.getRoles(), containsInAnyOrder("superuser")); + assertThat(mapping.getRoles(), iterableWithSize(1)); + assertThat(mapping.getRoles(), contains("superuser")); assertThat(mapping.getMetadata().size(), equalTo(1)); assertThat(mapping.getMetadata().get("dumb"), equalTo(true)); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java index 43e5fb21639..fe8220dad4e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.script.ScriptService; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; @@ -146,7 +147,8 @@ public abstract class KerberosRealmTestCase extends ESTestCase { when(mockClient.threadPool()).thenReturn(threadPool); when(mockClient.settings()).thenReturn(settings); - final NativeRoleMappingStore store = new NativeRoleMappingStore(Settings.EMPTY, mockClient, mock(SecurityIndexManager.class)); + final NativeRoleMappingStore store = new NativeRoleMappingStore(Settings.EMPTY, mockClient, mock(SecurityIndexManager.class), + mock(ScriptService.class)); final NativeRoleMappingStore roleMapper = spy(store); doAnswer(invocation -> { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java index c0a93d36ab8..70e8719c0f7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java @@ -8,6 +8,8 @@ package org.elasticsearch.xpack.security.authc.ldap; import com.unboundid.ldap.sdk.LDAPURL; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.SecureSettings; import org.elasticsearch.common.settings.SecureString; @@ -17,6 +19,9 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.script.ScriptModule; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.mustache.MustacheScriptEngine; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; @@ -29,11 +34,14 @@ import org.elasticsearch.xpack.core.security.authc.ldap.LdapSessionFactorySettin import org.elasticsearch.xpack.core.security.authc.ldap.LdapUserSearchSessionFactorySettings; import org.elasticsearch.xpack.core.security.authc.ldap.PoolingSessionFactorySettings; import org.elasticsearch.xpack.core.security.authc.ldap.SearchGroupsResolverSettings; +import org.elasticsearch.xpack.core.security.authc.ldap.support.LdapMetaDataResolverSettings; import org.elasticsearch.xpack.core.security.authc.ldap.support.LdapSearchScope; import org.elasticsearch.xpack.core.security.authc.support.CachingUsernamePasswordRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; import org.elasticsearch.xpack.core.security.authc.support.DnRoleMapperSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +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.user.User; import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; import org.elasticsearch.xpack.core.ssl.SSLService; @@ -42,6 +50,8 @@ import org.elasticsearch.xpack.security.authc.ldap.support.LdapTestCase; import org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory; import org.elasticsearch.xpack.security.authc.support.DnRoleMapper; import org.elasticsearch.xpack.security.authc.support.MockLookupRealm; +import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; +import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.After; import org.junit.Before; @@ -54,6 +64,7 @@ import java.util.function.Function; import static org.elasticsearch.xpack.core.security.authc.RealmSettings.getFullSettingKey; import static org.elasticsearch.xpack.core.security.authc.ldap.support.SessionFactorySettings.URLS_SETTING; import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -394,6 +405,75 @@ public class LdapRealmTests extends LdapTestCase { assertThat(user.roles(), arrayContaining("avenger")); } + /** + * This tests template role mappings (see + * {@link TemplateRoleName}) with an LDAP realm, using a additional + * metadata field (see {@link LdapMetaDataResolverSettings#ADDITIONAL_META_DATA_SETTING}). + */ + public void testLdapRealmWithTemplatedRoleMapping() throws Exception { + String groupSearchBase = "o=sevenSeas"; + String userTemplate = VALID_USER_TEMPLATE; + Settings settings = Settings.builder() + .put(defaultGlobalSettings) + .put(buildLdapSettings(ldapUrls(), userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE)) + .put(getFullSettingKey(REALM_IDENTIFIER.getName(), LdapMetaDataResolverSettings.ADDITIONAL_META_DATA_SETTING), "uid") + .build(); + RealmConfig config = getRealmConfig(REALM_IDENTIFIER, settings); + + SecurityIndexManager mockSecurityIndex = mock(SecurityIndexManager.class); + when(mockSecurityIndex.isAvailable()).thenReturn(true); + when(mockSecurityIndex.isIndexUpToDate()).thenReturn(true); + when(mockSecurityIndex.isMappingUpToDate()).thenReturn(true); + + Client mockClient = mock(Client.class); + when(mockClient.threadPool()).thenReturn(threadPool); + + final ScriptService scriptService = new ScriptService(defaultGlobalSettings, + Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()), ScriptModule.CORE_CONTEXTS); + NativeRoleMappingStore roleMapper = new NativeRoleMappingStore(defaultGlobalSettings, mockClient, mockSecurityIndex, + scriptService) { + @Override + protected void loadMappings(ActionListener> listener) { + listener.onResponse( + Arrays.asList( + this.buildMapping("m1", new BytesArray("{" + + "\"role_templates\":[{\"template\":{\"source\":\"_user_{{metadata.uid}}\"}}]," + + "\"enabled\":true," + + "\"rules\":{ \"any\":[" + + " { \"field\":{\"realm.name\":\"ldap1\"}}," + + " { \"field\":{\"realm.name\":\"ldap2\"}}" + + "]}}")), + this.buildMapping("m2", new BytesArray("{" + + "\"roles\":[\"should_not_happen\"]," + + "\"enabled\":true," + + "\"rules\":{ \"all\":[" + + " { \"field\":{\"realm.name\":\"ldap1\"}}," + + " { \"field\":{\"realm.name\":\"ldap2\"}}" + + "]}}")), + this.buildMapping("m3", new BytesArray("{" + + "\"roles\":[\"sales_admin\"]," + + "\"enabled\":true," + + "\"rules\":" + + " { \"field\":{\"dn\":\"*,ou=people,o=sevenSeas\"}}" + + "}")) + ) + ); + } + }; + LdapSessionFactory ldapFactory = new LdapSessionFactory(config, sslService, threadPool); + LdapRealm ldap = new LdapRealm(config, ldapFactory, + roleMapper, threadPool); + ldap.initialize(Collections.singleton(ldap), licenseState); + + PlainActionFuture future = new PlainActionFuture<>(); + ldap.authenticate(new UsernamePasswordToken("Horatio Hornblower", new SecureString(PASSWORD)), future); + final AuthenticationResult result = future.actionGet(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + User user = result.getUser(); + assertThat(user, notNullValue()); + assertThat(user.roles(), arrayContainingInAnyOrder("_user_hhornblo", "sales_admin")); + } + /** * The contract for {@link Realm} implementations is that they should log-and-return-null (and * not call {@link ActionListener#onFailure(Exception)}) if there is an internal exception that prevented them from performing an diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/ExpressionRoleMappingTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/ExpressionRoleMappingTests.java index 729bd08d7fa..57db6005119 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/ExpressionRoleMappingTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/ExpressionRoleMappingTests.java @@ -5,31 +5,48 @@ */ package org.elasticsearch.xpack.security.authc.support.mapper; +import org.elasticsearch.Version; import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.ByteBufferStreamInput; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.env.Environment; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.VersionUtils; +import org.elasticsearch.xpack.core.XPackClientPlugin; import org.elasticsearch.xpack.core.security.authc.RealmConfig; 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.AllExpression; +import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; import org.hamcrest.Matchers; import org.junit.Before; import org.mockito.Mockito; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Locale; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.iterableWithSize; import static org.hamcrest.Matchers.notNullValue; public class ExpressionRoleMappingTests extends ESTestCase { @@ -39,44 +56,44 @@ public class ExpressionRoleMappingTests extends ESTestCase { @Before public void setupMapping() throws Exception { realm = new RealmConfig(new RealmConfig.RealmIdentifier("ldap", "ldap1"), - Settings.EMPTY, Mockito.mock(Environment.class), new ThreadContext(Settings.EMPTY)); + Settings.EMPTY, Mockito.mock(Environment.class), new ThreadContext(Settings.EMPTY)); } - public void testParseValidJson() throws Exception { + public void testParseValidJsonWithFixedRoleNames() throws Exception { String json = "{" - + "\"roles\": [ \"kibana_user\", \"sales\" ], " - + "\"enabled\": true, " - + "\"rules\": { " - + " \"all\": [ " - + " { \"field\": { \"dn\" : \"*,ou=sales,dc=example,dc=com\" } }, " - + " { \"except\": { \"field\": { \"metadata.active\" : false } } }" - + " ]}" - + "}"; + + "\"roles\": [ \"kibana_user\", \"sales\" ], " + + "\"enabled\": true, " + + "\"rules\": { " + + " \"all\": [ " + + " { \"field\": { \"dn\" : \"*,ou=sales,dc=example,dc=com\" } }, " + + " { \"except\": { \"field\": { \"metadata.active\" : false } } }" + + " ]}" + + "}"; final ExpressionRoleMapping mapping = parse(json, "ldap_sales"); assertThat(mapping.getRoles(), Matchers.containsInAnyOrder("kibana_user", "sales")); assertThat(mapping.getExpression(), instanceOf(AllExpression.class)); final UserRoleMapper.UserData user1a = new UserRoleMapper.UserData( - "john.smith", "cn=john.smith,ou=sales,dc=example,dc=com", - Collections.emptyList(), Collections.singletonMap("active", true), realm + "john.smith", "cn=john.smith,ou=sales,dc=example,dc=com", + Collections.emptyList(), Collections.singletonMap("active", true), realm ); final UserRoleMapper.UserData user1b = new UserRoleMapper.UserData( - user1a.getUsername(), user1a.getDn().toUpperCase(Locale.US), user1a.getGroups(), user1a.getMetadata(), user1a.getRealm() + user1a.getUsername(), user1a.getDn().toUpperCase(Locale.US), user1a.getGroups(), user1a.getMetadata(), user1a.getRealm() ); final UserRoleMapper.UserData user1c = new UserRoleMapper.UserData( - user1a.getUsername(), user1a.getDn().replaceAll(",", ", "), user1a.getGroups(), user1a.getMetadata(), user1a.getRealm() + user1a.getUsername(), user1a.getDn().replaceAll(",", ", "), user1a.getGroups(), user1a.getMetadata(), user1a.getRealm() ); final UserRoleMapper.UserData user1d = new UserRoleMapper.UserData( - user1a.getUsername(), user1a.getDn().replaceAll("dc=", "DC="), user1a.getGroups(), user1a.getMetadata(), user1a.getRealm() + user1a.getUsername(), user1a.getDn().replaceAll("dc=", "DC="), user1a.getGroups(), user1a.getMetadata(), user1a.getRealm() ); final UserRoleMapper.UserData user2 = new UserRoleMapper.UserData( - "jamie.perez", "cn=jamie.perez,ou=sales,dc=example,dc=com", - Collections.emptyList(), Collections.singletonMap("active", false), realm + "jamie.perez", "cn=jamie.perez,ou=sales,dc=example,dc=com", + Collections.emptyList(), Collections.singletonMap("active", false), realm ); final UserRoleMapper.UserData user3 = new UserRoleMapper.UserData( - "simone.ng", "cn=simone.ng,ou=finance,dc=example,dc=com", - Collections.emptyList(), Collections.singletonMap("active", true), realm + "simone.ng", "cn=simone.ng,ou=finance,dc=example,dc=com", + Collections.emptyList(), Collections.singletonMap("active", true), realm ); assertThat(mapping.getExpression().match(user1a.asModel()), equalTo(true)); @@ -87,58 +104,218 @@ public class ExpressionRoleMappingTests extends ESTestCase { assertThat(mapping.getExpression().match(user3.asModel()), equalTo(false)); } + public void testParseValidJsonWithTemplatedRoleNames() throws Exception { + String json = "{" + + "\"role_templates\": [ " + + " { \"template\" : { \"source\":\"kibana_user\"} }," + + " { \"template\" : { \"source\":\"sales\"} }," + + " { \"template\" : { \"source\":\"_user_{{username}}\" }, \"format\":\"string\" }" + + " ], " + + "\"enabled\": true, " + + "\"rules\": { " + + " \"all\": [ " + + " { \"field\": { \"dn\" : \"*,ou=sales,dc=example,dc=com\" } }, " + + " { \"except\": { \"field\": { \"metadata.active\" : false } } }" + + " ]}" + + "}"; + final ExpressionRoleMapping mapping = parse(json, "ldap_sales"); + assertThat(mapping.getRoleTemplates(), iterableWithSize(3)); + assertThat(mapping.getRoleTemplates().get(0).getTemplate().utf8ToString(), equalTo("{\"source\":\"kibana_user\"}")); + assertThat(mapping.getRoleTemplates().get(0).getFormat(), equalTo(TemplateRoleName.Format.STRING)); + assertThat(mapping.getRoleTemplates().get(1).getTemplate().utf8ToString(), equalTo("{\"source\":\"sales\"}")); + assertThat(mapping.getRoleTemplates().get(1).getFormat(), equalTo(TemplateRoleName.Format.STRING)); + assertThat(mapping.getRoleTemplates().get(2).getTemplate().utf8ToString(), equalTo("{\"source\":\"_user_{{username}}\"}")); + assertThat(mapping.getRoleTemplates().get(2).getFormat(), equalTo(TemplateRoleName.Format.STRING)); + } + public void testParsingFailsIfRulesAreMissing() throws Exception { String json = "{" - + "\"roles\": [ \"kibana_user\", \"sales\" ], " - + "\"enabled\": true " - + "}"; + + "\"roles\": [ \"kibana_user\", \"sales\" ], " + + "\"enabled\": true " + + "}"; ParsingException ex = expectThrows(ParsingException.class, () -> parse(json, "bad_json")); assertThat(ex.getMessage(), containsString("rules")); } public void testParsingFailsIfRolesMissing() throws Exception { String json = "{" - + "\"enabled\": true, " - + "\"rules\": " - + " { \"field\": { \"dn\" : \"*,ou=sales,dc=example,dc=com\" } } " - + "}"; + + "\"enabled\": true, " + + "\"rules\": " + + " { \"field\": { \"dn\" : \"*,ou=sales,dc=example,dc=com\" } } " + + "}"; ParsingException ex = expectThrows(ParsingException.class, () -> parse(json, "bad_json")); assertThat(ex.getMessage(), containsString("role")); } public void testParsingFailsIfThereAreUnrecognisedFields() throws Exception { String json = "{" - + "\"disabled\": false, " - + "\"roles\": [ \"kibana_user\", \"sales\" ], " - + "\"rules\": " - + " { \"field\": { \"dn\" : \"*,ou=sales,dc=example,dc=com\" } } " - + "}"; + + "\"disabled\": false, " + + "\"roles\": [ \"kibana_user\", \"sales\" ], " + + "\"rules\": " + + " { \"field\": { \"dn\" : \"*,ou=sales,dc=example,dc=com\" } } " + + "}"; ParsingException ex = expectThrows(ParsingException.class, () -> parse(json, "bad_json")); assertThat(ex.getMessage(), containsString("disabled")); } public void testParsingIgnoresTypeFields() throws Exception { String json = "{" - + "\"enabled\": true, " - + "\"roles\": [ \"kibana_user\", \"sales\" ], " - + "\"rules\": " - + " { \"field\": { \"dn\" : \"*,ou=sales,dc=example,dc=com\" } }, " - + "\"doc_type\": \"role-mapping\", " - + "\"type\": \"doc\"" - + "}"; - final ExpressionRoleMapping mapping = parse(json, "from_index"); + + "\"enabled\": true, " + + "\"roles\": [ \"kibana_user\", \"sales\" ], " + + "\"rules\": " + + " { \"field\": { \"dn\" : \"*,ou=sales,dc=example,dc=com\" } }, " + + "\"doc_type\": \"role-mapping\", " + + "\"type\": \"doc\"" + + "}"; + final ExpressionRoleMapping mapping = parse(json, "from_index", true); assertThat(mapping.isEnabled(), equalTo(true)); - assertThat(mapping.getRoles(), containsInAnyOrder("kibana_user", "sales")); + assertThat(mapping.getRoles(), Matchers.containsInAnyOrder("kibana_user", "sales")); + } + + public void testParsingOfBothRoleNamesAndTemplates() throws Exception { + String json = "{" + + "\"enabled\": true, " + + "\"roles\": [ \"kibana_user\", \"sales\" ], " + + "\"role_templates\": [" + + " { \"template\" : \"{ \\\"source\\\":\\\"_user_{{username}}\\\" }\", \"format\":\"string\" }" + + "]," + + "\"rules\": " + + " { \"field\": { \"dn\" : \"*,ou=sales,dc=example,dc=com\" } }" + + "}"; + + // This is rejected when validating a request, but is valid when parsing the mapping + final ExpressionRoleMapping mapping = parse(json, "from_api", false); + assertThat(mapping.getRoles(), iterableWithSize(2)); + assertThat(mapping.getRoleTemplates(), iterableWithSize(1)); + } + + public void testToXContentWithRoleNames() throws Exception { + String source = "{" + + "\"roles\": [ " + + " \"kibana_user\"," + + " \"sales\"" + + " ], " + + "\"enabled\": true, " + + "\"rules\": { \"field\": { \"realm.name\" : \"saml1\" } }" + + "}"; + final ExpressionRoleMapping mapping = parse(source, getTestName()); + assertThat(mapping.getRoles(), iterableWithSize(2)); + + final String xcontent = Strings.toString(mapping); + assertThat(xcontent, equalTo( + "{" + + "\"enabled\":true," + + "\"roles\":[" + + "\"kibana_user\"," + + "\"sales\"" + + "]," + + "\"rules\":{\"field\":{\"realm.name\":\"saml1\"}}," + + "\"metadata\":{}" + + "}" + )); + } + + public void testToXContentWithTemplates() throws Exception { + String source = "{" + + "\"metadata\" : { \"answer\":42 }," + + "\"role_templates\": [ " + + " { \"template\" : { \"source\":\"_user_{{username}}\" }, \"format\":\"string\" }," + + " { \"template\" : { \"source\":\"{{#tojson}}groups{{/tojson}}\" }, \"format\":\"json\" }" + + " ], " + + "\"enabled\": false, " + + "\"rules\": { \"field\": { \"realm.name\" : \"saml1\" } }" + + "}"; + final ExpressionRoleMapping mapping = parse(source, getTestName()); + assertThat(mapping.getRoleTemplates(), iterableWithSize(2)); + + final String xcontent = Strings.toString(mapping.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS, true)); + assertThat(xcontent, equalTo( + "{" + + "\"enabled\":false," + + "\"role_templates\":[" + + "{\"template\":\"{\\\"source\\\":\\\"_user_{{username}}\\\"}\",\"format\":\"string\"}," + + "{\"template\":\"{\\\"source\\\":\\\"{{#tojson}}groups{{/tojson}}\\\"}\",\"format\":\"json\"}" + + "]," + + "\"rules\":{\"field\":{\"realm.name\":\"saml1\"}}," + + "\"metadata\":{\"answer\":42}," + + "\"doc_type\":\"role-mapping\"" + + "}" + )); + + final ExpressionRoleMapping parsed = parse(xcontent, getTestName(), true); + assertThat(parsed.getRoles(), iterableWithSize(0)); + assertThat(parsed.getRoleTemplates(), iterableWithSize(2)); + assertThat(parsed.getMetadata(), Matchers.hasKey("answer")); + } + + public void testSerialization() throws Exception { + final ExpressionRoleMapping original = randomRoleMapping(true); + + final Version version = VersionUtils.randomVersionBetween(random(), Version.V_7_1_0, null); + BytesStreamOutput output = new BytesStreamOutput(); + output.setVersion(version); + original.writeTo(output); + + final NamedWriteableRegistry registry = new NamedWriteableRegistry(new XPackClientPlugin(Settings.EMPTY).getNamedWriteables()); + StreamInput streamInput = new NamedWriteableAwareStreamInput(ByteBufferStreamInput.wrap(BytesReference.toBytes(output.bytes())), + registry); + streamInput.setVersion(version); + final ExpressionRoleMapping serialized = new ExpressionRoleMapping(streamInput); + assertEquals(original, serialized); + } + + public void testSerializationPreV71() throws Exception { + final ExpressionRoleMapping original = randomRoleMapping(false); + + final Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.V_7_0_0); + BytesStreamOutput output = new BytesStreamOutput(); + output.setVersion(version); + original.writeTo(output); + + final NamedWriteableRegistry registry = new NamedWriteableRegistry(new XPackClientPlugin(Settings.EMPTY).getNamedWriteables()); + StreamInput streamInput = new NamedWriteableAwareStreamInput(ByteBufferStreamInput.wrap(BytesReference.toBytes(output.bytes())), + registry); + streamInput.setVersion(version); + final ExpressionRoleMapping serialized = new ExpressionRoleMapping(streamInput); + assertEquals(original, serialized); } private ExpressionRoleMapping parse(String json, String name) throws IOException { + return parse(json, name, false); + } + + private ExpressionRoleMapping parse(String json, String name, boolean fromIndex) throws IOException { final NamedXContentRegistry registry = NamedXContentRegistry.EMPTY; final XContentParser parser = XContentType.JSON.xContent() - .createParser(registry, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json); + .createParser(registry, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json); final ExpressionRoleMapping mapping = ExpressionRoleMapping.parse(name, parser); assertThat(mapping, notNullValue()); assertThat(mapping.getName(), equalTo(name)); return mapping; } + private ExpressionRoleMapping randomRoleMapping(boolean acceptRoleTemplates) { + final boolean useTemplate = acceptRoleTemplates && randomBoolean(); + final List roles; + final List templates; + if (useTemplate) { + roles = Collections.emptyList(); + templates = Arrays.asList(randomArray(1, 5, TemplateRoleName[]::new, () -> + new TemplateRoleName(new BytesArray(randomAlphaOfLengthBetween(10, 25)), randomFrom(TemplateRoleName.Format.values())) + )); + } else { + roles = Arrays.asList(randomArray(1, 5, String[]::new, () -> randomAlphaOfLengthBetween(4, 12))); + templates = Collections.emptyList(); + } + return new ExpressionRoleMapping( + randomAlphaOfLengthBetween(3, 8), + new FieldExpression(randomAlphaOfLengthBetween(4, 12), + Collections.singletonList(new FieldExpression.FieldValue(randomInt(99)))), + roles, + templates, + Collections.singletonMap(randomAlphaOfLengthBetween(3, 12), randomIntBetween(30, 90)), + true + ); + } + } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStoreTests.java index 29407a86729..e96284ba154 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStoreTests.java @@ -10,10 +10,14 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.health.ClusterHealthStatus; +import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.script.ScriptModule; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.mustache.MustacheScriptEngine; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheAction; @@ -23,6 +27,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; 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.FieldExpression; import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression.FieldValue; import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames; @@ -54,12 +59,12 @@ public class NativeRoleMappingStoreTests extends ESTestCase { // Does match DN final ExpressionRoleMapping mapping1 = new ExpressionRoleMapping("dept_h", new FieldExpression("dn", Collections.singletonList(new FieldValue("*,ou=dept_h,o=forces,dc=gc,dc=ca"))), - Arrays.asList("dept_h", "defence"), Collections.emptyMap(), true); + Arrays.asList("dept_h", "defence"), Collections.emptyList(), Collections.emptyMap(), true); // Does not match - user is not in this group final ExpressionRoleMapping mapping2 = new ExpressionRoleMapping("admin", - new FieldExpression("groups", Collections.singletonList( - new FieldValue(randomiseDn("cn=esadmin,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")))), - Arrays.asList("admin"), Collections.emptyMap(), true); + new FieldExpression("groups", Collections.singletonList( + new FieldValue(randomiseDn("cn=esadmin,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")))), + Arrays.asList("admin"), Collections.emptyList(), Collections.emptyMap(), true); // Does match - user is one of these groups final ExpressionRoleMapping mapping3 = new ExpressionRoleMapping("flight", new FieldExpression("groups", Arrays.asList( @@ -67,18 +72,23 @@ public class NativeRoleMappingStoreTests extends ESTestCase { new FieldValue(randomiseDn("cn=betaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")), new FieldValue(randomiseDn("cn=gammaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")) )), - Arrays.asList("flight"), Collections.emptyMap(), true); + Collections.emptyList(), + Arrays.asList(new TemplateRoleName(new BytesArray("{ \"source\":\"{{metadata.extra_group}}\" }"), + TemplateRoleName.Format.STRING)), + Collections.emptyMap(), true); // Does not match - mapping is not enabled final ExpressionRoleMapping mapping4 = new ExpressionRoleMapping("mutants", new FieldExpression("groups", Collections.singletonList( new FieldValue(randomiseDn("cn=mutants,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")))), - Arrays.asList("mutants"), Collections.emptyMap(), false); + Arrays.asList("mutants"), Collections.emptyList(), Collections.emptyMap(), false); final Client client = mock(Client.class); SecurityIndexManager securityIndex = mock(SecurityIndexManager.class); + ScriptService scriptService = new ScriptService(Settings.EMPTY, + Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()), ScriptModule.CORE_CONTEXTS); when(securityIndex.isAvailable()).thenReturn(true); - final NativeRoleMappingStore store = new NativeRoleMappingStore(Settings.EMPTY, client, securityIndex) { + final NativeRoleMappingStore store = new NativeRoleMappingStore(Settings.EMPTY, client, securityIndex, scriptService) { @Override protected void loadMappings(ActionListener> listener) { final List mappings = Arrays.asList(mapping1, mapping2, mapping3, mapping4); @@ -96,7 +106,7 @@ public class NativeRoleMappingStoreTests extends ESTestCase { Arrays.asList( randomiseDn("cn=alphaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"), randomiseDn("cn=mutants,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca") - ), Collections.emptyMap(), realm); + ), Collections.singletonMap("extra_group", "flight"), realm); logger.info("UserData is [{}]", user); store.resolveRoles(user, future); @@ -213,7 +223,8 @@ public class NativeRoleMappingStoreTests extends ESTestCase { listener.onResponse(null); } }; - final NativeRoleMappingStore store = new NativeRoleMappingStore(Settings.EMPTY, client, mock(SecurityIndexManager.class)); + final NativeRoleMappingStore store = new NativeRoleMappingStore(Settings.EMPTY, client, mock(SecurityIndexManager.class), + mock(ScriptService.class)); store.refreshRealmOnChange(mockRealm); return store; } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/security.put_role_mapping.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/security.put_role_mapping.json index 626ff0d6da8..d65cf8f8358 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/security.put_role_mapping.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/security.put_role_mapping.json @@ -23,7 +23,7 @@ } }, "body": { - "description" : "The role to add", + "description" : "The role mapping to add", "required" : true } }