Skip to content

Commit 2c770ba

Browse files
authored
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
1 parent 965e311 commit 2c770ba

File tree

29 files changed

+1364
-291
lines changed

29 files changed

+1364
-291
lines changed

client/rest-high-level/src/main/java/org/elasticsearch/client/security/ExpressionRoleMapping.java

Lines changed: 27 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@
2929
import java.util.Collections;
3030
import java.util.List;
3131
import java.util.Map;
32+
import java.util.Objects;
3233

3334
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
35+
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
3436

3537
/**
3638
* A representation of a single role-mapping.
@@ -42,20 +44,22 @@ public final class ExpressionRoleMapping {
4244

4345
@SuppressWarnings("unchecked")
4446
static final ConstructingObjectParser<ExpressionRoleMapping, String> PARSER = new ConstructingObjectParser<>("role-mapping", true,
45-
(args, name) -> new ExpressionRoleMapping(name, (RoleMapperExpression) args[0], (List<String>) args[1],
46-
(Map<String, Object>) args[2], (boolean) args[3]));
47+
(args, name) -> new ExpressionRoleMapping(name, (RoleMapperExpression) args[0], (List<String>) args[1],
48+
(List<TemplateRoleName>) args[2], (Map<String, Object>) args[3], (boolean) args[4]));
4749

4850
static {
4951
PARSER.declareField(constructorArg(), (parser, context) -> RoleMapperExpressionParser.fromXContent(parser), Fields.RULES,
5052
ObjectParser.ValueType.OBJECT);
51-
PARSER.declareStringArray(constructorArg(), Fields.ROLES);
53+
PARSER.declareStringArray(optionalConstructorArg(), Fields.ROLES);
54+
PARSER.declareObjectArray(optionalConstructorArg(), (parser, ctx) -> TemplateRoleName.fromXContent(parser), Fields.ROLE_TEMPLATES);
5255
PARSER.declareField(constructorArg(), XContentParser::map, Fields.METADATA, ObjectParser.ValueType.OBJECT);
5356
PARSER.declareBoolean(constructorArg(), Fields.ENABLED);
5457
}
5558

5659
private final String name;
5760
private final RoleMapperExpression expression;
5861
private final List<String> roles;
62+
private final List<TemplateRoleName> roleTemplates;
5963
private final Map<String, Object> metadata;
6064
private final boolean enabled;
6165

@@ -70,10 +74,11 @@ public final class ExpressionRoleMapping {
7074
* @param enabled a flag when {@code true} signifies the role mapping is active
7175
*/
7276
public ExpressionRoleMapping(final String name, final RoleMapperExpression expr, final List<String> roles,
73-
final Map<String, Object> metadata, boolean enabled) {
77+
final List<TemplateRoleName> templates, final Map<String, Object> metadata, boolean enabled) {
7478
this.name = name;
7579
this.expression = expr;
76-
this.roles = Collections.unmodifiableList(roles);
80+
this.roles = roles == null ? Collections.emptyList() : Collections.unmodifiableList(roles);
81+
this.roleTemplates = templates == null ? Collections.emptyList() : Collections.unmodifiableList(templates);
7782
this.metadata = (metadata == null) ? Collections.emptyMap() : Collections.unmodifiableMap(metadata);
7883
this.enabled = enabled;
7984
}
@@ -90,6 +95,10 @@ public List<String> getRoles() {
9095
return roles;
9196
}
9297

98+
public List<TemplateRoleName> getRoleTemplates() {
99+
return roleTemplates;
100+
}
101+
93102
public Map<String, Object> getMetadata() {
94103
return metadata;
95104
}
@@ -99,53 +108,26 @@ public boolean isEnabled() {
99108
}
100109

