[Security] Correct DN matches in role-mapping rules (elastic/x-pack-elasticsearch#3704)
Changes the behaviour of the role mapping API to perform a "DistinguishedNameMatch" when the field is a DN. This is achieved by moving the responsibility for defining the matching rules from the expression to the data (ExpressionModel) Because the role mapping API is used within the SAML realm, which may or may not be using DNs, this implementation assumes that the "dn" and "groups" should be compared as DNs if they parse as a DN. For SAML this behaviour will generally do the right thing, as members of the "groups" field might be DNs (if the data is sourced from an LDAP directory) but often will not be. Original commit: elastic/x-pack-elasticsearch@3a4dfbba79
This commit is contained in:
parent
4271fd7cc3
commit
da7560a079
|
@ -7,6 +7,7 @@ package org.elasticsearch.xpack.core.security.authc.support.mapper;
|
|||
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.ParsingException;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
|
@ -99,7 +100,8 @@ 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(Map) matches} a
|
||||
* If the expression
|
||||
* {@link RoleMapperExpression#match(org.elasticsearch.xpack.security.authc.support.mapper.expressiondsl.ExpressionModel) matches} a
|
||||
* org.elasticsearch.xpack.security.authc.support.UserRoleMapper.UserData user, then the user should be assigned this mapping's
|
||||
* {@link #getRoles() roles}
|
||||
*/
|
||||
|
@ -133,7 +135,7 @@ public class ExpressionRoleMapping implements ToXContentObject, Writeable {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + "<" + name + " ; " + roles + " = " + expression + ">";
|
||||
return getClass().getSimpleName() + "<" + name + " ; " + roles + " = " + Strings.toString(expression) + ">";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,7 +8,6 @@ package org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl
|
|||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
|
@ -45,8 +44,8 @@ public final class AllExpression implements RoleMapperExpression {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean match(Map<String, Object> object) {
|
||||
return elements.stream().allMatch(RoleMapperExpression.predicate(object));
|
||||
public boolean match(ExpressionModel model) {
|
||||
return elements.stream().allMatch(RoleMapperExpression.predicate(model));
|
||||
}
|
||||
|
||||
public List<RoleMapperExpression> getElements() {
|
||||
|
|
|
@ -8,7 +8,6 @@ package org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl
|
|||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
|
@ -45,8 +44,8 @@ public final class AnyExpression implements RoleMapperExpression {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean match(Map<String, Object> object) {
|
||||
return elements.stream().anyMatch(RoleMapperExpression.predicate(object));
|
||||
public boolean match(ExpressionModel model) {
|
||||
return elements.stream().anyMatch(RoleMapperExpression.predicate(model));
|
||||
}
|
||||
|
||||
public List<RoleMapperExpression> getElements() {
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
package org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
|
@ -44,8 +43,8 @@ public final class ExceptExpression implements RoleMapperExpression {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean match(Map<String, Object> object) {
|
||||
return !expression.match(object);
|
||||
public boolean match(ExpressionModel model) {
|
||||
return !expression.match(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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.expressiondsl;
|
||||
|
||||
import org.elasticsearch.common.Numbers;
|
||||
import org.elasticsearch.common.collect.Tuple;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Represents the "model" object to be evaluated within a {@link RoleMapperExpression}.
|
||||
* The model is a flat object, where fields are defined by strings and value is either a
|
||||
* string, boolean, or number, or a collection of the above.
|
||||
*/
|
||||
public class ExpressionModel {
|
||||
|
||||
public static final Predicate<FieldExpression.FieldValue> NULL_PREDICATE = field -> field.getValue() == null;
|
||||
private Map<String, Tuple<Object, Predicate<FieldExpression.FieldValue>>> fields;
|
||||
|
||||
public ExpressionModel() {
|
||||
this.fields = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a field using a predicate that corresponds to the type of {@code value}
|
||||
*
|
||||
* @see #buildPredicate(Object)
|
||||
*/
|
||||
public ExpressionModel defineField(String name, Object value) {
|
||||
return defineField(name, value, buildPredicate(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a field using a supplied predicate.
|
||||
*/
|
||||
public ExpressionModel defineField(String name, Object value, Predicate<FieldExpression.FieldValue> predicate) {
|
||||
this.fields.put(name, new Tuple<>(value, predicate));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the named field, matches <em>any</em> of the provided values.
|
||||
*/
|
||||
public boolean test(String field, List<FieldExpression.FieldValue> values) {
|
||||
final Tuple<Object, Predicate<FieldExpression.FieldValue>> tuple = this.fields.get(field);
|
||||
final Predicate<FieldExpression.FieldValue> predicate;
|
||||
if (tuple == null) {
|
||||
predicate = NULL_PREDICATE;
|
||||
} else {
|
||||
predicate = tuple.v2();
|
||||
}
|
||||
return values.stream().anyMatch(predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@link Predicate} that matches correctly based on the type of the provided parameter.
|
||||
*/
|
||||
static Predicate<FieldExpression.FieldValue> buildPredicate(Object object) {
|
||||
if (object == null) {
|
||||
return NULL_PREDICATE;
|
||||
}
|
||||
if (object instanceof Boolean) {
|
||||
return field -> object.equals(field.getValue());
|
||||
}
|
||||
if (object instanceof Number) {
|
||||
return field -> numberEquals((Number) object, field.getValue());
|
||||
}
|
||||
if (object instanceof String) {
|
||||
return field -> field.getAutomaton() == null ? object.equals(field.getValue()) : field.getAutomaton().run((String) object);
|
||||
}
|
||||
if (object instanceof Collection) {
|
||||
return ((Collection<?>) object).stream()
|
||||
.map(element -> buildPredicate(element))
|
||||
.reduce((a, b) -> a.or(b))
|
||||
.orElse(fieldValue -> false);
|
||||
}
|
||||
throw new IllegalArgumentException("Unsupported value type " + object.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison of {@link Number} objects that compares by floating point when either value is a {@link Float} or {@link Double}
|
||||
* otherwise compares by {@link Numbers#toLongExact long}.
|
||||
*/
|
||||
private static boolean numberEquals(Number left, Object other) {
|
||||
if (left.equals(other)) {
|
||||
return true;
|
||||
}
|
||||
if ((other instanceof Number) == false) {
|
||||
return false;
|
||||
}
|
||||
Number right = (Number) other;
|
||||
if (left instanceof Double || left instanceof Float
|
||||
|| right instanceof Double || right instanceof Float) {
|
||||
return Double.compare(left.doubleValue(), right.doubleValue()) == 0;
|
||||
}
|
||||
return Numbers.toLongExact(left) == Numbers.toLongExact(right);
|
||||
}
|
||||
|
||||
}
|
|
@ -114,7 +114,7 @@ public final class ExpressionParser {
|
|||
private RoleMapperExpression parseFieldExpression(XContentParser parser) throws IOException {
|
||||
checkStartObject(parser);
|
||||
final String fieldName = readFieldName(Fields.FIELD.getPreferredName(), parser);
|
||||
final List<FieldExpression.FieldPredicate> values;
|
||||
final List<FieldExpression.FieldValue> values;
|
||||
if (parser.nextToken() == XContentParser.Token.START_ARRAY) {
|
||||
values = parseArray(Fields.FIELD, parser, this::parseFieldValue);
|
||||
} else {
|
||||
|
@ -166,19 +166,19 @@ public final class ExpressionParser {
|
|||
}
|
||||
}
|
||||
|
||||
private FieldExpression.FieldPredicate parseFieldValue(XContentParser parser) throws IOException {
|
||||
private FieldExpression.FieldValue parseFieldValue(XContentParser parser) throws IOException {
|
||||
switch (parser.currentToken()) {
|
||||
case VALUE_STRING:
|
||||
return FieldExpression.FieldPredicate.create(parser.text());
|
||||
return new FieldExpression.FieldValue(parser.text());
|
||||
|
||||
case VALUE_BOOLEAN:
|
||||
return FieldExpression.FieldPredicate.create(parser.booleanValue());
|
||||
return new FieldExpression.FieldValue(parser.booleanValue());
|
||||
|
||||
case VALUE_NUMBER:
|
||||
return FieldExpression.FieldPredicate.create(parser.longValue());
|
||||
return new FieldExpression.FieldValue(parser.longValue());
|
||||
|
||||
case VALUE_NULL:
|
||||
return FieldExpression.FieldPredicate.create(null);
|
||||
return new FieldExpression.FieldValue(null);
|
||||
|
||||
default:
|
||||
throw new ElasticsearchParseException("failed to parse rules expression. expected a field value but found [{}] instead",
|
||||
|
|
|
@ -5,21 +5,19 @@
|
|||
*/
|
||||
package org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl;
|
||||
|
||||
import org.elasticsearch.common.Numbers;
|
||||
import org.apache.lucene.util.automaton.CharacterRunAutomaton;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
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.regex.Regex;
|
||||
import org.elasticsearch.common.xcontent.ToXContent;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.xpack.core.security.support.Automatons;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* An expression that evaluates to <code>true</code> if a field (map element) matches
|
||||
|
@ -31,9 +29,9 @@ public final class FieldExpression implements RoleMapperExpression {
|
|||
public static final String NAME = "field";
|
||||
|
||||
private final String field;
|
||||
private final List<FieldPredicate> values;
|
||||
private final List<FieldValue> values;
|
||||
|
||||
public FieldExpression(String field, List<FieldPredicate> values) {
|
||||
public FieldExpression(String field, List<FieldValue> values) {
|
||||
if (field == null || field.isEmpty()) {
|
||||
throw new IllegalArgumentException("null or empty field name (" + field + ")");
|
||||
}
|
||||
|
@ -45,7 +43,7 @@ public final class FieldExpression implements RoleMapperExpression {
|
|||
}
|
||||
|
||||
public FieldExpression(StreamInput in) throws IOException {
|
||||
this(in.readString(), in.readList(FieldPredicate::readFrom));
|
||||
this(in.readString(), in.readList(FieldValue::readFrom));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -60,24 +58,15 @@ public final class FieldExpression implements RoleMapperExpression {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean match(Map<String, Object> object) {
|
||||
final Object fieldValue = object.get(field);
|
||||
if (fieldValue instanceof Collection) {
|
||||
return ((Collection) fieldValue).stream().anyMatch(this::matchValue);
|
||||
} else {
|
||||
return matchValue(fieldValue);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matchValue(Object fieldValue) {
|
||||
return values.stream().anyMatch(predicate -> predicate.test(fieldValue));
|
||||
public boolean match(ExpressionModel model) {
|
||||
return model.test(field, values);
|
||||
}
|
||||
|
||||
public String getField() {
|
||||
return field;
|
||||
}
|
||||
|
||||
public List<Predicate<Object>> getValues() {
|
||||
public List<FieldValue> getValues() {
|
||||
return Collections.unmodifiableList(values);
|
||||
}
|
||||
|
||||
|
@ -107,7 +96,7 @@ public final class FieldExpression implements RoleMapperExpression {
|
|||
values.get(0).toXContent(builder, params);
|
||||
} else {
|
||||
builder.startArray(this.field);
|
||||
for (FieldPredicate fp : values) {
|
||||
for (FieldValue fp : values) {
|
||||
fp.toXContent(builder, params);
|
||||
}
|
||||
builder.endArray();
|
||||
|
@ -116,60 +105,40 @@ public final class FieldExpression implements RoleMapperExpression {
|
|||
return builder.endObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* A special predicate for matching values in a {@link FieldExpression}. This interface
|
||||
* exists to support the serialisation ({@link ToXContent}, {@link Writeable}) of <em>field</em>
|
||||
* expressions.
|
||||
*/
|
||||
public static class FieldPredicate implements Predicate<Object>, ToXContent, Writeable {
|
||||
public static class FieldValue implements ToXContent, Writeable {
|
||||
private final Object value;
|
||||
private final Predicate<Object> predicate;
|
||||
@Nullable
|
||||
private final CharacterRunAutomaton automaton;
|
||||
|
||||
private FieldPredicate(Object value, Predicate<Object> predicate) {
|
||||
public FieldValue(Object value) {
|
||||
this.value = value;
|
||||
this.predicate = predicate;
|
||||
this.automaton = buildAutomaton(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(Object o) {
|
||||
return this.predicate.test(o);
|
||||
private static CharacterRunAutomaton buildAutomaton(Object value) {
|
||||
if (value instanceof String) {
|
||||
final String str = (String) value;
|
||||
if (Regex.isSimpleMatchPattern(str) || isLuceneRegex(str)) {
|
||||
return new CharacterRunAutomaton(Automatons.patterns(str));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params)
|
||||
throws IOException {
|
||||
return builder.value(value);
|
||||
private static boolean isLuceneRegex(String str) {
|
||||
return str.length() > 1 && str.charAt(0) == '/' && str.charAt(str.length() - 1) == '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an appropriate predicate based on the type and value of the argument.
|
||||
* The predicate is formed according to the following rules:
|
||||
* <ul>
|
||||
* <li>If <code>value</code> is <code>null</code>, then the predicate evaluates to
|
||||
* <code>true</code> <em>if-and-only-if</em> the predicate-argument is
|
||||
* <code>null</code></li>
|
||||
* <li>If <code>value</code> is a {@link Boolean}, then the predicate
|
||||
* evaluates to <code>true</code> <em>if-and-only-if</em> the predicate-argument is
|
||||
* an {@link Boolean#equals(Object) equal} <code>Boolean</code></li>
|
||||
* <li>If <code>value</code> is a {@link Number}, then the predicate
|
||||
* evaluates to <code>true</code> <em>if-and-only-if</em> the predicate-argument is
|
||||
* numerically equal to <code>value</code>. This class makes a best-effort to determine
|
||||
* numeric equality across different implementations of <code>Number</code>, but the
|
||||
* implementation can only be guaranteed for standard integral representations (
|
||||
* <code>Long</code>, <code>Integer</code>, etc)</li>
|
||||
* <li>If <code>value</code> is a {@link String}, then it is treated as a
|
||||
* {@link org.apache.lucene.util.automaton.Automaton Lucene automaton} pattern with
|
||||
* {@link Automatons#predicate(String...) corresponding predicate}.
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
public static FieldPredicate create(Object value) {
|
||||
Predicate<Object> predicate = buildPredicate(value);
|
||||
return new FieldPredicate(value, predicate);
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static FieldPredicate readFrom(StreamInput in) throws IOException {
|
||||
return create(in.readGenericValue());
|
||||
public CharacterRunAutomaton getAutomaton() {
|
||||
return automaton;
|
||||
}
|
||||
|
||||
public static FieldValue readFrom(StreamInput in) throws IOException {
|
||||
return new FieldValue(in.readGenericValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -177,41 +146,9 @@ public final class FieldExpression implements RoleMapperExpression {
|
|||
out.writeGenericValue(value);
|
||||
}
|
||||
|
||||
private static Predicate<Object> buildPredicate(Object object) {
|
||||
if (object == null) {
|
||||
return Objects::isNull;
|
||||
}
|
||||
if (object instanceof Boolean) {
|
||||
return object::equals;
|
||||
}
|
||||
if (object instanceof Number) {
|
||||
return (other) -> numberEquals((Number) object, other);
|
||||
}
|
||||
if (object instanceof String) {
|
||||
final String str = (String) object;
|
||||
if (str.isEmpty()) {
|
||||
return obj -> String.valueOf(obj).isEmpty();
|
||||
} else {
|
||||
final Predicate<String> predicate = Automatons.predicate(str);
|
||||
return obj -> predicate.test(String.valueOf(obj));
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unsupported value type " + object.getClass());
|
||||
}
|
||||
|
||||
private static boolean numberEquals(Number left, Object other) {
|
||||
if (left.equals(other)) {
|
||||
return true;
|
||||
}
|
||||
if ((other instanceof Number) == false) {
|
||||
return false;
|
||||
}
|
||||
Number right = (Number) other;
|
||||
if (left instanceof Double || left instanceof Float
|
||||
|| right instanceof Double || right instanceof Float) {
|
||||
return Double.compare(left.doubleValue(), right.doubleValue()) == 0;
|
||||
}
|
||||
return Numbers.toLongExact(left) == Numbers.toLongExact(right);
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
return builder.value(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,27 +5,27 @@
|
|||
*/
|
||||
package org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.elasticsearch.common.io.stream.NamedWriteable;
|
||||
import org.elasticsearch.common.xcontent.ToXContentObject;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Implementations of this interface represent an expression over a simple object that resolves to
|
||||
* a boolean value. The "simple object" is implemented as a (flattened) {@link Map}.
|
||||
* a boolean value. The "simple object" is provided as a {@link ExpressionModel}.
|
||||
*/
|
||||
public interface RoleMapperExpression extends ToXContentObject, NamedWriteable {
|
||||
|
||||
/**
|
||||
* Determines whether this expression matches against the provided object.
|
||||
* @param model
|
||||
*/
|
||||
boolean match(Map<String, Object> object);
|
||||
boolean match(ExpressionModel model);
|
||||
|
||||
/**
|
||||
* Adapt this expression to a standard {@link Predicate}
|
||||
*/
|
||||
default Predicate<Map<String, Object>> asPredicate() {
|
||||
default Predicate<ExpressionModel> asPredicate() {
|
||||
return this::match;
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ public interface RoleMapperExpression extends ToXContentObject, NamedWriteable {
|
|||
* a fixed object. Its purpose is for cases where there is a {@link java.util.stream.Stream} of
|
||||
* expressions, that need to be filtered against a single map.
|
||||
*/
|
||||
static Predicate<RoleMapperExpression> predicate(Map<String, Object> map) {
|
||||
static Predicate<RoleMapperExpression> predicate(ExpressionModel map) {
|
||||
return expr -> expr.match(map);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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.expressiondsl;
|
||||
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression.FieldValue;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
public class ExpressionModelPredicateTests extends ESTestCase {
|
||||
|
||||
public void testNullValue() throws Exception {
|
||||
final Predicate<FieldValue> predicate = ExpressionModel.buildPredicate(null);
|
||||
assertThat(predicate.test(new FieldValue(null)), is(true));
|
||||
assertThat(predicate.test(new FieldValue("")), is(false));
|
||||
assertThat(predicate.test(new FieldValue(1)), is(false));
|
||||
assertThat(predicate.test(new FieldValue(true)), is(false));
|
||||
}
|
||||
|
||||
public void testBooleanValue() throws Exception {
|
||||
final boolean matchValue = randomBoolean();
|
||||
final Predicate<FieldValue> predicate = ExpressionModel.buildPredicate(matchValue);
|
||||
assertThat(predicate.test(new FieldValue(matchValue)), is(true));
|
||||
Object value = !matchValue;
|
||||
assertThat(predicate.test(new FieldValue(value)), is(false));
|
||||
assertThat(predicate.test(new FieldValue(String.valueOf(matchValue))), is(false));
|
||||
assertThat(predicate.test(new FieldValue("")), is(false));
|
||||
assertThat(predicate.test(new FieldValue(1)), is(false));
|
||||
assertThat(predicate.test(new FieldValue(null)), is(false));
|
||||
}
|
||||
|
||||
public void testLongValue() throws Exception {
|
||||
final int intValue = randomInt();
|
||||
final long longValue = intValue;
|
||||
final Predicate<FieldValue> predicate = ExpressionModel.buildPredicate(longValue);
|
||||
|
||||
assertThat(predicate.test(new FieldValue(longValue)), is(true));
|
||||
assertThat(predicate.test(new FieldValue(intValue)), is(true));
|
||||
assertThat(predicate.test(new FieldValue(new BigInteger(String.valueOf(longValue)))), is(true));
|
||||
|
||||
assertThat(predicate.test(new FieldValue(longValue - 1)), is(false));
|
||||
assertThat(predicate.test(new FieldValue(intValue + 1)), is(false));
|
||||
assertThat(predicate.test(new FieldValue(String.valueOf(longValue))), is(false));
|
||||
assertThat(predicate.test(new FieldValue("")), is(false));
|
||||
assertThat(predicate.test(new FieldValue(true)), is(false));
|
||||
assertThat(predicate.test(new FieldValue(null)), is(false));
|
||||
}
|
||||
|
||||
public void testSimpleAutomatonValue() throws Exception {
|
||||
final String prefix = randomAlphaOfLength(3);
|
||||
FieldValue fieldValue = new FieldValue(prefix + "*");
|
||||
|
||||
assertThat(ExpressionModel.buildPredicate(prefix).test(fieldValue), is(true));
|
||||
assertThat(ExpressionModel.buildPredicate(prefix + randomAlphaOfLengthBetween(1, 5)).test(fieldValue), is(true));
|
||||
|
||||
assertThat(ExpressionModel.buildPredicate("_" + prefix).test(fieldValue), is(false));
|
||||
assertThat(ExpressionModel.buildPredicate(prefix.substring(0, 1)).test(fieldValue), is(false));
|
||||
|
||||
assertThat(ExpressionModel.buildPredicate("").test(fieldValue), is(false));
|
||||
assertThat(ExpressionModel.buildPredicate(1).test(fieldValue), is(false));
|
||||
assertThat(ExpressionModel.buildPredicate(true).test(fieldValue), is(false));
|
||||
assertThat(ExpressionModel.buildPredicate(null).test(fieldValue), is(false));
|
||||
}
|
||||
|
||||
public void testEmptyStringValue() throws Exception {
|
||||
final Predicate<FieldValue> predicate = ExpressionModel.buildPredicate("");
|
||||
|
||||
assertThat(predicate.test(new FieldValue("")), is(true));
|
||||
|
||||
assertThat(predicate.test(new FieldValue(randomAlphaOfLengthBetween(1, 3))), is(false));
|
||||
assertThat(predicate.test(new FieldValue(1)), is(false));
|
||||
assertThat(predicate.test(new FieldValue(true)), is(false));
|
||||
assertThat(predicate.test(new FieldValue(null)), is(false));
|
||||
}
|
||||
|
||||
public void testRegexAutomatonValue() throws Exception {
|
||||
final String substring = randomAlphaOfLength(5);
|
||||
final FieldValue fieldValue = new FieldValue("/.*" + substring + ".*/");
|
||||
|
||||
assertThat(ExpressionModel.buildPredicate(substring).test(fieldValue), is(true));
|
||||
assertThat(ExpressionModel.buildPredicate(randomAlphaOfLengthBetween(2, 4) + substring + randomAlphaOfLengthBetween(1, 5))
|
||||
.test(fieldValue), is(true));
|
||||
|
||||
assertThat(ExpressionModel.buildPredicate(substring.substring(1, 3)).test(fieldValue), is(false));
|
||||
|
||||
assertThat(ExpressionModel.buildPredicate("").test(fieldValue), is(false));
|
||||
assertThat(ExpressionModel.buildPredicate(1).test(fieldValue), is(false));
|
||||
assertThat(ExpressionModel.buildPredicate(true).test(fieldValue), is(false));
|
||||
assertThat(ExpressionModel.buildPredicate(null).test(fieldValue), is(false));
|
||||
}
|
||||
|
||||
}
|
|
@ -17,18 +17,17 @@ import org.elasticsearch.common.xcontent.XContentFactory;
|
|||
import org.elasticsearch.common.xcontent.XContentType;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.xpack.core.XPackClientPlugin;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression.FieldValue;
|
||||
import org.elasticsearch.xpack.core.watcher.support.xcontent.XContentSource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
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 ExpressionParserTests extends ESTestCase {
|
||||
|
||||
|
@ -37,9 +36,11 @@ public class ExpressionParserTests extends ESTestCase {
|
|||
FieldExpression field = checkExpressionType(parse(json), FieldExpression.class);
|
||||
assertThat(field.getField(), equalTo("username"));
|
||||
assertThat(field.getValues(), iterableWithSize(1));
|
||||
final Predicate<Object> predicate = field.getValues().get(0);
|
||||
assertThat(predicate.test("bob@shield.gov"), equalTo(true));
|
||||
assertThat(predicate.test("bob@example.net"), equalTo(false));
|
||||
final FieldValue value = field.getValues().get(0);
|
||||
assertThat(value.getValue(), equalTo("*@shield.gov"));
|
||||
assertThat(value.getAutomaton(), notNullValue());
|
||||
assertThat(value.getAutomaton().run("bob@shield.gov"), equalTo(true));
|
||||
assertThat(value.getAutomaton().run("bob@example.net"), equalTo(false));
|
||||
assertThat(json(field), equalTo(json.replaceAll("\\s", "")));
|
||||
}
|
||||
|
||||
|
@ -65,9 +66,11 @@ public class ExpressionParserTests extends ESTestCase {
|
|||
FieldExpression.class);
|
||||
assertThat(fieldShield.getField(), equalTo("username"));
|
||||
assertThat(fieldShield.getValues(), iterableWithSize(1));
|
||||
final Predicate<Object> predicateShield = fieldShield.getValues().get(0);
|
||||
assertThat(predicateShield.test("fury@shield.gov"), equalTo(true));
|
||||
assertThat(predicateShield.test("fury@shield.net"), equalTo(false));
|
||||
final FieldValue valueShield = fieldShield.getValues().get(0);
|
||||
assertThat(valueShield.getValue(), equalTo("*@shield.gov"));
|
||||
assertThat(valueShield.getAutomaton(), notNullValue());
|
||||
assertThat(valueShield.getAutomaton().run("fury@shield.gov"), equalTo(true));
|
||||
assertThat(valueShield.getAutomaton().run("fury@shield.net"), equalTo(false));
|
||||
|
||||
final AllExpression all = checkExpressionType(any.getElements().get(1),
|
||||
AllExpression.class);
|
||||
|
@ -77,19 +80,17 @@ public class ExpressionParserTests extends ESTestCase {
|
|||
FieldExpression.class);
|
||||
assertThat(fieldAvengers.getField(), equalTo("username"));
|
||||
assertThat(fieldAvengers.getValues(), iterableWithSize(1));
|
||||
final Predicate<Object> predicateAvengers = fieldAvengers.getValues().get(0);
|
||||
assertThat(predicateAvengers.test("stark@avengers.net"), equalTo(true));
|
||||
assertThat(predicateAvengers.test("romanov@avengers.org"), equalTo(true));
|
||||
assertThat(predicateAvengers.test("fury@shield.gov"), equalTo(false));
|
||||
final FieldValue valueAvengers = fieldAvengers.getValues().get(0);
|
||||
assertThat(valueAvengers.getAutomaton().run("stark@avengers.net"), equalTo(true));
|
||||
assertThat(valueAvengers.getAutomaton().run("romanov@avengers.org"), equalTo(true));
|
||||
assertThat(valueAvengers.getAutomaton().run("fury@shield.gov"), equalTo(false));
|
||||
|
||||
final FieldExpression fieldGroupsAdmin = checkExpressionType(all.getElements().get(1),
|
||||
FieldExpression.class);
|
||||
assertThat(fieldGroupsAdmin.getField(), equalTo("groups"));
|
||||
assertThat(fieldGroupsAdmin.getValues(), iterableWithSize(2));
|
||||
assertThat(fieldGroupsAdmin.getValues().get(0).test("admin"), equalTo(true));
|
||||
assertThat(fieldGroupsAdmin.getValues().get(0).test("foo"), equalTo(false));
|
||||
assertThat(fieldGroupsAdmin.getValues().get(1).test("operators"), equalTo(true));
|
||||
assertThat(fieldGroupsAdmin.getValues().get(1).test("foo"), equalTo(false));
|
||||
assertThat(fieldGroupsAdmin.getValues().get(0).getValue(), equalTo("admin"));
|
||||
assertThat(fieldGroupsAdmin.getValues().get(1).getValue(), equalTo("operators"));
|
||||
|
||||
final ExceptExpression except = checkExpressionType(all.getElements().get(2),
|
||||
ExceptExpression.class);
|
||||
|
@ -97,26 +98,25 @@ public class ExpressionParserTests extends ESTestCase {
|
|||
FieldExpression.class);
|
||||
assertThat(fieldDisavowed.getField(), equalTo("groups"));
|
||||
assertThat(fieldDisavowed.getValues(), iterableWithSize(1));
|
||||
assertThat(fieldDisavowed.getValues().get(0).test("disavowed"), equalTo(true));
|
||||
assertThat(fieldDisavowed.getValues().get(0).test("_disavowed_"), equalTo(false));
|
||||
assertThat(fieldDisavowed.getValues().get(0).getValue(), equalTo("disavowed"));
|
||||
|
||||
Map<String, Object> hawkeye = new HashMap<>();
|
||||
hawkeye.put("username", "hawkeye@avengers.org");
|
||||
hawkeye.put("groups", Arrays.asList("operators"));
|
||||
ExpressionModel hawkeye = new ExpressionModel();
|
||||
hawkeye.defineField("username", "hawkeye@avengers.org");
|
||||
hawkeye.defineField("groups", Arrays.asList("operators"));
|
||||
assertThat(expr.match(hawkeye), equalTo(true));
|
||||
|
||||
Map<String, Object> captain = new HashMap<>();
|
||||
captain.put("username", "america@avengers.net");
|
||||
ExpressionModel captain = new ExpressionModel();
|
||||
captain.defineField("username", "america@avengers.net");
|
||||
assertThat(expr.match(captain), equalTo(false));
|
||||
|
||||
Map<String, Object> warmachine = new HashMap<>();
|
||||
warmachine.put("username", "warmachine@avengers.net");
|
||||
warmachine.put("groups", Arrays.asList("admin", "disavowed"));
|
||||
ExpressionModel warmachine = new ExpressionModel();
|
||||
warmachine.defineField("username", "warmachine@avengers.net");
|
||||
warmachine.defineField("groups", Arrays.asList("admin", "disavowed"));
|
||||
assertThat(expr.match(warmachine), equalTo(false));
|
||||
|
||||
Map<String, Object> fury = new HashMap<>();
|
||||
fury.put("username", "fury@shield.gov");
|
||||
fury.put("groups", Arrays.asList("classified", "directors"));
|
||||
ExpressionModel fury = new ExpressionModel();
|
||||
fury.defineField("username", "fury@shield.gov");
|
||||
fury.defineField("groups", Arrays.asList("classified", "directors"));
|
||||
assertThat(expr.asPredicate().test(fury), equalTo(true));
|
||||
|
||||
assertThat(json(expr), equalTo(json.replaceAll("\\s", "")));
|
||||
|
@ -161,4 +161,4 @@ public class ExpressionParserTests extends ESTestCase {
|
|||
}
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,17 +5,23 @@
|
|||
*/
|
||||
package org.elasticsearch.xpack.security.authc.support;
|
||||
|
||||
import com.unboundid.ldap.sdk.DN;
|
||||
import com.unboundid.ldap.sdk.LDAPException;
|
||||
import org.apache.lucene.util.automaton.CharacterRunAutomaton;
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.ExpressionModel;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression;
|
||||
import org.elasticsearch.xpack.core.security.authz.permission.Role;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Where a realm users an authentication method that does not have in-built support for X-Pack
|
||||
|
@ -60,20 +66,24 @@ public interface UserRoleMapper {
|
|||
}
|
||||
|
||||
/**
|
||||
* Formats the user data as a <code>Map</code>.
|
||||
* The map is <em>not</em> nested - all values are simple Java values, but keys may
|
||||
* Formats the user data as a {@link ExpressionModel}.
|
||||
* The model does <em>not</em> have nested values - all values are simple Java values, but keys may
|
||||
* contain <code>.</code>.
|
||||
* For example, the {@link #metadata} values will be stored in the map with a key of
|
||||
* For example, the {@link #metadata} values will be stored in the model with a key of
|
||||
* <code>"metadata.KEY"</code> where <code>KEY</code> is the key from the metadata object.
|
||||
*/
|
||||
public Map<String, Object> asMap() {
|
||||
final Map<String, Object> map = new HashMap<>();
|
||||
map.put("username", username);
|
||||
map.put("dn", dn);
|
||||
map.put("groups", groups);
|
||||
metadata.keySet().forEach(k -> map.put("metadata." + k, metadata.get(k)));
|
||||
map.put("realm.name", realm.name());
|
||||
return map;
|
||||
public ExpressionModel asModel() {
|
||||
final ExpressionModel model = new ExpressionModel();
|
||||
model.defineField("username", username);
|
||||
model.defineField("dn", dn, new DistinguishedNamePredicate(dn));
|
||||
model.defineField("groups", groups, groups.stream()
|
||||
.<Predicate<FieldExpression.FieldValue>>map(DistinguishedNamePredicate::new)
|
||||
.reduce((a, b) -> a.or(b))
|
||||
.orElse(fieldValue -> false)
|
||||
);
|
||||
metadata.keySet().forEach(k -> model.defineField("metadata." + k, metadata.get(k)));
|
||||
model.defineField("realm.name", realm.name());
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -126,4 +136,98 @@ public interface UserRoleMapper {
|
|||
return realm;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A specialised predicate for fields that might be a DistinguishedName (e.g "dn" or "groups").
|
||||
*
|
||||
* The X500 specs define how to compare DistinguishedNames (but we mostly rely on {@link DN#equals(Object)}),
|
||||
* which means "CN=me,DC=example,DC=com" should be equal to "cn=me, dc=Example, dc=COM" (and other variations).
|
||||
|
||||
* The {@link FieldExpression} class doesn't know about special rules for special data types, but the
|
||||
* {@link ExpressionModel} class can take a custom {@code Predicate} that tests whether the data in the model
|
||||
* matches the {@link FieldExpression.FieldValue value} in the expression.
|
||||
*
|
||||
* The string constructor parameter may or may not actaully parse as a DN - the "dn" field <em>should</em>
|
||||
* always be a DN, however groups will be a DN if they're from an LDAP/AD realm, but often won't be for a SAML realm.
|
||||
*
|
||||
* Because the {@link FieldExpression.FieldValue} might be a pattern ({@link CharacterRunAutomaton automaton}),
|
||||
* we sometimes need to do more complex matching than just comparing a DN for equality.
|
||||
*
|
||||
*/
|
||||
class DistinguishedNamePredicate implements Predicate<FieldExpression.FieldValue> {
|
||||
private final String string;
|
||||
private final DN dn;
|
||||
|
||||
public DistinguishedNamePredicate(String string) {
|
||||
this.string = string;
|
||||
this.dn = parseDn(string);
|
||||
}
|
||||
|
||||
private static DN parseDn(String string) {
|
||||
try {
|
||||
return new DN(string);
|
||||
} catch (LDAPException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return string;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(FieldExpression.FieldValue fieldValue) {
|
||||
final CharacterRunAutomaton automaton = fieldValue.getAutomaton();
|
||||
if (automaton != null) {
|
||||
if (automaton.run(string)) {
|
||||
return true;
|
||||
}
|
||||
if (dn != null && automaton.run(dn.toNormalizedString())) {
|
||||
return true;
|
||||
}
|
||||
if (automaton.run(string.toLowerCase(Locale.ROOT)) || automaton.run(string.toUpperCase(Locale.ROOT))) {
|
||||
return true;
|
||||
}
|
||||
if (dn == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
assert fieldValue.getValue() instanceof String : "FieldValue " + fieldValue + " has automaton but value is "
|
||||
+ (fieldValue.getValue() == null ? "<null>" : fieldValue.getValue().getClass());
|
||||
String pattern = (String) fieldValue.getValue();
|
||||
|
||||
// If the pattern is "*,dc=example,dc=com" then the rule is actually trying to express a DN sub-tree match.
|
||||
// We can use dn.isDescendantOf for that
|
||||
if (pattern.startsWith("*,")) {
|
||||
final String suffix = pattern.substring(2);
|
||||
// if the suffix has a wildcard, then it's not a pure sub-tree match
|
||||
if (suffix.indexOf('*') == -1) {
|
||||
final DN dnSuffix = parseDn(suffix);
|
||||
if (dnSuffix != null && dn.isDescendantOf(dnSuffix, false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
if (fieldValue.getValue() instanceof String) {
|
||||
final String testString = (String) fieldValue.getValue();
|
||||
if (testString.equalsIgnoreCase(string)) {
|
||||
return true;
|
||||
}
|
||||
if (dn == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final DN testDn = parseDn(testString);
|
||||
if (testDn != null) {
|
||||
return dn.equals(testDn);
|
||||
}
|
||||
return testString.equalsIgnoreCase(dn.toNormalizedString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheRespons
|
|||
import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappingRequest;
|
||||
import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingRequest;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.ExpressionRoleMapping;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.ExpressionModel;
|
||||
import org.elasticsearch.xpack.core.security.client.SecurityClient;
|
||||
import org.elasticsearch.xpack.security.SecurityLifecycleService;
|
||||
import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm;
|
||||
|
@ -327,10 +328,10 @@ public class NativeRoleMappingStore extends AbstractComponent implements UserRol
|
|||
public void resolveRoles(UserData user, ActionListener<Set<String>> listener) {
|
||||
getRoleMappings(null, ActionListener.wrap(
|
||||
mappings -> {
|
||||
final Map<String, Object> userDataMap = user.asMap();
|
||||
final ExpressionModel model = user.asModel();
|
||||
Stream<ExpressionRoleMapping> stream = mappings.stream()
|
||||
.filter(ExpressionRoleMapping::isEnabled)
|
||||
.filter(m -> m.getExpression().match(userDataMap));
|
||||
.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(),
|
||||
|
|
|
@ -63,7 +63,7 @@ public class TransportPutRoleMappingActionTests extends ESTestCase {
|
|||
public void testPutValidMapping() throws Exception {
|
||||
final FieldExpression expression = new FieldExpression(
|
||||
"username",
|
||||
Collections.singletonList(FieldExpression.FieldPredicate.create("*"))
|
||||
Collections.singletonList(new FieldExpression.FieldValue("*"))
|
||||
);
|
||||
final PutRoleMappingResponse response = put("anarchy", expression, "superuser",
|
||||
Collections.singletonMap("dumb", true));
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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.security.authc.support;
|
||||
|
||||
import com.unboundid.ldap.sdk.DN;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression.FieldValue;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
public class DistinguishedNamePredicateTests extends ESTestCase {
|
||||
|
||||
public void testMatching() throws Exception {
|
||||
String randomDn = "CN=" + randomAlphaOfLengthBetween(3, 12)
|
||||
+ ",OU=" + randomAlphaOfLength(4)
|
||||
+ ", O=" + randomAlphaOfLengthBetween(2, 6);
|
||||
|
||||
// Randomly enter the DN in mixed case, lower case or upper case;
|
||||
final String inputDn;
|
||||
if (randomBoolean()) {
|
||||
inputDn = randomBoolean() ? randomDn.toLowerCase(Locale.ENGLISH) : randomDn.toUpperCase(Locale.ENGLISH);
|
||||
} else {
|
||||
inputDn = randomDn;
|
||||
}
|
||||
final Predicate<FieldValue> predicate = new UserRoleMapper.DistinguishedNamePredicate(inputDn);
|
||||
|
||||
assertPredicate(predicate, randomDn, true);
|
||||
assertPredicate(predicate, randomDn.toLowerCase(Locale.ROOT), true);
|
||||
assertPredicate(predicate, randomDn.toUpperCase(Locale.ROOT), true);
|
||||
assertPredicate(predicate, "/" + inputDn + "/", true);
|
||||
assertPredicate(predicate, new DN(randomDn).toNormalizedString(), true);
|
||||
assertPredicate(predicate, "*," + new DN(randomDn).getParent().toNormalizedString(), true);
|
||||
assertPredicate(predicate, "*," + new DN(inputDn).getParent().getParent().toNormalizedString(), true);
|
||||
assertPredicate(predicate, randomDn.replaceFirst(".*,", "*,"), true);
|
||||
assertPredicate(predicate, randomDn.replaceFirst("[^,]*,", "*, "), true);
|
||||
|
||||
assertPredicate(predicate, randomDn + ",CN=AU", false);
|
||||
assertPredicate(predicate, "X" + randomDn, false);
|
||||
assertPredicate(predicate, "", false);
|
||||
assertPredicate(predicate, 1.23, false);
|
||||
assertPredicate(predicate, true, false);
|
||||
assertPredicate(predicate, null, false);
|
||||
}
|
||||
|
||||
private void assertPredicate(Predicate<FieldValue> predicate, Object value, boolean expected) {
|
||||
assertThat("Predicate [" + predicate + "] match [" + value + "]", predicate.test(new FieldValue(value)), equalTo(expected));
|
||||
}
|
||||
}
|
|
@ -22,9 +22,6 @@
|
|||
|
||||
package org.elasticsearch.xpack.security.authc.support.mapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.elasticsearch.common.ParsingException;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
|
@ -41,6 +38,10 @@ import org.hamcrest.Matchers;
|
|||
import org.junit.Before;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
@ -71,10 +72,19 @@ public class ExpressionRoleMappingTests extends ESTestCase {
|
|||
assertThat(mapping.getRoles(), Matchers.containsInAnyOrder("kibana_user", "sales"));
|
||||
assertThat(mapping.getExpression(), instanceOf(AllExpression.class));
|
||||
|
||||
final UserRoleMapper.UserData user1 = new UserRoleMapper.UserData(
|
||||
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
|
||||
);
|
||||
final UserRoleMapper.UserData user1b = new UserRoleMapper.UserData(
|
||||
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()
|
||||
);
|
||||
final UserRoleMapper.UserData user1d = new UserRoleMapper.UserData(
|
||||
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
|
||||
|
@ -85,9 +95,12 @@ public class ExpressionRoleMappingTests extends ESTestCase {
|
|||
Collections.emptyList(), Collections.singletonMap("active", true), realm
|
||||
);
|
||||
|
||||
assertThat(mapping.getExpression().match(user1.asMap()), equalTo(true));
|
||||
assertThat(mapping.getExpression().match(user2.asMap()), equalTo(false));
|
||||
assertThat(mapping.getExpression().match(user3.asMap()), equalTo(false));
|
||||
assertThat(mapping.getExpression().match(user1a.asModel()), equalTo(true));
|
||||
assertThat(mapping.getExpression().match(user1b.asModel()), equalTo(true));
|
||||
assertThat(mapping.getExpression().match(user1c.asModel()), equalTo(true));
|
||||
assertThat(mapping.getExpression().match(user1d.asModel()), equalTo(true));
|
||||
assertThat(mapping.getExpression().match(user2.asModel()), equalTo(false));
|
||||
assertThat(mapping.getExpression().match(user3.asModel()), equalTo(false));
|
||||
}
|
||||
|
||||
public void testParsingFailsIfRulesAreMissing() throws Exception {
|
||||
|
@ -143,4 +156,4 @@ public class ExpressionRoleMappingTests extends ESTestCase {
|
|||
return mapping;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,6 @@
|
|||
*/
|
||||
package org.elasticsearch.xpack.security.authc.support.mapper;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.support.PlainActionFuture;
|
||||
import org.elasticsearch.client.Client;
|
||||
|
@ -17,14 +12,21 @@ import org.elasticsearch.common.settings.Settings;
|
|||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.ExpressionRoleMapping;
|
||||
import org.elasticsearch.xpack.security.SecurityLifecycleService;
|
||||
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
|
||||
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.ExpressionRoleMapping;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression.FieldPredicate;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression.FieldValue;
|
||||
import org.elasticsearch.xpack.security.SecurityLifecycleService;
|
||||
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
|
||||
import org.hamcrest.Matchers;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
@ -33,25 +35,25 @@ public class NativeUserRoleMapperTests extends ESTestCase {
|
|||
public void testResolveRoles() throws Exception {
|
||||
// Does match DN
|
||||
final ExpressionRoleMapping mapping1 = new ExpressionRoleMapping("dept_h",
|
||||
new FieldExpression("dn", Collections.singletonList(FieldPredicate.create("*,ou=dept_h,o=forces,dc=gc,dc=ca"))),
|
||||
new FieldExpression("dn", Collections.singletonList(new FieldValue("*,ou=dept_h,o=forces,dc=gc,dc=ca"))),
|
||||
Arrays.asList("dept_h", "defence"), Collections.emptyMap(), true);
|
||||
// Does not match - user is not in this group
|
||||
final ExpressionRoleMapping mapping2 = new ExpressionRoleMapping("admin",
|
||||
new FieldExpression("groups",
|
||||
Collections.singletonList(FieldPredicate.create("cn=esadmin,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"))),
|
||||
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);
|
||||
// Does match - user is one of these groups
|
||||
final ExpressionRoleMapping mapping3 = new ExpressionRoleMapping("flight",
|
||||
new FieldExpression("groups", Arrays.asList(
|
||||
FieldPredicate.create("cn=alphaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"),
|
||||
FieldPredicate.create("cn=betaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"),
|
||||
FieldPredicate.create("cn=gammaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")
|
||||
new FieldValue(randomiseDn("cn=alphaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")),
|
||||
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);
|
||||
// Does not match - mapping is not enabled
|
||||
final ExpressionRoleMapping mapping4 = new ExpressionRoleMapping("mutants",
|
||||
new FieldExpression("groups",
|
||||
Collections.singletonList(FieldPredicate.create("cn=mutants,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"))),
|
||||
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);
|
||||
|
||||
final Client client = mock(Client.class);
|
||||
|
@ -61,7 +63,9 @@ public class NativeUserRoleMapperTests extends ESTestCase {
|
|||
final NativeRoleMappingStore store = new NativeRoleMappingStore(Settings.EMPTY, client, lifecycleService) {
|
||||
@Override
|
||||
protected void loadMappings(ActionListener<List<ExpressionRoleMapping>> listener) {
|
||||
listener.onResponse(Arrays.asList(mapping1, mapping2, mapping3, mapping4));
|
||||
final List<ExpressionRoleMapping> mappings = Arrays.asList(mapping1, mapping2, mapping3, mapping4);
|
||||
logger.info("Role mappings are: [{}]", mappings);
|
||||
listener.onResponse(mappings);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -70,15 +74,38 @@ public class NativeUserRoleMapperTests extends ESTestCase {
|
|||
|
||||
final PlainActionFuture<Set<String>> future = new PlainActionFuture<>();
|
||||
final UserRoleMapper.UserData user = new UserRoleMapper.UserData("sasquatch",
|
||||
"cn=walter.langowski,ou=people,ou=dept_h,o=forces,dc=gc,dc=ca",
|
||||
randomiseDn("cn=walter.langowski,ou=people,ou=dept_h,o=forces,dc=gc,dc=ca"),
|
||||
Arrays.asList(
|
||||
"cn=alphaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca",
|
||||
"cn=mutants,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"
|
||||
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);
|
||||
|
||||
logger.info("UserData is [{}]", user);
|
||||
store.resolveRoles(user, future);
|
||||
final Set<String> roles = future.get();
|
||||
assertThat(roles, Matchers.containsInAnyOrder("dept_h", "defence", "flight"));
|
||||
}
|
||||
|
||||
}
|
||||
private String randomiseDn(String dn) {
|
||||
// Randomly transform the dn into another valid form that is logically identical,
|
||||
// but (potentially) textually different
|
||||
switch (randomIntBetween(0, 3)) {
|
||||
case 0:
|
||||
// do nothing
|
||||
return dn;
|
||||
case 1:
|
||||
return dn.toUpperCase(Locale.ROOT);
|
||||
case 2:
|
||||
// Upper case just the attribute name for each RDN
|
||||
return Arrays.stream(dn.split(",")).map(s -> {
|
||||
final String[] arr = s.split("=");
|
||||
arr[0] = arr[0].toUpperCase(Locale.ROOT);
|
||||
return String.join("=", arr);
|
||||
}).collect(Collectors.joining(","));
|
||||
case 3:
|
||||
return dn.replaceAll(",", ", ");
|
||||
}
|
||||
return dn;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
* 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.security.authc.support.mapper.expressiondsl;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression.FieldPredicate;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
public class FieldPredicateTests extends ESTestCase {
|
||||
|
||||
public void testNullValue() throws Exception {
|
||||
final FieldPredicate predicate = FieldPredicate.create(null);
|
||||
assertThat(predicate.test(null), is(true));
|
||||
assertThat(predicate.test(""), is(false));
|
||||
assertThat(predicate.test(1), is(false));
|
||||
assertThat(predicate.test(true), is(false));
|
||||
}
|
||||
|
||||
public void testBooleanValue() throws Exception {
|
||||
final boolean matchValue = randomBoolean();
|
||||
final FieldPredicate predicate = FieldPredicate.create(matchValue);
|
||||
assertThat(predicate.test(matchValue), is(true));
|
||||
assertThat(predicate.test(!matchValue), is(false));
|
||||
assertThat(predicate.test(String.valueOf(matchValue)), is(false));
|
||||
assertThat(predicate.test(""), is(false));
|
||||
assertThat(predicate.test(1), is(false));
|
||||
assertThat(predicate.test(null), is(false));
|
||||
}
|
||||
|
||||
public void testLongValue() throws Exception {
|
||||
final int intValue = randomInt();
|
||||
final long longValue = intValue;
|
||||
final FieldPredicate predicate = FieldPredicate.create(longValue);
|
||||
|
||||
assertThat(predicate.test(longValue), is(true));
|
||||
assertThat(predicate.test(intValue), is(true));
|
||||
assertThat(predicate.test(new BigInteger(String.valueOf(longValue))), is(true));
|
||||
|
||||
assertThat(predicate.test(longValue - 1), is(false));
|
||||
assertThat(predicate.test(intValue + 1), is(false));
|
||||
assertThat(predicate.test(String.valueOf(longValue)), is(false));
|
||||
assertThat(predicate.test(""), is(false));
|
||||
assertThat(predicate.test(true), is(false));
|
||||
assertThat(predicate.test(null), is(false));
|
||||
}
|
||||
|
||||
public void testSimpleAutomatonValue() throws Exception {
|
||||
final String prefix = randomAlphaOfLength(3);
|
||||
final FieldPredicate predicate = FieldPredicate.create(prefix + "*");
|
||||
|
||||
assertThat(predicate.test(prefix), is(true));
|
||||
assertThat(predicate.test(prefix + randomAlphaOfLengthBetween(1, 5)), is(true));
|
||||
|
||||
assertThat(predicate.test("_" + prefix), is(false));
|
||||
assertThat(predicate.test(prefix.substring(0, 1)), is(false));
|
||||
|
||||
assertThat(predicate.test(""), is(false));
|
||||
assertThat(predicate.test(1), is(false));
|
||||
assertThat(predicate.test(true), is(false));
|
||||
assertThat(predicate.test(null), is(false));
|
||||
}
|
||||
|
||||
public void testEmptyStringValue() throws Exception {
|
||||
final FieldPredicate predicate = FieldPredicate.create("");
|
||||
|
||||
assertThat(predicate.test(""), is(true));
|
||||
|
||||
assertThat(predicate.test(randomAlphaOfLengthBetween(1, 3)), is(false));
|
||||
assertThat(predicate.test(1), is(false));
|
||||
assertThat(predicate.test(true), is(false));
|
||||
assertThat(predicate.test(null), is(false));
|
||||
}
|
||||
|
||||
public void testRegexAutomatonValue() throws Exception {
|
||||
final String substring = randomAlphaOfLength(5);
|
||||
final FieldPredicate predicate = FieldPredicate.create("/.*" + substring + ".*/");
|
||||
|
||||
assertThat(predicate.test(substring), is(true));
|
||||
assertThat(predicate.test(
|
||||
randomAlphaOfLengthBetween(2, 4) + substring + randomAlphaOfLengthBetween(1, 5)),
|
||||
is(true));
|
||||
|
||||
assertThat(predicate.test(substring.substring(1, 3)), is(false));
|
||||
|
||||
assertThat(predicate.test(""), is(false));
|
||||
assertThat(predicate.test(1), is(false));
|
||||
assertThat(predicate.test(true), is(false));
|
||||
assertThat(predicate.test(null), is(false));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue