diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/RoleMapperExpression.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/RoleMapperExpression.java new file mode 100644 index 0000000000000..10c0d0911ba55 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/RoleMapperExpression.java @@ -0,0 +1,30 @@ +/* + * 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.support.expressiondsl; + +import org.elasticsearch.common.xcontent.ToXContentObject; + +/** + * Implementations of this interface represent an expression used for user role mapping + * that can later be resolved to a boolean value. + */ +public interface RoleMapperExpression extends ToXContentObject { + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/AllRoleMapperExpression.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/AllRoleMapperExpression.java new file mode 100644 index 0000000000000..b5cbe4d2e425a --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/AllRoleMapperExpression.java @@ -0,0 +1,55 @@ +/* + * 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.support.expressiondsl.expressions; + +import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression; + +import java.util.ArrayList; +import java.util.List; + +/** + * An expression that evaluates to true if-and-only-if all its children + * evaluate to true. + * An all expression with no children is always true. + */ +public final class AllRoleMapperExpression extends CompositeRoleMapperExpression { + + private AllRoleMapperExpression(String name, RoleMapperExpression[] elements) { + super(name, elements); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private List elements = new ArrayList<>(); + + public Builder addExpression(final RoleMapperExpression expression) { + assert expression != null : "expression cannot be null"; + elements.add(expression); + return this; + } + + public AllRoleMapperExpression build() { + return new AllRoleMapperExpression(CompositeType.ALL.getName(), elements.toArray(new RoleMapperExpression[0])); + } + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/AnyRoleMapperExpression.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/AnyRoleMapperExpression.java new file mode 100644 index 0000000000000..7632a071bd1c2 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/AnyRoleMapperExpression.java @@ -0,0 +1,55 @@ +/* + * 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.support.expressiondsl.expressions; + +import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression; + +import java.util.ArrayList; +import java.util.List; + +/** + * An expression that evaluates to true if at least one of its children + * evaluate to true. + * An any expression with no children is never true. + */ +public final class AnyRoleMapperExpression extends CompositeRoleMapperExpression { + + private AnyRoleMapperExpression(String name, RoleMapperExpression[] elements) { + super(name, elements); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private List elements = new ArrayList<>(); + + public Builder addExpression(final RoleMapperExpression expression) { + assert expression != null : "expression cannot be null"; + elements.add(expression); + return this; + } + + public AnyRoleMapperExpression build() { + return new AnyRoleMapperExpression(CompositeType.ANY.getName(), elements.toArray(new RoleMapperExpression[0])); + } + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/CompositeRoleMapperExpression.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/CompositeRoleMapperExpression.java new file mode 100644 index 0000000000000..2519c59b68846 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/CompositeRoleMapperExpression.java @@ -0,0 +1,100 @@ +/* + * 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.support.expressiondsl.expressions; + +import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Expression of role mapper expressions which can be combined by operators like AND, OR + *

+ * Expression builder example: + *

+ * {@code
+ * final RoleMapperExpression allExpression = AllRoleMapperExpression.builder()
+                    .addExpression(AnyRoleMapperExpression.builder()
+                            .addExpression(FieldRoleMapperExpression.ofUsername("user1@example.org"))
+                            .addExpression(FieldRoleMapperExpression.ofUsername("user2@example.org"))
+                            .build())
+                    .addExpression(FieldRoleMapperExpression.ofMetadata("metadata.location", "AMER"))
+                    .addExpression(new ExceptRoleMapperExpression(FieldRoleMapperExpression.ofUsername("user3@example.org")))
+                    .build();
+ * }
+ * 
+ */ +public abstract class CompositeRoleMapperExpression implements RoleMapperExpression { + private final String name; + private final List elements; + + CompositeRoleMapperExpression(final String name, final RoleMapperExpression... elements) { + assert name != null : "field name cannot be null"; + assert elements != null : "at least one field expression is required"; + this.name = name; + this.elements = Collections.unmodifiableList(Arrays.asList(elements)); + } + + public String getName() { + return this.getName(); + } + + public List getElements() { + return elements; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final CompositeRoleMapperExpression that = (CompositeRoleMapperExpression) o; + if (Objects.equals(this.getName(), that.getName()) == false) { + return false; + } + return Objects.equals(this.getElements(), that.getElements()); + } + + @Override + public int hashCode() { + return Objects.hash(name, elements); + } + + @Override + public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + builder.startObject(); + builder.startArray(name); + for (RoleMapperExpression e : elements) { + e.toXContent(builder, params); + } + builder.endArray(); + return builder.endObject(); + } + +} + diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/CompositeType.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/CompositeType.java new file mode 100644 index 0000000000000..1d6c8aea12263 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/CompositeType.java @@ -0,0 +1,59 @@ +/* + * 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.support.expressiondsl.expressions; + +import org.elasticsearch.common.ParseField; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public enum CompositeType { + + ANY("any"), ALL("all"), EXCEPT("except"); + + private static Map nameToType = Collections.unmodifiableMap(initialize()); + private ParseField field; + + CompositeType(String name) { + this.field = new ParseField(name); + } + + public String getName() { + return field.getPreferredName(); + } + + public ParseField getParseField() { + return field; + } + + public static CompositeType fromName(String name) { + return nameToType.get(name); + } + + private static Map initialize() { + Map map = new HashMap<>(); + for (CompositeType field : values()) { + map.put(field.getName(), field); + } + return map; + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/ExceptRoleMapperExpression.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/ExceptRoleMapperExpression.java new file mode 100644 index 0000000000000..c2cad0d18dad1 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/expressions/ExceptRoleMapperExpression.java @@ -0,0 +1,47 @@ +/* + * 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.support.expressiondsl.expressions; + +import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * A negating expression. That is, this expression evaluates to true if-and-only-if + * its delegate expression evaluate to false. + * Syntactically, except expressions are intended to be children of all + * expressions ({@link AllRoleMapperExpression}). + */ +public final class ExceptRoleMapperExpression extends CompositeRoleMapperExpression { + + public ExceptRoleMapperExpression(final RoleMapperExpression expression) { + super(CompositeType.EXCEPT.getName(), expression); + } + + @Override + public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + builder.startObject(); + builder.field(CompositeType.EXCEPT.getName()); + builder.value(getElements().get(0)); + return builder.endObject(); + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/fields/FieldRoleMapperExpression.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/fields/FieldRoleMapperExpression.java new file mode 100644 index 0000000000000..c96ac3cc5b5ec --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/fields/FieldRoleMapperExpression.java @@ -0,0 +1,122 @@ +/* + * 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.support.expressiondsl.fields; + +import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * An expression that evaluates to true if a field (map element) matches + * the provided values. A field expression may have more than one provided value, in which + * case the expression is true if any of the values are matched. + *

+ * Expression builder example: + *

+ * {@code
+ * final RoleMapperExpression usernameExpression = FieldRoleMapperExpression.ofUsername("user1@example.org");
+ * }
+ * 
+ */ +public class FieldRoleMapperExpression implements RoleMapperExpression { + + private final String field; + private final List values; + + public FieldRoleMapperExpression(final String field, final Object... values) { + if (field == null || field.isEmpty()) { + throw new IllegalArgumentException("null or empty field name (" + field + ")"); + } + if (values == null || values.length == 0) { + throw new IllegalArgumentException("null or empty values (" + values + ")"); + } + this.field = field; + this.values = Collections.unmodifiableList(Arrays.asList(values)); + } + + public String getField() { + return field; + } + + public List getValues() { + return values; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + final FieldRoleMapperExpression that = (FieldRoleMapperExpression) o; + + return Objects.equals(this.getField(), that.getField()) && Objects.equals(this.getValues(), that.getValues()); + } + + @Override + public int hashCode() { + int result = field.hashCode(); + result = 31 * result + values.hashCode(); + return result; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.startObject("field"); + builder.startArray(this.field); + for (Object value : values) { + builder.value(value); + } + builder.endArray(); + builder.endObject(); + return builder.endObject(); + } + + public static FieldRoleMapperExpression ofUsername(Object... values) { + return ofKeyValues("username", values); + } + + public static FieldRoleMapperExpression ofGroups(Object... values) { + return ofKeyValues("groups", values); + } + + public static FieldRoleMapperExpression ofDN(Object... values) { + return ofKeyValues("dn", values); + } + + public static FieldRoleMapperExpression ofMetadata(String key, Object... values) { + if (key.startsWith("metadata.") == false) { + throw new IllegalArgumentException("metadata key must have prefix 'metadata.'"); + } + return ofKeyValues(key, values); + } + + public static FieldRoleMapperExpression ofKeyValues(String key, Object... values) { + return new FieldRoleMapperExpression(key, values); + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/parser/RoleMapperExpressionParser.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/parser/RoleMapperExpressionParser.java new file mode 100644 index 0000000000000..98de4f4c2092c --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/expressiondsl/parser/RoleMapperExpressionParser.java @@ -0,0 +1,180 @@ +/* + * 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.support.expressiondsl.parser; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression; +import org.elasticsearch.client.security.support.expressiondsl.expressions.AllRoleMapperExpression; +import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression; +import org.elasticsearch.client.security.support.expressiondsl.expressions.CompositeType; +import org.elasticsearch.client.security.support.expressiondsl.expressions.ExceptRoleMapperExpression; +import org.elasticsearch.client.security.support.expressiondsl.fields.FieldRoleMapperExpression; +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Parses the JSON (XContent) based boolean expression DSL into a tree of + * {@link RoleMapperExpression} objects. + * Note: As this is client side parser, it mostly validates the structure of + * DSL being parsed it does not enforce rules + * like allowing "except" within "except" or "any" expressions. + */ +public final class RoleMapperExpressionParser { + public static final ParseField FIELD = new ParseField("field"); + + /** + * @param name The name of the expression tree within its containing object. + * Used to provide descriptive error messages. + * @param parser A parser over the XContent (typically JSON) DSL + * representation of the expression + */ + public RoleMapperExpression parse(final String name, final XContentParser parser) throws IOException { + return parseRulesObject(name, parser); + } + + private RoleMapperExpression parseRulesObject(final String objectName, final XContentParser parser) + throws IOException { + // find the start of the DSL object + final XContentParser.Token token; + if (parser.currentToken() == null) { + token = parser.nextToken(); + } else { + token = parser.currentToken(); + } + if (token != XContentParser.Token.START_OBJECT) { + throw new ElasticsearchParseException("failed to parse rules expression. expected [{}] to be an object but found [{}] instead", + objectName, token); + } + + final String fieldName = fieldName(objectName, parser); + final RoleMapperExpression expr = parseExpression(parser, fieldName, objectName); + if (parser.nextToken() != XContentParser.Token.END_OBJECT) { + throw new ElasticsearchParseException("failed to parse rules expression. object [{}] contains multiple fields", objectName); + } + return expr; + } + + private RoleMapperExpression parseExpression(XContentParser parser, String field, String objectName) + throws IOException { + + if (CompositeType.ANY.getParseField().match(field, parser.getDeprecationHandler())) { + final AnyRoleMapperExpression.Builder builder = AnyRoleMapperExpression.builder(); + parseExpressionArray(CompositeType.ANY.getParseField(), parser).forEach(builder::addExpression); + return builder.build(); + } else if (CompositeType.ALL.getParseField().match(field, parser.getDeprecationHandler())) { + final AllRoleMapperExpression.Builder builder = AllRoleMapperExpression.builder(); + parseExpressionArray(CompositeType.ALL.getParseField(), parser).forEach(builder::addExpression); + return builder.build(); + } else if (FIELD.match(field, parser.getDeprecationHandler())) { + return parseFieldExpression(parser); + } else if (CompositeType.EXCEPT.getParseField().match(field, parser.getDeprecationHandler())) { + return parseExceptExpression(parser); + } else { + throw new ElasticsearchParseException("failed to parse rules expression. field [{}] is not recognised in object [{}]", field, + objectName); + } + } + + private RoleMapperExpression parseFieldExpression(XContentParser parser) throws IOException { + checkStartObject(parser); + final String fieldName = fieldName(FIELD.getPreferredName(), parser); + + final List values; + if (parser.nextToken() == XContentParser.Token.START_ARRAY) { + values = parseArray(FIELD, parser, this::parseFieldValue); + } else { + values = Collections.singletonList(parseFieldValue(parser)); + } + if (parser.nextToken() != XContentParser.Token.END_OBJECT) { + throw new ElasticsearchParseException("failed to parse rules expression. object [{}] contains multiple fields", + FIELD.getPreferredName()); + } + + return FieldRoleMapperExpression.ofKeyValues(fieldName, values.toArray()); + } + + private RoleMapperExpression parseExceptExpression(XContentParser parser) throws IOException { + checkStartObject(parser); + return new ExceptRoleMapperExpression(parseRulesObject(CompositeType.EXCEPT.getName(), parser)); + } + + private void checkStartObject(XContentParser parser) throws IOException { + final XContentParser.Token token = parser.nextToken(); + if (token != XContentParser.Token.START_OBJECT) { + throw new ElasticsearchParseException("failed to parse rules expression. expected an object but found [{}] instead", token); + } + } + + private String fieldName(String objectName, XContentParser parser) throws IOException { + if (parser.nextToken() != XContentParser.Token.FIELD_NAME) { + throw new ElasticsearchParseException("failed to parse rules expression. object [{}] does not contain any fields", objectName); + } + String parsedFieldName = parser.currentName(); + return parsedFieldName; + } + + private List parseExpressionArray(ParseField field, XContentParser parser) + throws IOException { + parser.nextToken(); // parseArray requires that the parser is positioned + // at the START_ARRAY token + return parseArray(field, parser, p -> parseRulesObject(field.getPreferredName(), p)); + } + + private List parseArray(ParseField field, XContentParser parser, CheckedFunction elementParser) + throws IOException { + final XContentParser.Token token = parser.currentToken(); + if (token == XContentParser.Token.START_ARRAY) { + List list = new ArrayList<>(); + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + list.add(elementParser.apply(parser)); + } + return list; + } else { + throw new ElasticsearchParseException("failed to parse rules expression. field [{}] requires an array", field); + } + } + + private Object parseFieldValue(XContentParser parser) throws IOException { + switch (parser.currentToken()) { + case VALUE_STRING: + return parser.text(); + + case VALUE_BOOLEAN: + return parser.booleanValue(); + + case VALUE_NUMBER: + return parser.longValue(); + + case VALUE_NULL: + return null; + + default: + throw new ElasticsearchParseException("failed to parse rules expression. expected a field value but found [{}] instead", parser + .currentToken()); + } + } + +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/support/expressiondsl/RoleMapperExpressionDslTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/support/expressiondsl/RoleMapperExpressionDslTests.java new file mode 100644 index 0000000000000..df94640f172dd --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/support/expressiondsl/RoleMapperExpressionDslTests.java @@ -0,0 +1,97 @@ +/* + * 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.support.expressiondsl; + +import org.elasticsearch.client.security.support.expressiondsl.expressions.AllRoleMapperExpression; +import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression; +import org.elasticsearch.client.security.support.expressiondsl.expressions.ExceptRoleMapperExpression; +import org.elasticsearch.client.security.support.expressiondsl.fields.FieldRoleMapperExpression; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Date; + +import static org.hamcrest.Matchers.equalTo; + +public class RoleMapperExpressionDslTests extends ESTestCase { + + public void testRoleMapperExpressionToXContentType() throws IOException { + + final RoleMapperExpression allExpression = AllRoleMapperExpression.builder() + .addExpression(AnyRoleMapperExpression.builder() + .addExpression(FieldRoleMapperExpression.ofDN("*,ou=admin,dc=example,dc=com")) + .addExpression(FieldRoleMapperExpression.ofUsername("es-admin", "es-system")) + .build()) + .addExpression(FieldRoleMapperExpression.ofGroups("cn=people,dc=example,dc=com")) + .addExpression(new ExceptRoleMapperExpression(FieldRoleMapperExpression.ofMetadata("metadata.terminated_date", new Date( + 1537145401027L)))) + .build(); + + final XContentBuilder builder = XContentFactory.jsonBuilder(); + allExpression.toXContent(builder, ToXContent.EMPTY_PARAMS); + final String output = Strings.toString(builder); + final String expected = + "{"+ + "\"all\":["+ + "{"+ + "\"any\":["+ + "{"+ + "\"field\":{"+ + "\"dn\":[\"*,ou=admin,dc=example,dc=com\"]"+ + "}"+ + "},"+ + "{"+ + "\"field\":{"+ + "\"username\":["+ + "\"es-admin\","+ + "\"es-system\""+ + "]"+ + "}"+ + "}"+ + "]"+ + "},"+ + "{"+ + "\"field\":{"+ + "\"groups\":[\"cn=people,dc=example,dc=com\"]"+ + "}"+ + "},"+ + "{"+ + "\"except\":{"+ + "\"field\":{"+ + "\"metadata.terminated_date\":[\"2018-09-17T00:50:01.027Z\"]"+ + "}"+ + "}"+ + "}"+ + "]"+ + "}"; + + assertThat(expected, equalTo(output)); + } + + public void testFieldRoleMapperExpressionThrowsExceptionForMissingMetadataPrefix() { + final IllegalArgumentException ile = expectThrows(IllegalArgumentException.class, () -> FieldRoleMapperExpression.ofMetadata( + "terminated_date", new Date(1537145401027L))); + assertThat(ile.getMessage(), equalTo("metadata key must have prefix 'metadata.'")); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/support/expressiondsl/parser/RoleMapperExpressionParserTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/support/expressiondsl/parser/RoleMapperExpressionParserTests.java new file mode 100644 index 0000000000000..24ed5684fa856 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/support/expressiondsl/parser/RoleMapperExpressionParserTests.java @@ -0,0 +1,129 @@ +/* + * 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.support.expressiondsl.parser; + +import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression; +import org.elasticsearch.client.security.support.expressiondsl.expressions.CompositeRoleMapperExpression; +import org.elasticsearch.client.security.support.expressiondsl.fields.FieldRoleMapperExpression; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.DeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Collections; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.iterableWithSize; + +public class RoleMapperExpressionParserTests extends ESTestCase { + + public void testParseSimpleFieldExpression() throws Exception { + String json = "{ \"field\": { \"username\" : [\"*@shield.gov\"] } }"; + FieldRoleMapperExpression field = checkExpressionType(parse(json), FieldRoleMapperExpression.class); + assertThat(field.getField(), equalTo("username")); + assertThat(field.getValues(), iterableWithSize(1)); + assertThat(field.getValues().get(0), equalTo("*@shield.gov")); + + assertThat(toJson(field), equalTo(json.replaceAll("\\s", ""))); + } + + public void testParseComplexExpression() throws Exception { + String json = "{ \"any\": [" + + " { \"field\": { \"username\" : \"*@shield.gov\" } }, " + + " { \"all\": [" + + " { \"field\": { \"username\" : \"/.*\\\\@avengers\\\\.(net|org)/\" } }, " + + " { \"field\": { \"groups\" : [ \"admin\", \"operators\" ] } }, " + + " { \"except\":" + + " { \"field\": { \"groups\" : \"disavowed\" } }" + + " }" + + " ] }" + + "] }"; + final RoleMapperExpression expr = parse(json); + + assertThat(expr, instanceOf(CompositeRoleMapperExpression.class)); + CompositeRoleMapperExpression any = (CompositeRoleMapperExpression) expr; + + assertThat(any.getElements(), iterableWithSize(2)); + + final FieldRoleMapperExpression fieldShield = checkExpressionType(any.getElements().get(0), + FieldRoleMapperExpression.class); + assertThat(fieldShield.getField(), equalTo("username")); + assertThat(fieldShield.getValues(), iterableWithSize(1)); + assertThat(fieldShield.getValues().get(0), equalTo("*@shield.gov")); + + final CompositeRoleMapperExpression all = checkExpressionType(any.getElements().get(1), + CompositeRoleMapperExpression.class); + assertThat(all.getElements(), iterableWithSize(3)); + + final FieldRoleMapperExpression fieldAvengers = checkExpressionType(all.getElements().get(0), + FieldRoleMapperExpression.class); + assertThat(fieldAvengers.getField(), equalTo("username")); + assertThat(fieldAvengers.getValues(), iterableWithSize(1)); + assertThat(fieldAvengers.getValues().get(0), equalTo("/.*\\@avengers\\.(net|org)/")); + + final FieldRoleMapperExpression fieldGroupsAdmin = checkExpressionType(all.getElements().get(1), + FieldRoleMapperExpression.class); + assertThat(fieldGroupsAdmin.getField(), equalTo("groups")); + assertThat(fieldGroupsAdmin.getValues(), iterableWithSize(2)); + assertThat(fieldGroupsAdmin.getValues().get(0), equalTo("admin")); + assertThat(fieldGroupsAdmin.getValues().get(1), equalTo("operators")); + + final CompositeRoleMapperExpression except = checkExpressionType(all.getElements().get(2), + CompositeRoleMapperExpression.class); + final FieldRoleMapperExpression fieldDisavowed = checkExpressionType(except.getElements().get(0), + FieldRoleMapperExpression.class); + assertThat(fieldDisavowed.getField(), equalTo("groups")); + assertThat(fieldDisavowed.getValues(), iterableWithSize(1)); + assertThat(fieldDisavowed.getValues().get(0), equalTo("disavowed")); + + } + + private String toJson(final RoleMapperExpression expr) throws IOException { + final XContentBuilder builder = XContentFactory.jsonBuilder(); + expr.toXContent(builder, ToXContent.EMPTY_PARAMS); + final String output = Strings.toString(builder); + return output; + } + + private T checkExpressionType(RoleMapperExpression expr, Class type) { + assertThat(expr, instanceOf(type)); + return type.cast(expr); + } + + private RoleMapperExpression parse(String json) throws IOException { + return new RoleMapperExpressionParser().parse("rules", XContentType.JSON.xContent().createParser(new NamedXContentRegistry( + Collections.emptyList()), new DeprecationHandler() { + @Override + public void usedDeprecatedName(String usedName, String modernName) { + } + + @Override + public void usedDeprecatedField(String usedName, String replacedWith) { + } + }, json)); + } + +}