101110
@Override
102-
public int hashCode() {
103-
final int prime = 31;
104-
int result = 1;
105-
result = prime * result + (enabled ? 1231 : 1237);
106-
result = prime * result + ((expression == null) ? 0 : expression.hashCode());
107-
result = prime * result + ((metadata == null) ? 0 : metadata.hashCode());
108-
result = prime * result + ((name == null) ? 0 : name.hashCode());
109-
result = prime * result + ((roles == null) ? 0 : roles.hashCode());
110-
return result;
111+
public boolean equals(Object o) {
112+
if (this == o) return true;
113+
if (o == null || getClass() != o.getClass()) return false;
114+
final ExpressionRoleMapping that = (ExpressionRoleMapping) o;
115+
return this.enabled == that.enabled &&
116+
Objects.equals(this.name, that.name) &&
117+
Objects.equals(this.expression, that.expression) &&
118+
Objects.equals(this.roles, that.roles) &&
119+
Objects.equals(this.roleTemplates, that.roleTemplates) &&
120+
Objects.equals(this.metadata, that.metadata);
111121
}
112122

113123
@Override
114-
public boolean equals(Object obj) {
115-
if (this == obj)
116-
return true;
117-
if (obj == null)
118-
return false;
119-
if (getClass() != obj.getClass())
120-
return false;
121-
final ExpressionRoleMapping other = (ExpressionRoleMapping) obj;
122-
if (enabled != other.enabled)
123-
return false;
124-
if (expression == null) {
125-
if (other.expression != null)
126-
return false;
127-
} else if (!expression.equals(other.expression))
128-
return false;
129-
if (metadata == null) {
130-
if (other.metadata != null)
131-
return false;
132-
} else if (!metadata.equals(other.metadata))
133-
return false;
134-
if (name == null) {
135-
if (other.name != null)
136-
return false;
137-
} else if (!name.equals(other.name))
138-
return false;
139-
if (roles == null) {
140-
if (other.roles != null)
141-
return false;
142-
} else if (!roles.equals(other.roles))
143-
return false;
144-
return true;
124+
public int hashCode() {
125+
return Objects.hash(name, expression, roles, roleTemplates, metadata, enabled);
145126
}
146127

147128
public interface Fields {
148129
ParseField ROLES = new ParseField("roles");
130+
ParseField ROLE_TEMPLATES = new ParseField("role_templates");
149131
ParseField ENABLED = new ParseField("enabled");
150132
ParseField RULES = new ParseField("rules");
151133
ParseField METADATA = new ParseField("metadata");

client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutRoleMappingRequest.java

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,34 @@ public final class PutRoleMappingRequest implements Validatable, ToXContentObjec
4040
private final String name;
4141
private final boolean enabled;
4242
private final List<String> roles;
43+
private final List<TemplateRoleName> roleTemplates;
4344
private final RoleMapperExpression rules;
4445

4546
private final Map<String, Object> metadata;
4647
private final RefreshPolicy refreshPolicy;
4748

49+
@Deprecated
4850
public PutRoleMappingRequest(final String name, final boolean enabled, final List<String> roles, final RoleMapperExpression rules,
49-
@Nullable final Map<String, Object> metadata, @Nullable final RefreshPolicy refreshPolicy) {
51+
@Nullable final Map<String, Object> metadata, @Nullable final RefreshPolicy refreshPolicy) {
52+
this(name, enabled, roles, Collections.emptyList(), rules, metadata, refreshPolicy);
53+
}
54+
55+
public PutRoleMappingRequest(final String name, final boolean enabled, final List<String> roles, final List<TemplateRoleName> templates,
56+
final RoleMapperExpression rules, @Nullable final Map<String, Object> metadata,
57+
@Nullable final RefreshPolicy refreshPolicy) {
5058
if (Strings.hasText(name) == false) {
5159
throw new IllegalArgumentException("role-mapping name is missing");
5260
}
5361
this.name = name;
5462
this.enabled = enabled;
55-
if (roles == null || roles.isEmpty()) {
56-
throw new IllegalArgumentException("role-mapping roles are missing");
63+
this.roles = Collections.unmodifiableList(Objects.requireNonNull(roles, "role-mapping roles cannot be null"));
64+
this.roleTemplates = Collections.unmodifiableList(Objects.requireNonNull(templates, "role-mapping role_templates cannot be null"));
65+
if (this.roles.isEmpty() && this.roleTemplates.isEmpty()) {
66+
throw new IllegalArgumentException("in a role-mapping, one of roles or role_templates is required");
67+
}
68+
if (this.roles.isEmpty() == false && this.roleTemplates.isEmpty() == false) {
69+
throw new IllegalArgumentException("in a role-mapping, cannot specify both roles and role_templates");
5770
}
58-
this.roles = Collections.unmodifiableList(roles);
5971
this.rules = Objects.requireNonNull(rules, "role-mapping rules are missing");
6072
this.metadata = (metadata == null) ? Collections.emptyMap() : metadata;
6173
this.refreshPolicy = (refreshPolicy == null) ? RefreshPolicy.getDefault() : refreshPolicy;
@@ -73,6 +85,10 @@ public List<String> getRoles() {
7385
return roles;
7486
}
7587

88+
public List<TemplateRoleName> getRoleTemplates() {
89+
return roleTemplates;
90+
}
91+
7692
public RoleMapperExpression getRules() {
7793
return rules;
7894
}
@@ -87,7 +103,7 @@ public RefreshPolicy getRefreshPolicy() {
87103

88104
@Override
89105
public int hashCode() {
90-
return Objects.hash(name, enabled, refreshPolicy, roles, rules, metadata);
106+
return Objects.hash(name, enabled, refreshPolicy, roles, roleTemplates, rules, metadata);
91107
}
92108

93109
@Override
@@ -104,21 +120,22 @@ public boolean equals(Object obj) {
104120
final PutRoleMappingRequest other = (PutRoleMappingRequest) obj;
105121

106122
return (enabled == other.enabled) &&
107-
(refreshPolicy == other.refreshPolicy) &&
108-
Objects.equals(name, other.name) &&
109-
Objects.equals(roles, other.roles) &&
110-
Objects.equals(rules, other.rules) &&
111-
Objects.equals(metadata, other.metadata);
123+
(refreshPolicy == other.refreshPolicy) &&
124+
Objects.equals(name, other.name) &&
125+
Objects.equals(roles, other.roles) &&
126+
Objects.equals(roleTemplates, other.roleTemplates) &&
127+
Objects.equals(rules, other.rules) &&
128+
Objects.equals(metadata, other.metadata);
112129
}
113130

114131
@Override
115132
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
116133
builder.startObject();
117134
builder.field("enabled", enabled);
118135
builder.field("roles", roles);
136+
builder.field("role_templates", roleTemplates);
119137
builder.field("rules", rules);
120138
builder.field("metadata", metadata);
121139
return builder.endObject();
122140
}
123-
124141
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client.security;
21+
22+
import org.elasticsearch.common.ParseField;
23+
import org.elasticsearch.common.Strings;
24+
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
25+
import org.elasticsearch.common.xcontent.ObjectParser;
26+
import org.elasticsearch.common.xcontent.ToXContentObject;
27+
import org.elasticsearch.common.xcontent.XContentBuilder;
28+
import org.elasticsearch.common.xcontent.XContentParser;
29+
import org.elasticsearch.common.xcontent.XContentParserUtils;
30+
import org.elasticsearch.common.xcontent.XContentType;
31+
32+
import java.io.IOException;
33+
import java.util.Locale;
34+
import java.util.Map;
35+
import java.util.Objects;
36+
37+
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
38+
39+
/**
40+
* A role name that uses a dynamic template.
41+
*/
42+
public class TemplateRoleName implements ToXContentObject {
43+
44+
private static final ConstructingObjectParser<TemplateRoleName, Void> PARSER = new ConstructingObjectParser<>("template-role-name",
45+
true, args -> new TemplateRoleName((String) args[0], (Format) args[1]));
46+
47+
static {
48+
PARSER.declareString(ConstructingObjectParser.constructorArg(), Fields.TEMPLATE);
49+
PARSER.declareField(optionalConstructorArg(), Format::fromXContent, Fields.FORMAT, ObjectParser.ValueType.STRING);
50+
}
51+
private final String template;
52+
private final Format format;
53+
54+
public TemplateRoleName(String template, Format format) {
55+
this.template = Objects.requireNonNull(template);
56+
this.format = Objects.requireNonNull(format);
57+
}
58+
59+
public TemplateRoleName(Map<String, Object> template, Format format) throws IOException {
60+
this(Strings.toString(XContentBuilder.builder(XContentType.JSON.xContent()).map(template)), format);
61+
}
62+
63+
public String getTemplate() {
64+
return template;
65+
}
66+
67+
public Format getFormat() {
68+
return format;
69+
}
70+
71+
@Override
72+
public boolean equals(Object o) {
73+
if (this == o) {
74+
return true;
75+
}
76+
if (o == null || getClass() != o.getClass()) {
77+
return false;
78+
}
79+
final TemplateRoleName that = (TemplateRoleName) o;
80+
return Objects.equals(this.template, that.template) &&
81+
this.format == that.format;
82+
}
83+
84+
@Override
85+
public int hashCode() {
86+
return Objects.hash(template, format);
87+
}
88+
89+
@Override
90+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
91+
return builder.startObject()
92+
.field(Fields.TEMPLATE.getPreferredName(), template)
93+
.field(Fields.FORMAT.getPreferredName(), format.name().toLowerCase(Locale.ROOT))
94+
.endObject();
95+
}
96+
97+
static TemplateRoleName fromXContent(XContentParser parser) throws IOException {
98+
XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser::getTokenLocation);
99+
return PARSER.parse(parser, null);
100+
}
101+
102+
103+
public enum Format {
104+
STRING, JSON;
105+
106+
private static Format fromXContent(XContentParser parser) throws IOException {
107+
XContentParserUtils.ensureExpectedToken(XContentParser.Token.VALUE_STRING, parser.currentToken(), parser::getTokenLocation);
108+
return Format.valueOf(parser.text().toUpperCase(Locale.ROOT));
109+
}
110+
}
111+
112+
public interface Fields {
113+
ParseField TEMPLATE = new ParseField("template");
114+
ParseField FORMAT = new ParseField("format");
115+
}
116+
117+
}

client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ public void testPutRoleMapping() throws IOException {
141141
.addExpression(FieldRoleMapperExpression.ofUsername(username))
142142
.addExpression(FieldRoleMapperExpression.ofGroups(groupname))
143143
.build();
144-
final PutRoleMappingRequest putRoleMappingRequest = new PutRoleMappingRequest(roleMappingName, true, Collections.singletonList(
145-
rolename), rules, null, refreshPolicy);
144+
final PutRoleMappingRequest putRoleMappingRequest = new PutRoleMappingRequest(roleMappingName, true,
145+
Collections.singletonList(rolename), Collections.emptyList(), rules, null, refreshPolicy);
146146

147147
final Request request = SecurityRequestConverters.putRoleMapping(putRoleMappingRequest);
148148

0 commit comments

Comments
 (0)